5dd7e6a929402e413a6b16e0fce6b7e69d1c02ff..fbb61549bf96e9e0910b676a5524b0760d29c4be
2 天以前 WXL (wul)
测试完成
fbb615 对比 | 目录
2 天以前 WXL (wul)
测试完成
dac439 对比 | 目录
2 天以前 WXL (wul)
测试完成
a2c10d 对比 | 目录
6 天以前 WXL (wul)
测试完成
055c1f 对比 | 目录
7 天以前 WXL (wul)
测试完成
509e61 对比 | 目录
7 天以前 WXL (wul)
测试完成
bb0a72 对比 | 目录
7 天以前 WXL (wul)
测试完成
83928a 对比 | 目录
8 天以前 WXL (wul)
测试完成
55f987 对比 | 目录
9 天以前 WXL (wul)
测试完成
01d3b0 对比 | 目录
2026-03-30 WXL (wul)
测试完成
033489 对比 | 目录
2026-03-27 WXL (wul)
测试完成
d0c137 对比 | 目录
2026-03-27 WXL (wul)
测试完成
4c5d48 对比 | 目录
2026-03-26 WXL (wul)
测试完成
0c7cc2 对比 | 目录
2026-03-14 WXL (wul)
测试完成
21051d 对比 | 目录
2026-02-04 WXL (wul)
测试完成
5a554d 对比 | 目录
2026-02-04 WXL (wul)
测试完成
756ea2 对比 | 目录
2026-02-03 WXL (wul)
测试完成
5c16dc 对比 | 目录
2026-02-02 WXL (wul)
测试完成
7c71fc 对比 | 目录
2026-01-26 WXL (wul)
测试完成
ba1ad6 对比 | 目录
2026-01-19 WXL (wul)
测试完成
3d4123 对比 | 目录
2026-01-12 WXL (wul)
测试完成
6dec47 对比 | 目录
2026-01-06 WXL (wul)
测试完成
8dfc91 对比 | 目录
2026-01-06 WXL (wul)
测试完成
17efc8 对比 | 目录
2025-12-18 WXL (wul)
测试完成
e38536 对比 | 目录
2025-12-16 WXL (wul)
测试完成
3cf6f5 对比 | 目录
2025-12-12 WXL (wul)
测试完成
1feb41 对比 | 目录
2025-12-05 WXL (wul)
测试完成
22bd20 对比 | 目录
2025-12-02 WXL (wul)
测试完成
43017c 对比 | 目录
2025-12-02 WXL (wul)
测试完成
f08b75 对比 | 目录
2025-12-02 WXL (wul)
测试完成
0bda1b 对比 | 目录
2025-11-22 WXL (wul)
宣教更新
3573a1 对比 | 目录
2025-11-21 WXL (wul)
测试完成
3b5ff0 对比 | 目录
2025-11-17 WXL (wul)
测试完成
e0909a 对比 | 目录
已删除4个文件
已修改62个文件
已添加22个文件
63034 ■■■■■ 文件已修改
SLTD-WL.zip 补丁 | 查看 | 原始文档 | blame | 历史
Xhyy.zip 补丁 | 查看 | 原始文档 | blame | 历史
dist.zip 补丁 | 查看 | 原始文档 | blame | 历史
package.json 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
public/demotel.html 203 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
public/jssip-3.10.0.js 28403 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/App.vue 29 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/AiCentre/EChartsdata.js 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/AiCentre/Followup.js 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/AiCentre/Qtemplate.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/AiCentre/SingleTask.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/AiCentre/phoneCall.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/AiCentre/questionnaire.js 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/login.js 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/smartor/patouthosp.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/user.js 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AskRegular/index.vue 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Assistant/index.vue 1132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/CallButton/index.vue 62 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Regular/index.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SortCheckbox/index.vue 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Navbar.vue 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/plugins/auth.js 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 237 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/getters.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/permission.js 194 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/user.js 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/request.js 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/sipService.js 253 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/configurationmyd/batch.vue 922 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/configurationmyd/components/DetailsAnomaly.vue 923 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/configurationmyd/dispose.vue 980 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/configurationmyd/index.vue 2376 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/particulars/index.vue 889 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/sfstatistics/IndicatorStatistics.vue 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/sfstatistics/components/FollowupStatistics.vue 1030 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/sfstatistics/components/SatisfactionStatistics.vue 1809 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/sfstatistics/components/components/SeedetailsDialog.vue 401 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/sfstatistics/components/components/TopicDialog.vue 208 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/sfstatistics/components/visitStatistics.vue 1030 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/sfstatistics/index.vue 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/Continue/ContinueFordetails.vue 2389 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/Continue/index.vue 2103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/HistoricalFollow/index.vue 1327 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/OutpatientAgain/index.vue 97 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/SpecificDisease/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/Tracking/index.vue 1793 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/again/index.vue 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/complaint/index.vue 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/discharge/index.vue 189 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/discharge/js/prototype.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/discharge/outpatientService.vue 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/mzsatisfaction/index.vue 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/outpatient/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/record/TracingInfo/index.vue 2971 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/record/detailpage/index copy.vue 3703 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/record/detailpage/index.vue 1423 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/record/index.vue 90 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/record/physical/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/satisfaction/index.vue 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/technology/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/zbAgain/index.vue 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/zysatisfaction/index.vue 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/index.vue 44 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/knowledge/questionbank/particulars/index.vue 162 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/login-sy.vue 322 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/login.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/outsideChainwtnew.vue 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/patient/patient/AwaitingAdmission.vue 224 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/patient/patient/behospitalized.vue 725 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/patient/patient/hospital.vue 669 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/patient/patient/indexls.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/patient/physical/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/patient/propaganda/Missioncreation.vue 538 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/patient/propaganda/QuestionnaireTask.vue 128 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/patient/propaganda/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/patient/propaganda/particty.vue 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/patient/questionnaire/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/patient/shadow/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/patient/subsequent/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/repositoryai/templateku/configurat/index.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sfstatistics/ProblemStatistics/index.vue 536 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sfstatistics/percentage/index.vue 1140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sfstatistics/percentage/satisfaction.vue 408 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vue.config.js 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xinhua.zip 补丁 | 查看 | 原始文档 | blame | 历史
SLTD-WL.zip
Binary files differ
Xhyy.zip
Binary files differ
dist.zip
Binary files differ
package.json
@@ -1,7 +1,7 @@
{
  "name": "ruoyi",
  "version": "3.8.5",
  "description": "景宁人民医院智慧随访平台",
  "description": "丽水人民医院智慧随访平台",
  "author": "杭新",
  "license": "MIT",
  "scripts": {
@@ -52,6 +52,7 @@
    "dayjs": "^1.11.7",
    "echarts": "^5.4.2",
    "element-ui": "^2.15.4",
    "exceljs": "^4.4.0",
    "file-saver": "^2.0.5",
    "fuse.js": "6.4.3",
    "highlight.js": "9.18.5",
@@ -81,11 +82,13 @@
    "vue-count-to": "1.0.13",
    "vue-cropper": "0.5.5",
    "vue-meta": "2.4.0",
    "vue-print-nb": "^1.7.5",
    "vue-quill-editor": "^3.0.6",
    "vue-router": "3.4.9",
    "vuedraggable": "^2.24.3",
    "vuex": "3.6.0",
    "xlsx": "^0.18.5"
    "xlsx": "^0.18.5",
    "xlsx-js-style": "^1.2.0"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "4.4.6",
public/demotel.html
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,203 @@
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>SIP电话Demo</title>
    <meta http-equiv="Pragma" content="no-cache">
    <meta http-equiv="Expires" content="-1">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta http-equiv="cache-control" content="no-store">
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <script language="javascript" type="text/javascript" src="./jssip-3.10.0.js"></script>
</head>
<body style="background-color:darkgray;">
    <table border="1" cellspacing="0" bordercolor="gray" style="width:auto;">
        <tr>
            <td style="text-align:center;width:120px;">
                <span> æœåŠ¡å™¨åœ°å€:</span>
            </td>
            <td style="text-align: center; width: 250px;">
                <input id="tbServer" type="text" style="width:200px;text-align:left;" value="192.168.1.96" />
            </td>
        </tr>
        <tr>
            <td style="text-align:center;width:120px;">
                <span> åˆ†æœºå·:</span>
            </td>
            <td style="text-align: center; width: 250px;">
                <input id="tbExtension" type="text" style="width:200px;text-align:left;" value="8005" />
            </td>
        </tr>
        <tr>
            <td style="text-align:center;width:120px;">
                <span> å¯†ç :</span>
            </td>
            <td style="text-align: center; width: 250px;">
                <input id="tbPassword" type="text" style="width:200px;text-align:left;" value="8005" />
            </td>
        </tr>
        <tr>
            <td style="text-align:center;width:120px;" colspan="2">
                <button onclick="f_register()">注册</button>
            </td>
        </tr>
        <tr>
            <td style="text-align:center;width:120px;">
                <span> å‘¼å«å·ç :</span>
            </td>
            <td style="text-align: center; width: 250px;">
                <input id="tbPhoneNo" type="text" style="width:200px;text-align:left;" value="013958077789" />
            </td>
        </tr>
        <tr>
            <td style="text-align:center;width:120px;" colspan="2">
                <button onclick="f_makecall()">软外拨</button>
                <button onclick="f_hangup()">挂断</button>
            </td>
        </tr>
    </table>
    <script type="text/javascript">
        var cur_session = undefined;
        var ua = null;
        String.prototype.Right = function (i) {
            return this.slice(this.length - i, this.length);
        };
        Date.prototype.Format = function (fmt) { //author: meizz
            var o = {
                "M+": this.getMonth() + 1, //月份
                "d+": this.getDate(), //日
                "h+": this.getHours(), //小时
                "m+": this.getMinutes(), //分
                "s+": this.getSeconds(), //秒
                "q+": Math.floor((this.getMonth() + 3) / 3), //季度
                "S": ('00' + this.getMilliseconds()).Right(3)    //毫秒
            };
            if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
            for (var k in o)
                if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
            return fmt;
        };
        function logger_info(message, ...optionalParams) {
            console.log("[" + new Date().Format("yyyy-MM-dd hh:mm:ss.S") + "]" + message, optionalParams);
        }
        function f_register() {
            var server = document.getElementById('tbServer').value;
            var username = document.getElementById('tbExtension').value;
            var passwd = document.getElementById('tbPassword').value;
            var uristr = 'sip:' + username;
            if (uristr.indexOf('@') < 1)
                uristr = uristr + '@' + server;
            //
            logger_info(`开始注册到服务器:${server},分机号:${username}...`);
            var socket = new JsSIP.WebSocketInterface('wss://' + server +':7443');
            var configuration = {
                sockets: [socket],
                uri: uristr,
                password: passwd,
                display_name: username,
                iceServers: [
                    { urls: '' }
                ]
            };
            ua = new JsSIP.UA(configuration);
            ua.on('registered', function (e) { logger_info("注册成功。", e); });
            ua.on('unregistered', function (e) { logger_info('取消注册。', e); });
            ua.on('registrationFailed', function (e) { logger_info('注册失败的。', e); });
            ua.on('newRTCSession', function (e) {
                logger_info('newRTCSession', e);
                //
                cur_session = e.session;
                cur_session.on("accepted", function (e) { logger_info("呼叫已接通。", e); });
                cur_session.on("ended", function (e) { logger_info("呼叫结束。", e); });
                cur_session.oniceconnectionstatechange = function () {
                    if (cur_session.connection.iceConnectionState === 'completed') {
                        // èŽ·å–æœ¬åœ°SDP
                        const localSdp = cur_session.connection.localDescription;
                        // ä¿®æ”¹SDP(这里以修改音频编解码器优先级为例)
                        const sdp = localSdp.sdp;
                        const modifiedSdp = sdp.replace(/m=audio (\d+) RTP\/AVP (\d+) (.*)/g, (match, port, payloadTypes, formats) => {
                            // å‡è®¾æˆ‘们想将PCMU编解码器的优先级提高
                            const preferredPayloadType = '0'; // PCMU的payload type
                            const preferredFormat = formats.split(' ').find(format => format.startsWith(preferredPayloadType));
                            if (preferredFormat) {
                                const newFormats = formats.split(' ').filter(format => format !== preferredFormat).concat(preferredFormat);
                                return `m=audio ${port} RTP/AVP ${newFormats.join(' ')}`;
                            }
                            return match;
                        });
                        // åˆ›å»ºæ–°çš„RTCSessionDescription对象
                        const newDescription = new RTCSessionDescription({
                            type: localSdp.type,
                            sdp: modifiedSdp
                        });
                        // è®¾ç½®ä¿®æ”¹åŽçš„SDP
                        cur_session.connection.setLocalDescription(newDescription).catch(error => {
                            logger_info('Failed to set local description:', error);
                        });
                    }
                }
                //
                if (e.originator == "remote")
                    e.session.answer();
            });
            ua.start();
        }
        function f_hangup() {
            if (cur_session != undefined) {
                cur_session.terminate();
                cur_session = undefined;
            }
        }
        function f_makecall() {
            if (ua == undefined)
                return;
            //
            var eventHandlers = {
                'progress': function (e) {
                    logger_info('call is in progress', e);
                },
                'failed': function (e) {
                    logger_info('call failed with cause: ', e);
                },
                'ended': function (e) {
                    logger_info('呼叫结束,挂机原因: ', e);
                },
                'confirmed': function (e) {
                    logger_info('call confirmed', e);
                }
            };
            var options = {
                'eventHandlers': eventHandlers,
                'mediaConstraints': { 'audio': true, 'video': false },
                sessionTimersExpires: 1800
            };
            var server = document.getElementById('tbServer').value;
            var phone = document.getElementById('tbPhoneNo').value;
            logger_info("开始呼叫号码:", phone);
            cur_session = ua.call('sip:' + phone + '@' + server, options);
        }
    </script>
</body>
</html>
public/jssip-3.10.0.js
¶Ô±ÈÐÂÎļþ
ÎļþÌ«´ó
src/App.vue
@@ -3,6 +3,13 @@
    <router-view />
    <theme-picker />
    <!-- <Assistant v-if="routertf" /> -->
    <Assistant
      v-if="Assvite"
      :initial-position="{ x: 50, y: 200 }"
      :auto-hide="false"
      :hide-delay="3000"
      primary-color="#1890ff"
    />
  </div>
</template>
@@ -13,17 +20,29 @@
  name: "App",
  components: {
    ThemePicker,
    Assistant: () => import("./components/Assistant"), //异步组件加载方式
    Assistant: () => import("./components/Assistant"),
  },
  data() {
    return {
      routers: window.location.href,
      routertf: true,
      Assvite: true,
    };
  },
  created() {
    var startIndex = this.routers.indexOf("param5=") + "param5=".length; // æ‰¾åˆ°ç¬¬ä¸€ä¸ªå­—符的位置
    this.routertf = JSON.parse(this.routers.substring(startIndex)); // æˆªå–从 'param5=' ä¹‹åŽçš„内容
    // åˆå§‹åŒ–判断
    this.checkAndUpdateAssvite();
  },
  watch: {
    // ç›‘听路由变化
    '$route'(to, from) {
      this.checkAndUpdateAssvite();
    }
  },
  methods: {
    checkAndUpdateAssvite() {
      const isLoginPage = window.location.pathname.includes("/login");
      this.Assvite = !isLoginPage;
      console.log('当前路由:', this.$route.path, '是否登录页:', isLoginPage, '显示悬浮球:', this.Assvite);
    }
  },
  metaInfo() {
    return {
src/api/AiCentre/EChartsdata.js
@@ -1,6 +1,5 @@
import request from "@/utils/request";
// æŸ¥è¯¢é—¨è¯Šçœ‹ç—…人次和人数
export function getEChartsPatMedOuthospCount(data) {
  return request({
@@ -65,6 +64,7 @@
  return request({
    url: "/smartor/organization/list",
    method: "get",
    params: data,
  });
}
// å‘送短信
@@ -72,6 +72,14 @@
  return request({
    url: "/sms/send",
    method: "post",
    data: data
    data: data,
  });
}
// å‘送短信
export function getCurrentUserServiceSubtaskCount(data) {
  return request({
    url: "/smartor/serviceSubtask/getCurrentUserServiceSubtaskCount",
    method: "post",
    data: data,
  });
}
src/api/AiCentre/Followup.js
@@ -45,7 +45,13 @@
    // data: data,
  });
}
export function getTaskFollowupList(data) {
  return request({
    url: "/smartor/ivrTaskTemplate/list",
    method: "post",
    data: data,
  });
}
// æ–°å¢žéšè®¿æ¨¡æ¿åˆ†ç±»æ ‘
export function addFollowupclassify(data) {
  return request({
src/api/AiCentre/Qtemplate.js
@@ -9,6 +9,14 @@
    data: data,
  });
}
// æŸ¥è¯¢é—®å·æ¨¡æ¿åˆ—表
export function taskgetQtemplateobj(data) {
  return request({
    url: "smartor/svytemplateTask/selectInfoByCondition",
    method: "post",
    data: data,
  });
}
// é—®å·æ¨¡æ¿åˆ—表
export function getQtemplatelist(data) {
  return request({
@@ -224,3 +232,11 @@
    data: data,
  });
}
// åŒ»æŠ¤ä¿å­˜æ•°æ®
export function savequestiondetail(data) {
  return request({
    url: "/smartor/subtaskAnswer/savequestiondetail",
    method: "post",
    data: data,
  });
}
src/api/AiCentre/SingleTask.js
@@ -104,6 +104,14 @@
    data: data,
  });
}
// è¯Šæ–­æŸ¥è¯¢åŽ†å²æœåŠ¡åˆ—è¡¨
export function historservelist(data) {
  return request({
    url: "/smartor/serviceSubtask/getSubtaskByDiagname",
    method: "post",
    data: data,
  });
}
// å¿«æ·æŸ¥è¯¢ä»»åŠ¡åˆ—è¡¨
export function buidegetTasklist(data) {
  return request({
@@ -128,6 +136,14 @@
    method: "get",
  });
}
// èŽ·å–è¯­éŸ³ä»»åŠ¡æ¨¡æ¿è¯¦æƒ…
export function selectInfoByCondition(data) {
  return request({
     url: "/smartor/ivrTaskTemplate/selectInfoByCondition",
    method: "post",
    data: data,
  });
}
// ä»»åŠ¡æ¨¡æ¿æ–°å¢žä¿®æ”¹
export function TaskTemplatecomit(data) {
  return request({
src/api/AiCentre/phoneCall.js
@@ -10,7 +10,7 @@
    },
  });
}
// æŸ¥è¯¢å¤–部患者表
// æ›´æ–°åˆ†çº§å·
export function CallsetState(data) {
  return request({
    url: "/smartor/ServiceTelInfo/setState",
src/api/AiCentre/questionnaire.js
@@ -30,7 +30,14 @@
      data: data,
    });
  }
// æŸ¥è¯¢é—®é¢˜ç»Ÿè®¡
export function compileissuestatistics(data) {
    return request({
      url: "/smartor/svytemplatescript/countPatByScript",
      method: "post",
      data: data,
    });
  }
  // æ–°å¢žé—®å·é—®é¢˜åˆ†ç±»
  export function addissueclassify(data) {
src/api/login.js
@@ -1,12 +1,13 @@
import request from "@/utils/request";
// ç™»å½•方法
export function login(username, password, code, orgid) {
export function login(username, password, code, orgid,campusid) {
  const data = {
    username,
    password,
    code,
    orgid,
    campusid,
  };
  return request({
    url: "/login",
src/api/smartor/patouthosp.js
@@ -8,6 +8,14 @@
    data: query
  })
}
// æŸ¥è¯¢å¾…入院记录列表
export function listPatMedInhosp(query) {
  return request({
    url: '/smartor/patinhosp/selectPatMedInhospList',
    method: 'post',
    data: query
  })
}
// æŸ¥è¯¢æ‚£è€…门诊记录详细
export function getPatouthosp(id) {
src/api/system/user.js
@@ -94,17 +94,18 @@
    data: data,
  });
}
// æ»¡æ„åº¦æ˜Žç»†æŸ¥è¯¢
export function getSfStatisticsJoydetails(data) {
// æ»¡æ„åº¦ç»Ÿè®¡
export function getSfStatisticsJoy(data) {
  return request({
    url: "/smartor/serviceSubtask/getSfStatisticsJoydetails",
    url: "/smartor/serviceSubtask/getSfStatisticsCount",
    method: "post",
    data: data,
  });
}// æ»¡æ„åº¦ç»Ÿè®¡
export function getSfStatisticsJoy(data) {
}
// æ»¡æ„åº¦ç»Ÿè®¡è¯¦æƒ…
export function getSfStatisticsJoyInfo(data) {
  return request({
    url: "/smartor/serviceSubtask/getSfStatisticsJoy",
    url: "/smartor/serviceSubtask/getSfStatisticsCountDetails",
    method: "post",
    data: data,
  });
@@ -119,7 +120,7 @@
}
// ä»»åŠ¡éšè®¿çŽ‡ç»Ÿè®¡è¡¨çš„ä¸‹é’»æ˜Žç»†
// sendstate = 2 å¾…随访 5 å¾…随访失败
// sendstate = 2 å¾…随访 5 å¾…随访失败
// preachform = ä»»åŠ¡å½¢å¼(1,人工 2,纸质  3,电话  4,短信  5.微信公众号 6.微信小程序 7.支付宝小程序  8.智能机器人  9.钉钉)
export function querySubtaskList(data) {
  return request({
@@ -195,3 +196,19 @@
    data: data,
  });
}
// çœç«‹åŒå¾·æ»¡æ„åº¦ç»Ÿè®¡é¡µé¢˜ç›®æ˜Žç»†
export function statistics(data) {
  return request({
    url: "/smartor/satisfaction/statistics",
    method: "post",
    data: data,
  });
}
// çœç«‹åŒå¾·æ»¡æ„åº¦ç»Ÿè®¡é¡µé¢˜ç›®æ˜Žç»†
export function satisfactionGraph(data) {
  return request({
    url: "/smartor/satisfaction/satisfactionGraph",
    method: "post",
    data: data,
  });
}
src/components/AskRegular/index.vue
@@ -14,10 +14,18 @@
        ></el-col>
        <el-col :span="12"
          ><el-form-item label="异常提醒">
            <el-radio-group v-model="radio">
              <el-radio :label="3">是</el-radio>
              <el-radio :label="6">否</el-radio>
            </el-radio-group>
            <el-select v-model="item.isabnormal" placeholder="请选择状态">
              <el-option :value="0" label="正常" :style="{ color: '#67C23A' }">
                <span style="color: #67c23a">● æ­£å¸¸</span>
              </el-option>
              <el-option :value="2" label="警告" :style="{ color: '#FFBA00' }">
                <span style="color: #ffba00">● è­¦å‘Š</span>
              </el-option>
              <el-option :value="1" label="异常" :style="{ color: '#f75c5c' }">
                <span style="color: #f75c5c">● å¼‚常</span>
              </el-option>
            </el-select>
          </el-form-item></el-col
        >
        <el-col :span="12" v-if="intent"
src/components/Assistant/index.vue
@@ -1,479 +1,809 @@
<template>
  <div
    ref="floatDrag"
    class="float-position"
    id="float-box"
    :style="{
      left: left + 'px',
      top: top + 'px',
      right: right + 'px !important',
      zIndex: zIndex,
    ref="floatBall"
    class="float-ball"
    :class="{
      'float-ball-hidden': isHidden && !isHovering,
      'float-ball-expanded': isExpanded,
    }"
    @touchmove.prevent
    @mousemove.prevent
    @mousedown="mouseDown"
    @mouseup="mouseUp"
    :style="{
      left: position.x + 'px',
      top: position.y + 'px',
      '--primary-color': primaryColor,
      '--hover-color': hoverColor,
    }"
    @mouseenter="handleMouseEnter"
    @mouseleave="handleMouseLeave"
  >
    <div class="drag">
      <svg
        t="1682058484158"
        class="icon"
        viewBox="0 0 1024 1024"
        version="1.1"
        xmlns="http://www.w3.org/2000/svg"
        p-id="2023"
        width="32"
        height="32"
      >
        <path
          d="M556.297 172.715a42.407 42.407 0 0 1 42.426 42.398l0.837 267.69c-0.118 1.703 0.63 2.737 1.408 2.737 0.63 0 1.29-0.699 1.506-2.284l37.74-208.953c3.732-20.672 21.844-36.166 42.162-36.166a40.074 40.074 0 0 1 7.136 0.64c23.064 4.164 38.391 27.562 34.217 50.587l-33.656 244.529c0 2.559 0.483 4.478 1.32 4.478 0.58 0 1.328-0.935 2.175-3.218l50.144-134.063c6.27-17.65 23.034-29.403 40.793-29.403A39.798 39.798 0 0 1 797.892 374c22.08 7.875 33.626 33.41 25.78 55.47l-87.904 287.191c-0.453 1.585-0.984 3.16-1.437 4.725l-0.187 0.591v0.128a187.031 187.031 0 0 1-177.847 129.1c-53.156 0-108.42-18.752-150.472-51-45.419-27.336-190.968-183.783-190.968-183.783-22.09-22.07-18.792-55.882 3.297-77.962 11.537-11.537 25.919-17.6 40.173-17.6 13.033 0 25.967 5.05 36.51 15.592l63.138 63.157c8.603 8.594 18.132 12.699 26.922 12.699a26.952 26.952 0 0 0 20.88-9.893c7.658-9.037 4.635-36.914 2.49-54.594l-31.668-260.259c-2.825-23.26 13.781-45.724 37.003-48.549a40.497 40.497 0 0 1 4.853-0.295c21.282 0 39.749 16.98 42.387 38.597l34.926 204.425c0.905 2.54 2.342 4.036 3.602 4.036s2.353-1.496 2.58-4.922l11.88-265.741a42.417 42.417 0 0 1 42.467-42.398m0-70.875a113.36 113.36 0 0 0-104.344 69.153c-0.246 0.57-0.482 1.152-0.718 1.732a111.234 111.234 0 0 0-90.022 10.976 113.597 113.597 0 0 0-32.415 29.207 115.23 115.23 0 0 0-19.067 38.489 113.843 113.843 0 0 0-3.465 44.68l21.36 175.77a120.842 120.842 0 0 0-69.3-21.863c-33.468 0-65.549 13.614-90.286 38.332-23.212 23.202-36.993 53.363-38.863 84.952a120.92 120.92 0 0 0 34.502 92.216c5.532 5.906 39.64 42.407 79.203 82.412 74.586 75.422 105.328 99.648 122.702 110.771 53.973 40.36 123.254 63.414 190.674 63.414A257.906 257.906 0 0 0 801.14 745.1c0.247-0.709 0.483-1.417 0.7-2.136l0.117-0.374a178.56 178.56 0 0 0 1.723-5.64l87.413-285.578a113.203 113.203 0 0 0 5.729-42.86 115.585 115.585 0 0 0-35.772-77.135 111.431 111.431 0 0 0-67.45-30.19l0.148-0.985a113.676 113.676 0 0 0-1.201-43.155 115.408 115.408 0 0 0-16.872-39.523 113.774 113.774 0 0 0-30.703-30.968 111.077 111.077 0 0 0-84.981-17.06 113.203 113.203 0 0 0-103.694-67.656z"
          fill="#ffffff"
          p-id="2024"
        ></path>
      </svg>
    </div>
    <div class="content" id="content" @click="handelFlex">
      <!-- <img src="../../../../assets/image/alarm.png" alt="" /> -->
      <div class="label">
        <div v-if="flag">展开</div>
        <div v-else>收起</div>
      </div>
      <div class="item-container">
        <div
          v-for="(item, index) in powerList"
          :key="index"
          @click.stop="activeHandle(index,item.url)"
    <!-- ä¸»çƒä½“ -->
    <div
      class="ball-main"
      :class="{ 'ball-main-expanded': isExpanded }"
      @click="toggleExpand"
      @mousedown="startDrag"
      @touchstart="startDrag"
    >
      <!-- æŠ˜å çŠ¶æ€å›¾æ ‡ -->
      <div v-if="!isExpanded" class="ball-icon">
        <svg
          class="fold-icon"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="2"
        >
          <path d="M4 6h16M4 12h16M4 18h16" />
        </svg>
      </div>
      <!-- å±•开状态关闭按钮 -->
      <div v-else class="close-btn" @click.stop="toggleExpand">
        <svg
          class="close-icon"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="2"
        >
          <path d="M6 18L18 6M6 6l12 12" />
        </svg>
      </div>
      <!-- è§’标提示(有未读数时显示) -->
      <div v-if="totalUnread > 0" class="ball-badge">
        {{ totalUnread > 99 ? "99+" : totalUnread }}
      </div>
    </div>
    <!-- å±•开的内容面板 -->
    <transition name="ball-expand">
      <div v-if="isExpanded" class="ball-content">
        <div class="content-header">
          <h3>随访工作台</h3>
          <div class="update-time">更新于 {{ updateTime }}</div>
        </div>
        <div class="stats-grid">
          <div
            :class="activeIndex == index ? 'active power-item' : 'power-item'"
            v-for="(item, index) in statsItems"
            :key="index"
            class="stat-item"
            :class="{ 'stat-item-highlight': item.highlight }"
            @click="handleItemClick(item)"
          >
            <img :src="item.path" alt="" style="width: 26px" />
            <div class="stat-icon">
              <svg
                v-if="item.icon === 'IconUsers'"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                stroke-width="2"
              >
                <path
                  d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13-7.157a4 4 0 11-8 0 4 4 0 018 0z"
                />
              </svg>
              <svg
                v-else-if="item.icon === 'IconAlertCircle'"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                stroke-width="2"
              >
                <path d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
              </svg>
              <svg
                v-else-if="item.icon === 'IconTask'"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                stroke-width="2"
              >
                <path
                  d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
                />
              </svg>
            </div>
            <div class="stat-info">
              <div class="stat-label">{{ item.label }}</div>
              <div class="stat-value">{{ item.value }}</div>
              <div
                v-if="item.trend"
                class="stat-trend"
                :class="'trend-' + item.trend.type"
              >
                <span class="trend-arrow">{{ item.trend.arrow }}</span>
                <span class="trend-value">{{ item.trend.value }}</span>
              </div>
            </div>
            <div v-if="item.unread > 0" class="stat-badge">
              {{ item.unread > 99 ? "99+" : item.unread }}
            </div>
          </div>
          <div :class="activeIndex == index ? 'active-des des' : 'des'">
            {{ item.label }}
        </div>
        <div class="quick-actions">
          <div
            v-for="(action, index) in quickActions"
            :key="index"
            class="action-item"
            @click="handleActionClick(action)"
          >
            <div class="action-icon">
              <svg
                v-if="action.icon === 'IconMessageCircle'"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                stroke-width="2"
              >
                <path
                  d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
                />
              </svg>
              <svg
                v-else-if="action.icon === 'IconPhone'"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                stroke-width="2"
              >
                <path
                  d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
                />
              </svg>
            </div>
            <div class="action-label">{{ action.label }}</div>
          </div>
        </div>
      </div>
    </div>
    </transition>
  </div>
</template>
<script>
import { getCurrentUserServiceSubtaskCount } from "@/api/AiCentre/index";
export default {
  name: "DragBall",
  name: "FloatBall",
  props: {
    distanceRight: {
      type: Number,
      default: 36,
    // åˆå§‹ä½ç½®
    initialPosition: {
      type: Object,
      default: () => ({ x: 20, y: 100 }),
    },
    distanceBottom: {
      type: Number,
      default: 600,
    },
    isScrollHidden: {
      type: Boolean,
      default: false,
    },
    isCanDraggable: {
    // æ˜¯å¦è‡ªåŠ¨éšè—
    autoHide: {
      type: Boolean,
      default: true,
    },
    zIndex: {
    // éšè—å»¶è¿Ÿï¼ˆæ¯«ç§’)
    hideDelay: {
      type: Number,
      default: 50,
      default: 2000,
    },
    value: {
    // ä¸»é¢˜é¢œè‰²
    primaryColor: {
      type: String,
      default: "悬浮球!",
      default: "#4f46e5",
    },
    // æ‚¬åœé¢œè‰²
    hoverColor: {
      type: String,
      default: "#4338ca",
    },
    // æ•°æ®æºï¼ˆå¯ä»Žå¤–部传入)
    statsData: {
      type: Object,
      default: null,
    },
  },
  data() {
    return {
      clientWidth: null,
      clientHeight: null,
      left: null,
      top: null,
      right: null,
      timer: null,
      currentTop: 0,
      mousedownX: 0,
      mousedownY: 0,
      isExpanded: false,
      isHovering: false,
      isHidden: false,
      isDragging: false,
      position: { ...this.initialPosition },
      dragStart: { x: 0, y: 0 },
      hideTimer: null,
      updateTime: "",
      roles: null,
      // ç»Ÿè®¡æ•°æ®
      statsItems: [
        {
          id: "pending",
          label: "待随访",
          value: "0",
          unread: 0,
          urltype: 2,
          icon: "IconUsers",
          url: "/followvisit/discharge",
          trend: { type: "up", arrow: "", value: "" },
          highlight: true,
        },
        {
          id: "failed",
          label: "随访失败",
          value: "0",
          unread: 0,
          urltype: 3,
          icon: "IconAlertCircle",
          url: "/followvisit/discharge",
          trend: { type: "down", arrow: "", value: "" },
        },
        {
          id: "abnormal",
          label: "任务异常",
          value: "0",
          unread: 0,
          urltype: 4,
          icon: "IconAlertCircle",
          url: "/followvisit/discharge",
          trend: { type: "up", arrow: "", value: "" },
        },
        {
          id: "myTasks",
          label: "我的任务",
          value: "0",
          unread: 0,
          urltype: 5,
          icon: "IconTask",
          url: "/followvisit/discharge",
          trend: { type: "stable", arrow: "", value: "" },
        },
      ],
      flag: true, // æŽ§åˆ¶æ‚¬æµ®æ¡†æ˜¯å¦å±•å¼€
      box: "", // æ‚¬æµ®çƒçš„dom
      activeIndex: 0, //高亮显示
      powerList: [
      // å¿«æ·æ“ä½œ
      quickActions: [
        {
          path: require("@/assets/images/huanzheliebiao.png"),
          url:'/patient/patient/',
          label: "患者",
          id: "sms",
          label: "创建问卷任务",
          icon: "IconMessageCircle",
          url: "/followvisit/QuestionnaireTask?type=2&serviceType=2",
        },
        {
          path: require("@/assets/images/fwwu.png"),
          url:'/followvisit/tasklist/',
          label: "任务",
          id: "call",
          label: "创建语音任务",
          icon: "IconPhone",
          url: "/followvisit/particty?type=1&serviceType=2",
        },
        {
          path: require("@/assets/images/duanxinjilu.png"),
          url:'',
          label: "短信",
        },
        {
          path: require("@/assets/images/dianhua.png"),
          url:'',
          label: "电话",
        },
        {
          path: require("@/assets/images/zxlt.png"),
          url:'',
          label: "在线聊天",
        },
        // {
        //   id: 'chat',
        //   label: '在线聊天',
        //   icon: 'IconMessageCircle',
        //   url: '/chat'
        // }
      ],
    };
  },
  created() {
    this.clientWidth = document.documentElement.clientWidth;
    this.clientHeight = document.documentElement.clientHeight;
  computed: {
    totalUnread() {
      return this.statsItems.reduce((sum, item) => sum + item.unread, 0);
    },
  },
  mounted() {
    this.isCanDraggable &&
      this.$nextTick(() => {
        this.floatDrag = this.$refs.floatDrag;
        // èŽ·å–å…ƒç´ ä½ç½®å±žæ€§
        this.floatDragDom = this.floatDrag.getBoundingClientRect();
        // è®¾ç½®åˆå§‹ä½ç½®
        this.left = this.clientWidth - this.floatDragDom.width - this.distanceRight;
        // this.right = 0;
        this.top =
          this.clientHeight - this.floatDragDom.height - this.distanceBottom;
        this.initDraggable();
      });
    // this.isScrollHidden && window.addEventListener('scroll', this.handleScroll);
    this.roles = this.$store.state.user.roles;
    this.loadPosition();
    if (this.autoHide) {
      this.startAutoHide();
    }
    // ç‚¹å‡»å¤–部关闭
    document.addEventListener("click", this.handleClickOutside);
    // çª—口大小变化时重新定位
    window.addEventListener("resize", this.handleResize);
    this.box = document.getElementById("float-box");
  },
  beforeUnmount() {
    window.removeEventListener("scroll", this.handleScroll);
  beforeDestroy() {
    document.removeEventListener("click", this.handleClickOutside);
    window.removeEventListener("resize", this.handleResize);
    clearTimeout(this.hideTimer);
  },
  methods: {
    // ä¼¸ç¼©æ‚¬æµ®çƒ
    handelFlex() {
      if (this.flag) {
        this.buffer(this.box, "height", 600);
      } else {
        this.buffer(this.box, "height", 70);
      }
      this.flag = !this.flag;
      console.log("是否展开", this.flag);
    },
    // ç‚¹å‡»å“ªä¸ªpower
    activeHandle(index,url) {
      //把我们自定义的下标赋值
      this.activeIndex = index;
      this.$router.push({
        path: url,
      })
      console.log("HHHH", index);
    },
    // èŽ·å–è¦æ”¹å˜å¾—æ ·å¼å±žæ€§
    getStyleAttr(obj, attr) {
      if (obj.currentStyle) {
        // IE å’Œ opera
        return obj.currentStyle[attr];
      } else {
        return window.getComputedStyle(obj, null)[attr];
      }
    },
    // åŠ¨ç”»å‡½æ•°
    buffer(eleObj, attr, target) {
      // setInterval方式开启动画
      //先清后设
      // clearInterval(eleObj.timer);
      // let speed = 0
      // let begin = 0
      // //设置定时器
      // eleObj.timer = setInterval(() => {
      //     //获取动画属性的初始值
      //     begin = parseInt(this.getStyleAttr(eleObj, attr));
      //     speed = (target - begin) * 0.2;
      //     speed = target > begin ? Math.ceil(speed) : Math.floor(speed);
      //     eleObj.style[attr] = begin + speed + "px";
      //     if (begin === target) {
      //         clearInterval(eleObj.timer);
      //     }
      // }, 20);
      // cancelAnimationFrame开启动画
      // å…ˆæ¸…后设
      cancelAnimationFrame(eleObj.timer);
      let speed = 0;
      let begin = 0;
      let _this = this;
      eleObj.timer = requestAnimationFrame(function fn() {
        begin = parseInt(_this.getStyleAttr(eleObj, attr));
        // åŠ¨ç”»é€Ÿåº¦
        speed = (target - begin) * 0.9;
        speed = target > begin ? Math.ceil(speed) : Math.floor(speed);
        eleObj.style[attr] = begin + speed + "px";
        eleObj.timer = requestAnimationFrame(fn);
        if (begin === target) {
          cancelAnimationFrame(eleObj.timer);
        }
      });
    },
    /**
     * çª—口resize监听
     */
    handleResize() {
      // this.clientWidth = document.documentElement.clientWidth;
      // this.clientHeight = document.documentElement.clientHeight;
      // console.log(window.innerWidth);
      // console.log(document.documentElement.clientWidth);
      this.checkDraggablePosition();
    },
    /**
     * åˆå§‹åŒ–draggable
     */
    initDraggable() {
      this.floatDrag.addEventListener("touchstart", this.toucheStart);
      this.floatDrag.addEventListener("touchmove", (e) => this.touchMove(e));
      this.floatDrag.addEventListener("touchend", this.touchEnd);
    },
    mouseDown(e) {
      const event = e || window.event;
      this.mousedownX = event.screenX;
      this.mousedownY = event.screenY;
      const that = this;
      let floatDragWidth = this.floatDragDom.width / 2;
      let floatDragHeight = this.floatDragDom.height / 2;
      if (event.preventDefault) {
        event.preventDefault();
  methods: {
    toggleExpand() {
      this.isExpanded = !this.isExpanded;
      if (this.isExpanded) {
        this.isHidden = false;
        clearTimeout(this.hideTimer);
        this.updateStats();
      }
      this.canClick = false;
      this.floatDrag.style.transition = "none";
      document.onmousemove = function (e) {
        var event = e || window.event;
        that.left = event.clientX - floatDragWidth;
        that.top = event.clientY - floatDragHeight;
        if (that.left < 0) that.left = 0;
        if (that.top < 0) that.top = 0;
        // é¼ æ ‡ç§»å‡ºå¯è§†åŒºåŸŸåŽç»™æŒ‰é’®è¿˜åŽŸ
        if (
          event.clientY < 0 ||
          event.clientY > Number(this.clientHeight) ||
          event.clientX > Number(this.clientWidth) ||
          event.clientX < 0
        ) {
          this.right = 0;
          this.top =
            this.clientHeight - this.floatDragDom.height - this.distanceBottom;
          document.onmousemove = null;
          this.floatDrag.style.transition = "all 0.3s";
          return;
    },
    handleMouseEnter() {
      this.isHovering = true;
      if (this.autoHide) {
        clearTimeout(this.hideTimer);
        this.isHidden = false;
      }
    },
    handleMouseLeave() {
      this.isHovering = false;
      if (this.autoHide && !this.isExpanded) {
        this.startAutoHide();
      }
    },
    startAutoHide() {
      this.hideTimer = setTimeout(() => {
        if (!this.isExpanded && !this.isHovering) {
          this.isHidden = true;
        }
        if (
          that.left >=
          document.documentElement.clientWidth - floatDragWidth * 2
        ) {
          that.left = document.documentElement.clientWidth - floatDragWidth * 2;
      }, this.hideDelay);
    },
    startDrag(e) {
      e.preventDefault();
      e.stopPropagation();
      this.isDragging = true;
      const clientX = e.type.includes("touch")
        ? e.touches[0].clientX
        : e.clientX;
      const clientY = e.type.includes("touch")
        ? e.touches[0].clientY
        : e.clientY;
      this.dragStart = {
        x: clientX - this.position.x,
        y: clientY - this.position.y,
      };
      const onMove = (moveEvent) => {
        if (!this.isDragging) return;
        const moveX = moveEvent.type.includes("touch")
          ? moveEvent.touches[0].clientX
          : moveEvent.clientX;
        const moveY = moveEvent.type.includes("touch")
          ? moveEvent.touches[0].clientY
          : moveEvent.clientY;
        const newX = moveX - this.dragStart.x;
        const newY = moveY - this.dragStart.y;
        // è¾¹ç•Œæ£€æŸ¥
        const maxX = window.innerWidth - 60;
        const maxY = window.innerHeight - 60;
        this.position.x = Math.max(0, Math.min(newX, maxX));
        this.position.y = Math.max(0, Math.min(newY, maxY));
      };
      const onEnd = () => {
        this.isDragging = false;
        document.removeEventListener("mousemove", onMove);
        document.removeEventListener("mouseup", onEnd);
        document.removeEventListener("touchmove", onMove);
        document.removeEventListener("touchend", onEnd);
        // å¦‚果靠近边缘,自动吸附
        if (this.position.x < 20) {
          this.position.x = 0;
        } else if (this.position.x > window.innerWidth - 80) {
          this.position.x = window.innerWidth - 60;
        }
        if (that.top >= that.clientHeight - floatDragHeight * 2) {
          that.top = that.clientHeight - floatDragHeight * 2;
        // ä¿å­˜ä½ç½®åˆ°æœ¬åœ°å­˜å‚¨
        try {
          localStorage.setItem(
            "floatBallPosition",
            JSON.stringify(this.position)
          );
        } catch (e) {
          console.error("保存位置失败:", e);
        }
      };
      document.addEventListener("mousemove", onMove);
      document.addEventListener("mouseup", onEnd);
      document.addEventListener("touchmove", onMove, { passive: false });
      document.addEventListener("touchend", onEnd);
    },
    mouseUp(e) {
      const event = e || window.event;
      //判断只是单纯的点击,没有拖拽
    handleItemClick(item) {
      if (item.url) {
        console.log(item.url, "item.url");
        // this.$router.push(item.url);
        this.$router.replace({
          path: item.url,
          query: {
            errtype: item.urltype,
          },
        });
        this.toggleExpand();
      }
    },
    handleActionClick(action) {
      console.log(this.roles, "this.roles");
      if (
        this.mousedownY == event.screenY &&
        this.mousedownX == event.screenX
        action.url &&
        (this.roles.includes("admin") || this.roles.includes("sysadmin"))
      ) {
        this.$emit("handlepaly");
      }
      document.onmousemove = null;
      this.checkDraggablePosition();
      this.floatDrag.style.transition = "all 0.3s";
    },
    toucheStart() {
      this.canClick = false;
      this.floatDrag.style.transition = "none";
    },
    touchMove(e) {
      this.canClick = true;
      if (e.targetTouches.length === 1) {
        // å•指拖动
        let touch = event.targetTouches[0];
        this.left = touch.clientX - this.floatDragDom.width / 2;
        this.top = touch.clientY - this.floatDragDom.height / 2;
      }
    },
    touchEnd() {
      if (!this.canClick) return; // è§£å†³ç‚¹å‡»äº‹ä»¶å’Œtouch事件冲突的问题
      this.floatDrag.style.transition = "all 0.3s";
      this.checkDraggablePosition();
    },
    /**
     * åˆ¤æ–­å…ƒç´ æ˜¾ç¤ºä½ç½®
     * åœ¨çª—口改变和move end时调用
     */
    checkDraggablePosition() {
      this.clientWidth = document.documentElement.clientWidth;
      this.clientHeight = document.documentElement.clientHeight;
      if (this.left + this.floatDragDom.width / 2 >= this.clientWidth / 2) {
        // åˆ¤æ–­ä½ç½®æ˜¯å¾€å·¦å¾€å³æ»‘动
        this.left = this.clientWidth - this.floatDragDom.width;
        this.$router.replace(action.url);
        this.toggleExpand();
      } else {
        this.left = 0;
        this.$modal.msgError("非管理员用户暂无创建任务权限");
      }
      if (this.top < 0) {
        // åˆ¤æ–­æ˜¯å¦è¶…出屏幕上沿
        this.top = 0;
    },
    async updateStats() {
      try {
        // è¿™é‡Œå¯ä»¥æ›¿æ¢ä¸ºå®žé™…çš„ API è°ƒç”¨
        // const response = await this.$api.getFollowupStats()
        // this.statsItems = response.data
        // æ¨¡æ‹Ÿæ•°æ®æ›´æ–°
        const mockData = {
          pending: {
            value: "128",
            unread: null,
            trend: { type: "up", arrow: "↑", value: "5" },
          },
          failed: {
            value: "24",
            unread: null,
            trend: { type: "down", arrow: "↓", value: "2" },
          },
          abnormal: {
            value: "8",
            unread: null,
            trend: { type: "up", arrow: "↑", value: "3" },
          },
          myTasks: {
            value: "156",
            unread: null,
            trend: { type: "stable", arrow: "→", value: "0" },
          },
        };
        const response = await getCurrentUserServiceSubtaskCount();
        mockData.pending.value = response.pendingVisitCount;
        mockData.failed.value = response.failedVisitCount;
        mockData.abnormal.value = response.abnormalVisitCount;
        mockData.myTasks.value = response.allVisitCount;
        this.statsItems = this.statsItems.map((item) => {
          const data = mockData[item.id] || {};
          return {
            ...item,
            value: data.value || item.value,
            unread: data.unread || item.unread,
            trend: data.trend || item.trend,
          };
        });
        // æ›´æ–°æ—¶é—´
        const now = new Date();
        this.updateTime = `${now.getHours().toString().padStart(2, "0")}:${now
          .getMinutes()
          .toString()
          .padStart(2, "0")}`;
      } catch (error) {
        console.error("更新统计数据失败:", error);
      }
      if (this.top + this.floatDragDom.height >= this.clientHeight) {
        // åˆ¤æ–­æ˜¯å¦è¶…出屏幕下沿
        this.top = this.clientHeight - this.floatDragDom.height;
    },
    loadPosition() {
      try {
        const savedPosition = localStorage.getItem("floatBallPosition");
        if (savedPosition) {
          const parsed = JSON.parse(savedPosition);
          this.position = parsed;
        }
      } catch (e) {
        console.error("加载位置失败:", e);
      }
    },
    handleClickOutside(e) {
      if (
        this.isExpanded &&
        this.$refs.floatBall &&
        !this.$refs.floatBall.contains(e.target)
      ) {
        this.toggleExpand();
      }
    },
    handleResize() {
      const maxX = window.innerWidth - 60;
      const maxY = window.innerHeight - 60;
      this.position.x = Math.min(this.position.x, maxX);
      this.position.y = Math.min(this.position.y, maxY);
    },
  },
};
</script>
<style>
html,
body {
  overflow: hidden;
}
</style>
<style scoped lang="scss">
.float-position {
<style scoped>
.float-ball {
  position: fixed;
  z-index: 10003 !important;
  left: 0;
  top: 20%;
  width: 70px;
  height: 70px;
  border-radius: 32px;
  z-index: 9999;
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  pointer-events: auto;
}
.float-ball-hidden {
  opacity: 0.3;
  transform: translateX(10px);
}
.float-ball-hidden:hover {
  opacity: 1;
  transform: translateX(0);
}
.ball-main {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--primary-color), #7c3aed);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: move;
  box-shadow: 0 4px 20px rgba(79, 70, 229, 0.3);
  transition: all 0.3s ease;
  position: relative;
  z-index: 10000;
}
.ball-main:hover {
  background: linear-gradient(135deg, var(--hover-color), #6d28d9);
  box-shadow: 0 6px 25px rgba(79, 70, 229, 0.4);
  transform: scale(1.05);
}
.ball-main-expanded {
  background: linear-gradient(135deg, #6366f1, #8b5cf6);
}
.ball-icon {
  width: 24px;
  height: 24px;
  color: white;
}
.fold-icon {
  width: 100%;
  height: 100%;
}
.close-btn {
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  overflow: hidden;
  user-select: none;
  color: white;
  transition: transform 0.2s ease;
}
  display: block;
  background: black;
  background: -webkit-radial-gradient(100px 100px, circle, #5788FE, #292929);
  //   background: -moz-radial-gradient(100px 100px, circle, #35a1a1, #000);Firefox æµè§ˆå™¨çš„实现
  //   background: radial-gradient(100px 100px, circle, #35a1a1, #000);标准 HTML5 å±žæ€§
  margin: 0;
  .drag {
    width: 70px;
    height: 35px;
    // background: #f2e96a;
    text-align: center;
    line-height: 35px;
    border-bottom: 1px solid #fff;
.close-btn:hover {
  transform: rotate(90deg);
}
.close-icon {
  width: 20px;
  height: 20px;
}
.ball-badge {
  position: absolute;
  top: -5px;
  right: -5px;
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  background: #ef4444;
  color: white;
  font-size: 12px;
  font-weight: 600;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid white;
  animation: pulse 2s infinite;
}
@keyframes pulse {
  0%,
  100% {
    transform: scale(1);
  }
  .content {
    width: 70px;
    height: 35px;
    // background: #716af2;
    .label {
      width: 70px;
      height: 35px;
      text-align: center;
      line-height: 35px;
      color: white;
    }
    .label:hover {
      color: rgb(19, 217, 243);
      transition: all 0.5;
    }
    .item-container {
      margin-top: 10px;
      width: 70px;
      height: 500px;
      display: flex;
      justify-content: space-between;
      align-items: center;
      flex-direction: column;
      .power-item {
        width: 40px;
        height: 40px;
        border-radius: 50%;
        background-color: #f1f7ff;
        display: flex;
        justify-content: center;
        align-items: center;
        flex-direction: column;
      }
      .des {
        width: 40px;
        text-align: center;
        margin-bottom: 5px;
        font-size: 10px;
        color: #fff;
      }
    }
  }
  .close {
    width: 20px;
    height: 20px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    background: rgba(0, 0, 0, 0.6);
    position: absolute;
    right: -10px;
    top: -12px;
    cursor: pointer;
  50% {
    transform: scale(1.1);
  }
}
.cart {
  border-radius: 50%;
  width: 5em;
  height: 5em;
.ball-content {
  position: absolute;
  top: 70px;
  left: 0;
  width: 320px;
  background: white;
  border-radius: 16px;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
  overflow: hidden;
  z-index: 9999;
}
.ball-expand-enter-active,
.ball-expand-leave-active {
  transition: all 0.3s ease;
}
.ball-expand-enter,
.ball-expand-leave-to {
  opacity: 0;
  transform: translateY(-10px);
}
.content-header {
  padding: 20px 20px 16px;
  background: linear-gradient(135deg, var(--primary-color), #7c3aed);
  color: white;
}
.content-header h3 {
  margin: 0 0 8px 0;
  font-size: 18px;
  font-weight: 600;
}
.update-time {
  font-size: 12px;
  opacity: 0.9;
}
.stats-grid {
  padding: 16px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}
.stat-item {
  padding: 16px;
  background: #f8fafc;
  border-radius: 12px;
  cursor: pointer;
  transition: all 0.2s ease;
  position: relative;
  border: 2px solid transparent;
}
.stat-item:hover {
  background: #f1f5f9;
  border-color: #e2e8f0;
  transform: translateY(-2px);
}
.stat-item-highlight {
  border-color: var(--primary-color);
  background: linear-gradient(to bottom right, #f0f9ff, #f8fafc);
}
.stat-icon {
  width: 32px;
  height: 32px;
  background: white;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 12px;
  color: var(--primary-color);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.stat-icon svg {
  width: 18px;
  height: 18px;
}
.stat-label {
  font-size: 12px;
  color: #64748b;
  margin-bottom: 4px;
}
.stat-value {
  font-size: 20px;
  font-weight: 700;
  color: #1e293b;
  margin-bottom: 4px;
}
.stat-trend {
  font-size: 11px;
  display: flex;
  align-items: center;
  gap: 2px;
}
.trend-up {
  color: #10b981;
}
.trend-down {
  color: #ef4444;
}
.trend-stable {
  color: #64748b;
}
.trend-arrow {
  font-size: 10px;
}
.stat-badge {
  position: absolute;
  top: 12px;
  right: 12px;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  background: #ef4444;
  color: white;
  font-size: 10px;
  font-weight: 600;
  border-radius: 9px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.header-notice {
  display: inline-block;
  transition: all 0.3s;
  span {
    vertical-align: initial;
  }
  .notice-badge {
    color: inherit;
    .header-notice-icon {
      font-size: 16px;
      padding: 4px;
    }
  }
.quick-actions {
  padding: 12px 20px 20px;
  border-top: 1px solid #f1f5f9;
  display: flex;
  gap: 12px;
  justify-content: center;
}
.drag-ball .drag-content {
  overflow-wrap: break-word;
  font-size: 14px;
  color: #fff;
  letter-spacing: 2px;
.action-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  padding: 12px;
  border-radius: 8px;
  transition: all 0.2s ease;
  flex: 1;
}
.active {
  background-color: #f9f1db !important;
.action-item:hover {
  background: #f8fafc;
}
.active-des {
  color: #71dcfa !important;
  font-size: 20px !important;
  font-weight: 500 !important;
.action-icon {
  width: 24px;
  height: 24px;
  color: var(--primary-color);
}
.action-icon svg {
  width: 20px;
  height: 20px;
}
.action-label {
  font-size: 12px;
  color: #475569;
  font-weight: 500;
}
</style>
src/components/CallButton/index.vue
@@ -11,10 +11,17 @@
    <!-- å‘¼å«æŒ‰é’® -->
    <button
      :class="['call-btn', { calling: isCalling }]"
      :class="[
        'call-btn',
        {
          calling: isCalling,
          registering: isRegistering,
        },
      ]"
      @click="startCall"
      :disabled="isCalling || sipStatus !== '已注册'"
      :disabled="isButtonDisabled"
    >
      <i v-if="isRegistering" class="el-icon-loading"></i>
      {{ callButtonText }}
    </button>
@@ -41,15 +48,18 @@
    const randomNum = Math.floor(Math.random() * 20) + 1000; // å®šä¹‰éšæœºåˆ†æœºå·
    return {
      isCalling: false,
      isRegistering: true, // åˆå§‹ä¸ºæ³¨å†Œä¸­çŠ¶æ€
      randomNum: randomNum,
      randomID: null,
      callStatus: "idle", // idle, calling, connected, ended
      sipStatus: "未连接",
      sipStatusClass: "status-disconnected",
      sipConfig: {
        wsUrl: "wss://192.168.10.124:7443",
        // ç§»é™¤ç¡¬ç¼–码的wsUrl和domain
        wsUrl: "",
        sipUri: "",
        password: "Smartor@2023",
        password: "Smartor@2023",//丽水
        // password: "heskj@1234",//市一
        displayName: "Web å°é¾™",
        // realm: "9.208.5.18:8090",
      },
@@ -74,11 +84,17 @@
      }
      return "";
    },
    callStatusClass() {
      return `status-${this.callStatus}`;
    isButtonDisabled() {
      return (
        this.isCalling || this.sipStatus !== "已注册" || this.isRegistering
      );
    },
    callButtonText() {
      if (this.isRegistering) return "注册中...";
      return this.isCalling ? "通话中..." : "一键呼叫";
    },
    callStatusClass() {
      return `status-${this.callStatus}`;
    },
  },
  created() {
@@ -86,16 +102,29 @@
  },
  async mounted() {
    const orgName = localStorage.getItem("orgname");
    if (orgName == "景宁畲族自治县人民医院") {
      return;
    }
    await this.CallgetList();
    this.isRegistering = true; // å¼€å§‹æ³¨å†Œ
    sipService.init(this.sipConfig);
    // è®¾ç½®çŠ¶æ€å›žè°ƒ
    sipService.onStatusChange = (status) => {
      this.sipStatus = status.text;
      this.sipStatusClass = `status-${status.type}`;
      // æ³¨å†ŒæˆåŠŸæˆ–å¤±è´¥æ—¶å–æ¶ˆåŠ è½½çŠ¶æ€
      if (status.type === "registered" || status.type === "failed") {
        this.isRegistering = false;
      }
      // æ³¨å†ŒæˆåŠŸ
      if (status.type === "registered") {
        this.startCallsetState();
      }
      // å¤„理注册失败和断开连接情况
      if (status.type === "failed" || status.type === "disconnected") {
        this.overCallsetState(); // é‡Šæ”¾åˆ†æœºå·
        this.isRegistering = false;
      }
    };
@@ -107,6 +136,13 @@
      // é€šçŸ¥çˆ¶ç»„件通话状态变化
      this.$emit("call-status-change", status);
    };
    // æ·»åŠ æ³¨å†Œè¶…æ—¶å¤„ç†
    setTimeout(() => {
      if (this.isRegistering && this.sipStatus !== "已注册") {
        this.isRegistering = false;
        this.$message.warning("SIP注册超时,请检查网络连接");
      }
    }, 10000); // 10秒超时
  },
  methods: {
    async startCall() {
@@ -158,9 +194,15 @@
        const res = await CallgetList();
        this.randomNum = res.data[0].tel;
        this.randomID = res.data[0].id;
        // æ­£ç¡®è®¾ç½® sipUri
        this.sipConfig.sipUri = `${this.randomNum}@192.168.10.124`;
        this.startCallsetState();
        // åŠ¨æ€è®¾ç½®sipUri,域名部分会在sipService中动态处理
        const orgName = localStorage.getItem("orgname");
        if (orgName == "丽水市中医院") {
          this.sipConfig.sipUri = `${this.randomNum}@192.168.10.124`;
        } else if (orgName == "龙泉市人民医院") {
          this.sipConfig.sipUri = `${this.randomNum}@10.10.0.220`;
        } else if (orgName == "第一人民医院湖滨院区"||orgName == "第一人民医院吴山院区") {
          this.sipConfig.sipUri = `${this.randomNum}@192.169.129.198`;
        }
      } catch (error) {
        console.error("获取分机号失败:", error);
        // this.updateStatus("failed", "获取分机号失败");
src/components/Regular/index.vue
@@ -18,13 +18,21 @@
              </el-input> </el-form-item
          ></el-col>
          <el-col :span="12"
            ><el-form-item label="异常提醒">
              <el-radio-group v-model="item.isabnormal">
                <el-radio :label="1">是</el-radio>
                <el-radio :label="0">否</el-radio>
              </el-radio-group>
            </el-form-item></el-col
          >
          ><el-form-item label="异常提醒">
            <el-select v-model="item.isabnormal" placeholder="请选择状态">
              <el-option :value="0" label="正常" :style="{ color: '#67C23A' }">
                <span style="color: #67c23a">● æ­£å¸¸</span>
              </el-option>
              <el-option :value="2" label="警告" :style="{ color: '#FFBA00' }">
                <span style="color: #ffba00">● è­¦å‘Š</span>
              </el-option>
              <el-option :value="1" label="异常" :style="{ color: '#f75c5c' }">
                <span style="color: #f75c5c">● å¼‚常</span>
              </el-option>
            </el-select>
          </el-form-item></el-col
        >
          <!-- <el-col :span="12" v-if="intent"
            ><el-form-item label="选项节点">
              <el-input
src/components/SortCheckbox/index.vue
@@ -48,12 +48,23 @@
  name: "OrderedCheckboxGroup",
  props: {
    options: {
      type: Array,
      default: () => [],
        type: Array,
        default: () => [],
        validator: (value) => {
            return Array.isArray(value);
        }
    },
    value: {
      type: Array,
      default: () => [],
        type: Array,
        default: () => [],
        validator: (value) => {
            // å…è®¸ç©ºæ•°ç»„,但如果是非数组值则发出警告
            if (!Array.isArray(value) && value !== null && value !== undefined) {
                console.warn('value prop should be an array, received:', typeof value);
                return false;
            }
            return true;
        }
    },
    initialselectedOrder: {
      type: Array,
@@ -83,16 +94,20 @@
    value: {
      immediate: true,
      handler(newVal) {
        if (
          Array.isArray(newVal) &&
          newVal.length > 0 &&
          typeof newVal[0] === "object"
        ) {
          console.log(this.selectedOrder, "111");
        // é¦–先确保newVal是数组,如果不是则转换为空数组
        if (!Array.isArray(newVal)) {
          console.warn(
            "Expected array for value prop, received:",
            typeof newVal
          );
          this.checkedValues = [];
          this.selectedOrder = [];
          return;
        }
        if (newVal.length > 0 && typeof newVal[0] === "object") {
          // 1. ä¼ å…¥çš„æ˜¯å¯¹è±¡æ•°ç»„ [{ sort, preachform, compensateTime }]
          this.checkedValues = newVal.map((item) => item.preachform); // æå– preachform ç»„成选中值数组
          // æž„建 selectedOrder,优先使用传入的 compensateTime,否则用默认值
          this.checkedValues = newVal.map((item) => item.preachform);
          this.selectedOrder = newVal.map((item) => ({
            value: item.preachform,
            compensateTime: item.hasOwnProperty("compensateTime")
@@ -100,12 +115,11 @@
              : this.defaultCompensateTime,
          }));
        } else {
          // 2. ä¼ å…¥çš„æ˜¯å­—符串数组 (如 ["1", "3", "4"],兼容之前的用法)
          // 2. ä¼ å…¥çš„æ˜¯å­—符串数组
          if (JSON.stringify(newVal) !== JSON.stringify(this.checkedValues)) {
            this.checkedValues = [...newVal];
            console.log(this.selectedOrder, "222");
            console.log(this.newVal, "22");
            // æž„建或更新 selectedOrder,保留已有的 compensateTime
            const newOrder = [];
            newVal.forEach((value) => {
              const existingItem = this.selectedOrder.find(
@@ -114,11 +128,14 @@
              if (existingItem) {
                newOrder.push(existingItem);
              } else {
                // ä¿®å¤hasOwnProperty方法调用
                const existingCompensateTime = this.hasOwnProperty(value);
                newOrder.push({
                  value,
                  compensateTime: this.hasOwnProperty(value)
                    ? this.hasOwnProperty(value)
                    : this.defaultCompensateTime,
                  compensateTime:
                    existingCompensateTime !== false
                      ? existingCompensateTime
                      : this.defaultCompensateTime,
                });
              }
            });
@@ -126,7 +143,7 @@
          }
        }
      },
      deep: true, // å»ºè®®æ·»åŠ  deep: true ä»¥ç¡®ä¿å¯¹è±¡æ•°ç»„内的变化能被捕获
      deep: true,
    },
    checkedValues(newVal, oldVal) {
      console.log(this.selectedOrder, "333");
@@ -186,15 +203,17 @@
      }
    },
    hasOwnProperty(patfrom) {
      console.log(patfrom);
      console.log(this.initialselectedOrder);
      // ä½¿ç”¨find方法查找匹配的对象
      const foundObject = this.initialselectedOrder.find(
        (item) => item.preachform === patfrom
      );
      if (!this.initialselectedOrder || !Array.isArray(this.initialselectedOrder)) {
        return false;
    }
      // å¦‚果找到对象,返回其compensateTime;否则返回false
      return foundObject ? foundObject.compensateTime : false;
    // ä½¿ç”¨find方法查找匹配的对象
    const foundObject = this.initialselectedOrder.find(
        (item) => item.preachform === patfrom
    );
    // å¦‚果找到对象,返回其compensateTime;否则返回false
    return foundObject ? foundObject.compensateTime : false;
    },
    // å‘射变化事件
    emitChangeEvent() {
src/layout/components/Navbar.vue
@@ -1,12 +1,21 @@
<template>
  <div class="navbar">
    <hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
    <hamburger
      id="hamburger-container"
      :is-active="sidebar.opened"
      class="hamburger-container"
      @toggleClick="toggleSideBar"
    />
    <breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!topNav"/>
    <top-nav id="topmenu-container" class="topmenu-container" v-if="topNav"/>
    <breadcrumb
      id="breadcrumb-container"
      class="breadcrumb-container"
      v-if="!topNav"
    />
    <top-nav id="topmenu-container" class="topmenu-container" v-if="topNav" />
    <div class="right-menu">
      <template v-if="device!=='mobile'">
      <template v-if="device !== 'mobile'">
        <search id="header-search" class="right-menu-item" />
        <!-- <el-tooltip content="源码地址" effect="dark" placement="bottom">
@@ -22,12 +31,15 @@
        <!-- <el-tooltip content="布局大小" effect="dark" placement="bottom">
          <size-select id="size-select" class="right-menu-item hover-effect" />
        </el-tooltip> -->
        <span class="username">{{ username }}</span>
      </template>
      <el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">
      <el-dropdown
        class="avatar-container right-menu-item hover-effect"
        trigger="click"
      >
        <div class="avatar-wrapper">
          <img :src="avatar" class="user-avatar">
          <img :src="avatar" class="user-avatar" />
          <i class="el-icon-caret-bottom" />
        </div>
        <el-dropdown-menu slot="dropdown">
@@ -47,15 +59,15 @@
</template>
<script>
import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import TopNav from '@/components/TopNav'
import Hamburger from '@/components/Hamburger'
import Screenfull from '@/components/Screenfull'
import SizeSelect from '@/components/SizeSelect'
import Search from '@/components/HeaderSearch'
import RuoYiGit from '@/components/RuoYi/Git'
import RuoYiDoc from '@/components/RuoYi/Doc'
import { mapGetters } from "vuex";
import Breadcrumb from "@/components/Breadcrumb";
import TopNav from "@/components/TopNav";
import Hamburger from "@/components/Hamburger";
import Screenfull from "@/components/Screenfull";
import SizeSelect from "@/components/SizeSelect";
import Search from "@/components/HeaderSearch";
import RuoYiGit from "@/components/RuoYi/Git";
import RuoYiDoc from "@/components/RuoYi/Doc";
export default {
  components: {
@@ -66,48 +78,51 @@
    SizeSelect,
    Search,
    RuoYiGit,
    RuoYiDoc
    RuoYiDoc,
  },
  computed: {
    ...mapGetters([
      'sidebar',
      'avatar',
      'device'
    ]),
    ...mapGetters(["sidebar", "avatar", "device"]),
    setting: {
      get() {
        return this.$store.state.settings.showSettings
        return this.$store.state.settings.showSettings;
      },
      set(val) {
        this.$store.dispatch('settings/changeSetting', {
          key: 'showSettings',
          value: val
        })
      }
        this.$store.dispatch("settings/changeSetting", {
          key: "showSettings",
          value: val,
        });
      },
    },
    topNav: {
      get() {
        return this.$store.state.settings.topNav
      }
    }
        return this.$store.state.settings.topNav;
      },
    },
    username: {
      get() {
        return this.$store.state.user.name;
      },
    },
  },
  methods: {
    toggleSideBar() {
      this.$store.dispatch('app/toggleSideBar')
      this.$store.dispatch("app/toggleSideBar");
    },
    async logout() {
      this.$confirm('确定注销并退出系统吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.$store.dispatch('LogOut').then(() => {
          location.href = '/index';
      this.$confirm("确定注销并退出系统吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          this.$store.dispatch("LogOut").then(() => {
            location.href = "/index";
          });
        })
      }).catch(() => {});
    }
  }
}
        .catch(() => {});
    },
  },
};
</script>
<style lang="scss" scoped>
@@ -116,21 +131,20 @@
  overflow: hidden;
  position: relative;
  background: #fff;
  box-shadow: 0 1px 4px rgba(0,21,41,.08);
  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
  .hamburger-container {
    line-height: 46px;
    height: 100%;
    float: left;
    cursor: pointer;
    transition: background .3s;
    -webkit-tap-highlight-color:transparent;
    transition: background 0.3s;
    -webkit-tap-highlight-color: transparent;
    &:hover {
      background: rgba(0, 0, 0, .025)
      background: rgba(0, 0, 0, 0.025);
    }
  }
  .breadcrumb-container {
    float: left;
  }
@@ -149,13 +163,17 @@
    float: right;
    height: 100%;
    line-height: 50px;
    display: flex; // æ–°å¢ž
    align-items: center; // æ–°å¢žï¼šåž‚直居中
    justify-content: center; // å¯é€‰ï¼šæ°´å¹³å±…中
    &:focus {
      outline: none;
    }
    .right-menu-item {
      display: inline-block;
      display: inline-flex; // ä¿®æ”¹ä¸ºinline-flex
      align-items: center; // ç¡®ä¿å†…容垂直居中
      padding: 0 8px;
      height: 100%;
      font-size: 18px;
@@ -164,17 +182,25 @@
      &.hover-effect {
        cursor: pointer;
        transition: background .3s;
        transition: background 0.3s;
        &:hover {
          background: rgba(0, 0, 0, .025)
          background: rgba(0, 0, 0, 0.025);
        }
      }
    }
    .username {
      // åˆ é™¤åŽŸæ¥çš„ margin-bottom: 20px;
      display: flex;
      align-items: center;
      color: #1789fa;
      height: 100%;
      padding: 0 12px;
      white-space: nowrap; // é˜²æ­¢æ–‡å­—换行
    }
    .avatar-container {
      margin-right: 30px;
      margin-top: 10px;
      .avatar-wrapper {
        margin-top: 5px;
        position: relative;
src/main.js
@@ -13,6 +13,7 @@
import router from "./router";
import directive from "./directive"; // directive
import plugins from "./plugins"; // plugins
import Print from 'vue-print-nb'
import { download } from "@/utils/request";
//引入quill-editor编辑器
import VueQuillEditor from "vue-quill-editor";
@@ -21,6 +22,8 @@
import "quill/dist/quill.snow.css";
import "quill/dist/quill.bubble.css";
Vue.use(VueQuillEditor);
Vue.use(Print)
// å¼•å…¥
import { codemirror } from "vue-codemirror";
import "@/utils/cm-setting.js";
@@ -89,6 +92,14 @@
  Vue.filter(key, filters[key]);
}
import moment from "moment"
// 1. åœ¨main.js中添加错误监听
window.addEventListener('unhandledrejection', (event) => {
  if (event.reason && event.reason.message &&
      event.reason.message.includes('Loading chunk')) {
    // é‡æ–°åŠ è½½é¡µé¢
    window.location.reload();
  }
});
Vue.prototype.$moment = moment;
src/plugins/auth.js
@@ -1,20 +1,31 @@
import store from '@/store'
import store from "@/store";
function authPermission(permission) {
  const all_permission = "*:*:*";
  const permissions = store.getters && store.getters.permissions
  const permissions = store.getters && store.getters.permissions;
  if (permission && permission.length > 0) {
    return permissions.some(v => {
      return all_permission === v || v === permission
    })
    return permissions.some((v) => {
      return all_permission === v || v === permission;
    });
  } else {
    return false
    return false;
  }
}
function authRole(role) {
  const super_admin = "admin";
  const roles = store.getters && store.getters.roles
  const roles = store.getters && store.getters.roles;
  if (role && role.length > 0) {
    return roles.some((v) => {
      return super_admin === v || v === role;
    });
  } else {
    return false;
  }
}
function authRoles(role) {
  const super_admin = "admin";
  const roles = store.getters && store.getters.roles;
  if (role && role.length > 0) {
    return roles.some(v => {
      return super_admin === v || v === role
@@ -31,15 +42,15 @@
  },
  // éªŒè¯ç”¨æˆ·æ˜¯å¦å«æœ‰æŒ‡å®šæƒé™ï¼Œåªéœ€åŒ…含其中一个
  hasPermiOr(permissions) {
    return permissions.some(item => {
      return authPermission(item)
    })
    return permissions.some((item) => {
      return authPermission(item);
    });
  },
  // éªŒè¯ç”¨æˆ·æ˜¯å¦å«æœ‰æŒ‡å®šæƒé™ï¼Œå¿…须全部拥有
  hasPermiAnd(permissions) {
    return permissions.every(item => {
      return authPermission(item)
    })
    return permissions.every((item) => {
      return authPermission(item);
    });
  },
  // éªŒè¯ç”¨æˆ·æ˜¯å¦å…·å¤‡æŸè§’色
  hasRole(role) {
@@ -47,14 +58,14 @@
  },
  // éªŒè¯ç”¨æˆ·æ˜¯å¦å«æœ‰æŒ‡å®šè§’色,只需包含其中一个
  hasRoleOr(roles) {
    return roles.some(item => {
      return authRole(item)
    })
    return roles.some((item) => {
      return authRoles(item);
    });
  },
  // éªŒè¯ç”¨æˆ·æ˜¯å¦å«æœ‰æŒ‡å®šè§’色,必须全部拥有
  hasRoleAnd(roles) {
    return roles.every(item => {
      return authRole(item)
    })
  }
}
    return roles.every((item) => {
      return authRole(item);
    });
  },
};
src/router/index.js
@@ -1,10 +1,10 @@
import Vue from 'vue'
import Router from 'vue-router'
import Vue from "vue";
import Router from "vue-router";
Vue.use(Router)
Vue.use(Router);
/* Layout */
import Layout from '@/layout'
import Layout from "@/layout";
/**
 * Note: è·¯ç”±é…ç½®é¡¹
@@ -31,182 +31,187 @@
// å…¬å…±è·¯ç”±
export const constantRoutes = [
  {
    path: '/redirect',
    path: "/redirect",
    component: Layout,
    hidden: true,
    children: [
      {
        path: '/redirect/:path(.*)',
        component: () => import('@/views/redirect')
      }
    ]
        path: "/redirect/:path(.*)",
        component: () => import("@/views/redirect"),
      },
    ],
  },
  {
    path: '/login',
    component: () => import('@/views/login'),
    hidden: true
  },
    {
    path: '/loginSSO',
    component: () => import('@/views/loginSSO'),
    hidden: true
    path: "/login",
    component: () => import("@/views/login"),
    hidden: true,
  },
  {
    path: '/sf',
    component: () => import('@/views/outsideChainnew'),
    hidden: true
    path: "/loginSSO",
    component: () => import("@/views/loginSSO"),
    hidden: true,
  },
  {
    path: '/wt',
    component: () => import('@/views/outsideChainwtnew'),
    hidden: true
    path: "/sf",
    component: () => import("@/views/outsideChainnew"),
    hidden: true,
  },
  {
    path: '/xj',
    component: () => import('@/views/outsideChainxjnew'),
    hidden: true
    path: "/wt",
    component: () => import("@/views/outsideChainwtnew"),
    hidden: true,
  },
  {
    path: '/satisfaction',
    component: () => import('@/views/satisfaction'),
    hidden: true
    path: "/xj",
    component: () => import("@/views/outsideChainxjnew"),
    hidden: true,
  },
  {
    path: '/outsideChain',
    component: () => import('@/views/outsideChain'),
    hidden: true
    path: "/satisfaction",
    component: () => import("@/views/satisfaction"),
    hidden: true,
  },
  {
    path: '/outsideChainwt',
    component: () => import('@/views/outsideChainwt'),
    hidden: true
    path: "/outsideChain",
    component: () => import("@/views/outsideChain"),
    hidden: true,
  },
  {
    path: '/outsideChainxj',
    component: () => import('@/views/outsideChainxj'),
    hidden: true
    path: "/outsideChainwt",
    component: () => import("@/views/outsideChainwt"),
    hidden: true,
  },
  {
    path: '/previews',
    component: () => import('@/views/previews'),
    hidden: true
    path: "/outsideChainxj",
    component: () => import("@/views/outsideChainxj"),
    hidden: true,
  },
  {
    path: '/register',
    component: () => import('@/views/register'),
    hidden: true
    path: "/previews",
    component: () => import("@/views/previews"),
    hidden: true,
  },
  {
    path: '/404',
    component: () => import('@/views/error/404'),
    hidden: true
    path: "/register",
    component: () => import("@/views/register"),
    hidden: true,
  },
  {
    path: '/401',
    component: () => import('@/views/error/401'),
    hidden: true
    path: "/404",
    component: () => import("@/views/error/404"),
    hidden: true,
  },
  {
    path: '',
    path: "/401",
    component: () => import("@/views/error/401"),
    hidden: true,
  },
  {
    path: "",
    component: Layout,
    redirect: 'index',
    redirect: "index",
    children: [
      {
        path: 'index',
        component: () => import('@/views/index'),
        name: 'Index',
        meta: { title: '首页', icon: 'dashboard', affix: true }
      }
    ]
        path: "index",
        component: () => import("@/views/index"),
        name: "Index",
        meta: {
          title: "首页",
          icon: "dashboard",
          affix: true,
          roles: ["admin", "sysadmin"],
        },
      },
    ],
  },
  {
    path: '/user',
    path: "/user",
    component: Layout,
    hidden: true,
    redirect: 'noredirect',
    redirect: "noredirect",
    children: [
      {
        path: 'profile',
        component: () => import('@/views/system/user/profile/index'),
        name: 'Profile',
        meta: { title: '个人中心', icon: 'user' }
      }
    ]
  }
]
        path: "profile",
        component: () => import("@/views/system/user/profile/index"),
        name: "Profile",
        meta: { title: "个人中心", icon: "user" },
      },
    ],
  },
];
// åŠ¨æ€è·¯ç”±ï¼ŒåŸºäºŽç”¨æˆ·æƒé™åŠ¨æ€åŽ»åŠ è½½
export const dynamicRoutes = [
  {
    path: '/system/user-auth',
    path: "/system/user-auth",
    component: Layout,
    hidden: true,
    permissions: ['system:user:edit'],
    permissions: ["system:user:edit"],
    children: [
      {
        path: 'role/:userId(\\d+)',
        component: () => import('@/views/system/user/authRole'),
        name: 'AuthRole',
        meta: { title: '分配角色', activeMenu: '/system/user' }
      }
    ]
        path: "role/:userId(\\d+)",
        component: () => import("@/views/system/user/authRole"),
        name: "AuthRole",
        meta: { title: "分配角色", activeMenu: "/system/user" },
      },
    ],
  },
  {
    path: '/system/role-auth',
    path: "/system/role-auth",
    component: Layout,
    hidden: true,
    permissions: ['system:role:edit'],
    permissions: ["system:role:edit"],
    children: [
      {
        path: 'user/:roleId(\\d+)',
        component: () => import('@/views/system/role/authUser'),
        name: 'AuthUser',
        meta: { title: '分配用户', activeMenu: '/system/role' }
      }
    ]
        path: "user/:roleId(\\d+)",
        component: () => import("@/views/system/role/authUser"),
        name: "AuthUser",
        meta: { title: "分配用户", activeMenu: "/system/role" },
      },
    ],
  },
  {
    path: '/system/dict-data',
    path: "/system/dict-data",
    component: Layout,
    hidden: true,
    permissions: ['system:dict:list'],
    permissions: ["system:dict:list"],
    children: [
      {
        path: 'index/:dictId(\\d+)',
        component: () => import('@/views/system/dict/data'),
        name: 'Data',
        meta: { title: '字典数据', activeMenu: '/system/dict' }
      }
    ]
        path: "index/:dictId(\\d+)",
        component: () => import("@/views/system/dict/data"),
        name: "Data",
        meta: { title: "字典数据", activeMenu: "/system/dict" },
      },
    ],
  },
  {
    path: '/monitor/job-log',
    path: "/monitor/job-log",
    component: Layout,
    hidden: true,
    permissions: ['monitor:job:list'],
    permissions: ["monitor:job:list"],
    children: [
      {
        path: 'index/:jobId(\\d+)',
        component: () => import('@/views/monitor/job/log'),
        name: 'JobLog',
        meta: { title: '调度日志', activeMenu: '/monitor/job' }
      }
    ]
        path: "index/:jobId(\\d+)",
        component: () => import("@/views/monitor/job/log"),
        name: "JobLog",
        meta: { title: "调度日志", activeMenu: "/monitor/job" },
      },
    ],
  },
  {
    path: '/tool/gen-edit',
    path: "/tool/gen-edit",
    component: Layout,
    hidden: true,
    permissions: ['tool:gen:edit'],
    permissions: ["tool:gen:edit"],
    children: [
      {
        path: 'index/:tableId(\\d+)',
        component: () => import('@/views/tool/gen/editTable'),
        name: 'GenEdit',
        meta: { title: '修改生成配置', activeMenu: '/tool/gen' }
      }
    ]
  }/*,
        path: "index/:tableId(\\d+)",
        component: () => import("@/views/tool/gen/editTable"),
        name: "GenEdit",
        meta: { title: "修改生成配置", activeMenu: "/tool/gen" },
      },
    ],
  } /*,
  {
    path: '/smartor/archive',
    component: Layout,
@@ -220,17 +225,17 @@
        meta: { title: '患者维护', activeMenu: '/archive/add' }
      }
    ]
  }*/
]
  }*/,
];
// é˜²æ­¢è¿žç»­ç‚¹å‡»å¤šæ¬¡è·¯ç”±æŠ¥é”™
let routerPush = Router.prototype.push;
Router.prototype.push = function push(location) {
  return routerPush.call(this, location).catch(err => err)
}
  return routerPush.call(this, location).catch((err) => err);
};
export default new Router({
  mode: 'history', // åŽ»æŽ‰url中的#
  mode: "history", // åŽ»æŽ‰url中的#
  scrollBehavior: () => ({ y: 0 }),
  routes: constantRoutes
})
  routes: constantRoutes,
});
src/store/getters.js
@@ -15,6 +15,7 @@
  permissions: (state) => state.user.permissions,
  belongWards: (state) => state.user.belongWards,
  belongDepts: (state) => state.user.belongDepts,
  satisfactionCategories: (state) => state.user.satisfactionCategories,
  hisUserId: (state) => state.user.hisUserId,
  permission_routes: (state) => state.permission.routes,
  topbarRouters: (state) => state.permission.topbarRouters,
src/store/modules/permission.js
@@ -1,11 +1,10 @@
import auth from '@/plugins/auth'
import router, { constantRoutes, dynamicRoutes } from '@/router'
import { getRouters } from '@/api/menu'
import Layout from '@/layout/index'
import ParentView from '@/components/ParentView'
import InnerLink from '@/layout/components/InnerLink'
import auth from "@/plugins/auth";
import router, { constantRoutes, dynamicRoutes } from "@/router";
import { getRouters } from "@/api/menu";
import Layout from "@/layout/index";
import ParentView from "@/components/ParentView";
import InnerLink from "@/layout/components/InnerLink";
import store from "@/store";
const permission = {
  state: {
@@ -13,144 +12,165 @@
    addRoutes: [],
    defaultRoutes: [],
    topbarRouters: [],
    sidebarRouters: []
    sidebarRouters: [],
  },
  mutations: {
    SET_ROUTES: (state, routes) => {
      state.addRoutes = routes
      state.routes = constantRoutes.concat(routes)
      state.addRoutes = routes;
      state.routes = constantRoutes.concat(routes);
    },
    SET_DEFAULT_ROUTES: (state, routes) => {
      state.defaultRoutes = constantRoutes.concat(routes)
      state.defaultRoutes = constantRoutes.concat(routes);
    },
    SET_TOPBAR_ROUTES: (state, routes) => {
      state.topbarRouters = routes
      state.topbarRouters = routes;
    },
    SET_SIDEBAR_ROUTERS: (state, routes) => {
      state.sidebarRouters = routes
      state.sidebarRouters = routes;
    },
  },
  actions: {
    // ç”Ÿæˆè·¯ç”±
    GenerateRoutes({ commit }) {
      return new Promise(resolve => {
      return new Promise((resolve) => {
        // å‘后端请求路由数据
        getRouters().then(res => {
          const sdata = JSON.parse(JSON.stringify(res.data))
          const rdata = JSON.parse(JSON.stringify(res.data))
          const sidebarRoutes = filterAsyncRouter(sdata)
          const rewriteRoutes = filterAsyncRouter(rdata, false, true)
          const asyncRoutes = filterDynamicRoutes(dynamicRoutes);
          rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true })
          router.addRoutes(asyncRoutes);
          commit('SET_ROUTES', rewriteRoutes)
          commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes))
          commit('SET_DEFAULT_ROUTES', sidebarRoutes)
          commit('SET_TOPBAR_ROUTES', sidebarRoutes)
          resolve(rewriteRoutes)
          console.log(res.data,'路由数据');
          const result = res.data.find(item => item.name == "Followvisit");
          Processrouting(result)
        getRouters().then((res) => {
          const sdata = JSON.parse(JSON.stringify(res.data));
          const rdata = JSON.parse(JSON.stringify(res.data));
          const sidebarRoutes = filterAsyncRouter(sdata);
          const rewriteRoutes = filterAsyncRouter(rdata, false, true);
          // å…³é”®ä¿®æ”¹ï¼šå¯¹é™æ€è·¯ç”±ä¹Ÿè¿›è¡Œæƒé™è¿‡æ»¤
          // console.log(constantRoutes, "静");
          // console.log(dynamicRoutes, "动");
        })
      })
    }
  }
}
function Processrouting(result){
  const arrf=[];
  const arr=store.getters.Serviceauthority;
  console.log(result,'result');
  console.log(arr,'arr');
  result.children.forEach(objA => {
    arr.forEach(objB => {
          const filteredConstantRoutes = filterDynamicRoutes(constantRoutes);
          const asyncRoutes = filterDynamicRoutes(dynamicRoutes);
          rewriteRoutes.push({ path: "*", redirect: "/404", hidden: true });
          // æ·»åŠ è¿‡æ»¤åŽçš„è·¯ç”±
          router.addRoutes(filteredConstantRoutes);
          router.addRoutes(asyncRoutes);
          // æäº¤åˆ°store时也使用过滤后的路由
          commit("SET_ROUTES", rewriteRoutes);
          commit(
            "SET_SIDEBAR_ROUTERS",
            filteredConstantRoutes.concat(sidebarRoutes)
          );
          commit("SET_DEFAULT_ROUTES", sidebarRoutes);
          commit("SET_TOPBAR_ROUTES", sidebarRoutes);
          resolve(rewriteRoutes);
          console.log(res.data, "路由数据");
          const result = res.data.find((item) => item.name == "Followvisit");
          Processrouting(result);
        });
      });
    },
  },
};
function Processrouting(result) {
  const arrf = [];
  const arr = store.getters.Serviceauthority;
  console.log(result, "result");
  console.log(arr, "arr");
  result.children.forEach((objA) => {
    arr.forEach((objB) => {
      if (objA.meta.title === objB.label) {
        arrf.push(objB);
      }
    });
  });
  console.log(arrf,'arrf');
  store.commit('SET_Serviceauthority', arrf);
  console.log(arrf, "arrf");
  store.commit("SET_Serviceauthority", arrf);
}
// éåŽ†åŽå°ä¼ æ¥çš„è·¯ç”±å­—ç¬¦ä¸²ï¼Œè½¬æ¢ä¸ºç»„ä»¶å¯¹è±¡
function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
  return asyncRouterMap.filter(route => {
  return asyncRouterMap.filter((route) => {
    if (type && route.children) {
      route.children = filterChildren(route.children)
      route.children = filterChildren(route.children);
    }
    if (route.component) {
      // Layout ParentView ç»„件特殊处理
      if (route.component === 'Layout') {
        route.component = Layout
      } else if (route.component === 'ParentView') {
        route.component = ParentView
      } else if (route.component === 'InnerLink') {
        route.component = InnerLink
      if (route.component === "Layout") {
        route.component = Layout;
      } else if (route.component === "ParentView") {
        route.component = ParentView;
      } else if (route.component === "InnerLink") {
        route.component = InnerLink;
      } else {
        route.component = loadView(route.component)
        route.component = loadView(route.component);
      }
    }
    if (route.children != null && route.children && route.children.length) {
      route.children = filterAsyncRouter(route.children, route, type)
      route.children = filterAsyncRouter(route.children, route, type);
    } else {
      delete route['children']
      delete route['redirect']
      delete route["children"];
      delete route["redirect"];
    }
    return true
  })
    return true;
  });
}
function filterChildren(childrenMap, lastRouter = false) {
  var children = []
  var children = [];
  childrenMap.forEach((el, index) => {
    if (el.children && el.children.length) {
      if (el.component === 'ParentView' && !lastRouter) {
        el.children.forEach(c => {
          c.path = el.path + '/' + c.path
      if (el.component === "ParentView" && !lastRouter) {
        el.children.forEach((c) => {
          c.path = el.path + "/" + c.path;
          if (c.children && c.children.length) {
            children = children.concat(filterChildren(c.children, c))
            return
            children = children.concat(filterChildren(c.children, c));
            return;
          }
          children.push(c)
        })
        return
          children.push(c);
        });
        return;
      }
    }
    if (lastRouter) {
      el.path = lastRouter.path + '/' + el.path
      el.path = lastRouter.path + "/" + el.path;
    }
    children = children.concat(el)
  })
  return children
    children = children.concat(el);
  });
  return children;
}
// åŠ¨æ€è·¯ç”±éåŽ†ï¼ŒéªŒè¯æ˜¯å¦å…·å¤‡æƒé™
export function filterDynamicRoutes(routes) {
  const res = []
  routes.forEach(route => {
  const res = [];
  routes.forEach((route) => {
    // é¦–先检查权限字符串 (permissions)
    if (route.permissions) {
      if (auth.hasPermiOr(route.permissions)) {
        res.push(route)
      }
    } else if (route.roles) {
      if (auth.hasRoleOr(route.roles)) {
        res.push(route)
        res.push(route);
      }
    }
  })
  return res
    // ç„¶åŽæ£€æŸ¥è§’色权限 (roles) - è¿™æ˜¯æ‚¨éœ€è¦é‡ç‚¹å…³æ³¨çš„部分
    else if (route.children) {
      if (
        route.children[0]?.meta?.roles &&
        auth.hasRoleOr(route.children[0].meta.roles)
      ) {
        res.push(route);
      }
    }
    // å¯¹äºŽæ²¡æœ‰è®¾ç½®æƒé™çš„路由,默认允许访问
    else {
      res.push(route);
    }
  });
  return res;
}
export const loadView = (view) => {
  if (process.env.NODE_ENV === 'development') {
    return (resolve) => require([`@/views/${view}`], resolve)
  if (process.env.NODE_ENV === "development") {
    return (resolve) => require([`@/views/${view}`], resolve);
  } else {
    // ä½¿ç”¨ import å®žçŽ°ç”Ÿäº§çŽ¯å¢ƒçš„è·¯ç”±æ‡’åŠ è½½
    return () => import(`@/views/${view}`)
    return () => import(`@/views/${view}`);
  }
}
};
export default permission
export default permission;
src/store/modules/user.js
@@ -13,6 +13,7 @@
    belongDepts: [],
    roles: [],
    permissions: [],
    satisfactionCategories:{},
    // æœåŠ¡ç±»åž‹
    Serviceauthority: [
      {
@@ -112,6 +113,9 @@
    SET_hisUserId: (state, hisUserId) => {
      state.hisUserId = hisUserId;
    },
        SET_satisfactionCategories: (state, satisfactionCategories) => {
      state.satisfactionCategories = satisfactionCategories;
    },
    SET_leaveldeptcodes: (state, belongDepts) => {
      state.belongDepts = belongDepts;
    },
@@ -125,10 +129,13 @@
      const username = userInfo.username.trim();
      const password = userInfo.password;
      const code = userInfo.code;
      const campusid = userInfo.campusid;
      const orgid = userInfo.orgid;
      console.log(orgid, campusid, "88");
      return new Promise((resolve, reject) => {
        login(username, password, code, orgid)
        login(username, password, code, orgid, campusid)
          .then((res) => {
            setToken(res.token);
            commit("SET_TOKEN", res.token);
@@ -171,13 +178,13 @@
              localStorage.setItem("YongHuXM", "LQZYY");
            } else if (orgid == "47243006833112611A2101") {
              localStorage.setItem("orgname", "庆元县中医医院");
              localStorage.setItem("ZuHuID", "1429338802177000004");
              localStorage.setItem("ZuHuID", "1429338802177000005");
              localStorage.setItem("deptCode", "");
              localStorage.setItem("YongHuID", "1462585966286868480");
              localStorage.setItem("YongHuXM", "QYZYY");
            } else if (orgid == "47234002X33112111A2101") {
              localStorage.setItem("orgname", "青田县中医医院");
              localStorage.setItem("ZuHuID", "1429338802177000005");
              localStorage.setItem("ZuHuID", "1429338802177000004");
              localStorage.setItem("deptCode", "");
              localStorage.setItem("YongHuID", "1462614919332499458");
              localStorage.setItem("YongHuXM", "QTHCZYY");
@@ -190,6 +197,42 @@
            } else if (orgid == "20001001") {
              localStorage.setItem("orgname", "省立同德翠苑院区");
              localStorage.setItem("ZuHuID", "");
              localStorage.setItem("deptCode", "");
              localStorage.setItem("YongHuID", "");
              localStorage.setItem("YongHuXM", "");
            } else if (orgid == "47252003933112411A2101") {
              localStorage.setItem("orgname", "松阳县中医医院");
              localStorage.setItem("ZuHuID", "1429338802177000008");
              localStorage.setItem("deptCode", "");
              localStorage.setItem("YongHuID", "1497875635748474880");
              localStorage.setItem("YongHuXM", "SYZYY");
            } else if (orgid == "47231077933110211A1101") {
              localStorage.setItem("orgname", "莲都区人民医院");
              localStorage.setItem("ZuHuID", "1429338802177000011");
              localStorage.setItem("deptCode", "01020901");
              localStorage.setItem("YongHuID", "1512710152715767808");
              localStorage.setItem("YongHuXM", "LDRMYY");
            } else if (orgid == "1" && campusid == 1) {
              localStorage.setItem("orgname", "第一人民医院湖滨院区");
              localStorage.setItem("ZuHuID", "");
              localStorage.setItem("deptCode", "");
              localStorage.setItem("YongHuID", "");
              localStorage.setItem("YongHuXM", "");
            } else if (orgid == "1" && campusid == 2) {
              localStorage.setItem("orgname", "第一人民医院吴山院区");
              localStorage.setItem("ZuHuID", "");
              localStorage.setItem("deptCode", "");
              localStorage.setItem("YongHuID", "");
              localStorage.setItem("YongHuXM", "");
            } else if (orgid == "47246116333112211A1001") {
              localStorage.setItem("orgname", "缙云县人民医院");
              localStorage.setItem("ZuHuID", "1429338802177000010");
              localStorage.setItem("deptCode", "");
              localStorage.setItem("YongHuID", "");
              localStorage.setItem("YongHuXM", "");
            } else if (orgid == "47226079133110211G1001") {
              localStorage.setItem("orgname", "丽水市妇幼保健院");
              localStorage.setItem("ZuHuID", "1429338802177000014");
              localStorage.setItem("deptCode", "");
              localStorage.setItem("YongHuID", "");
              localStorage.setItem("YongHuXM", "");
@@ -223,6 +266,15 @@
            commit("SET_nickNAME", user.nickName);
            commit("SET_Id", user.userId);
            commit("SET_hisUserId", user.hisUserId);
            commit("SET_satisfactionCategories", user.satisfactionCategories);
            // if (user.userName == "admin") {
            //   commit("SET_leaveldeptcodes", []);
            //   commit("SET_leavehospitaldistrictcodes", []);
            // } else {
            //   commit("SET_leavehospitaldistrictcodes", user.belongWards);
            //   commit("SET_leaveldeptcodes", user.belongDepts);
            // }
            commit("SET_leavehospitaldistrictcodes", user.belongWards);
            commit("SET_leaveldeptcodes", user.belongDepts);
            commit("SET_AVATAR", avatar);
src/utils/request.js
@@ -110,7 +110,9 @@
          }
        )
          .then(() => {
          this.$router.replace("/login");
            console.log('选择重新登录');
            this.$router.replace("/login");
          })
          .catch(() => {
            isRelogin.show = false;
src/utils/sipService.js
@@ -1,5 +1,30 @@
import JsSIP from "jssip";
import { Notification, MessageBox, Message, Loading } from "element-ui";
// åŒ»é™¢æœºæž„与SIP服务器映射配置
// å…¬å¸æœåС噍192.168.100.6
const HOSPITAL_CONFIG = {
  ä¸½æ°´å¸‚中医院: {
    wsUrl: "wss://192.168.10.124:7443",
    domain: "192.168.10.124",
  },
  é¾™æ³‰å¸‚人民医院: {
    wsUrl: "wss://10.10.0.220:7443",
    domain: "10.10.0.220",
  },
   ç¬¬ä¸€äººæ°‘医院湖滨院区: {
    wsUrl: "wss://192.169.129.198:7443",
    domain: "192.169.129.198",
  },
  ç¬¬ä¸€äººæ°‘医院吴山院区: {
    wsUrl: "wss://192.169.129.198:7443",
    domain: "192.169.129.198",
  },
  // å¯ä»¥ç»§ç»­æ·»åŠ å…¶ä»–åŒ»é™¢é…ç½®
  default: {
    wsUrl: "wss://192.168.10.124:7443",
    domain: "192.168.10.124",
  },
};
class SipService {
  constructor() {
    this.ua = null;
@@ -9,17 +34,40 @@
    this.onIncomingCall = null;
    this.isRegistered = false; // æ–°å¢žæ³¨å†ŒçŠ¶æ€æ ‡å¿—
    this.registrationTime = null; // æ–°å¢žæ³¨å†ŒæˆåŠŸæ—¶é—´æˆ³
    this.currentConfig = null; // å­˜å‚¨å½“前配置
  }
  init(config) {
  // èŽ·å–åŒ»é™¢é…ç½®æ–¹æ³•
  getHospitalConfig() {
    const orgName = localStorage.getItem("orgname");
    return HOSPITAL_CONFIG[orgName] || HOSPITAL_CONFIG.default;
  }
  init(baseConfig) {
    try {
      this.updateStatus("connecting", "连接中;...");
      // èŽ·å–æœºæž„åç§°ï¼Œå¦‚æžœæ²¡æœ‰ä¼ å…¥åˆ™ä»ŽlocalStorage读取
      const orgName = baseConfig.orgName || localStorage.getItem("orgname");
      // æ ¹æ®æœºæž„名称获取对应的服务器配置
      const hospitalConfig = this.getHospitalConfig(orgName);
      console.log(hospitalConfig, "88");
      // åˆå¹¶é…ç½®
      this.currentConfig = {
        ...baseConfig,
        ...hospitalConfig,
      };
      console.log(
        `当前机构: ${orgName}, ä½¿ç”¨æœåС噍: ${this.currentConfig.domain}`
      );
      this.updateStatus("connecting", "连接中...");
      console.log(baseConfig.sipUri, "baseConfig.sipUri");
      this.ua = new JsSIP.UA({
        sockets: [new JsSIP.WebSocketInterface(config.wsUrl)],
        uri: config.sipUri,
        password: config.password,
        display_name: config.displayName,
        sockets: [new JsSIP.WebSocketInterface(this.currentConfig.wsUrl)],
        uri: baseConfig.sipUri, // è¿™é‡Œä½¿ç”¨åŸºç¡€çš„sipUri,domain部分会被动态替换
        password: baseConfig.password,
        display_name: baseConfig.displayName,
        iceServers: [],
        register: true,
        sessionExpires: 1800,
@@ -77,7 +125,9 @@
      const remaining = minDelay - timeSinceRegistration;
      return {
        canCall: false,
        reason: `注册成功,请等待 ${Math.ceil(remaining / 1000)} ç§’后再呼叫`,
        reason: `注册成功,资源加载中请等待 ${Math.ceil(
          remaining / 1000
        )} ç§’后再呼叫`,
      };
    }
@@ -86,6 +136,7 @@
  makeCall(targetNumber) {
    const { canCall, reason } = this.canMakeCall();
    if (!canCall) {
      Message.error(reason);
      return Promise.reject(new Error(reason));
    }
    return new Promise((resolve, reject) => {
@@ -97,7 +148,8 @@
        if (!this.ua.isRegistered()) {
          throw new Error("SIP未注册,无法呼叫");
        }
        const targetUri = `sip:${targetNumber}@${this.currentConfig.domain}`;
        console.log(`呼叫目标: ${targetUri}`);
        const options = {
          sessionTimers: true, // å¯ç”¨ä¼šè¯è®¡æ—¶å™¨
          sessionTimersExpires: 150,
@@ -120,10 +172,7 @@
          },
        };
        this.currentSession = this.ua.call(
          `sip:${targetNumber}@192.168.10.124`,
          options
        );
        this.currentSession = this.ua.call(targetUri, options);
        this.setupPeerConnection(this.currentSession);
        this.setupAudio(this.currentSession);
@@ -150,31 +199,181 @@
    });
  }
  //   normalizeSDP(offer) {
  //     let sdp = offer.sdp;
  //  console.log("原始SDP:", sdp); // è°ƒè¯•用,捕获原始SDP
  //     // æ ‡å‡†åŒ–SDP
  //     sdp = sdp.replace(/c=IN IP4.*\r\n/, "c=IN IP4 0.0.0.0\r\n");
  //     sdp = sdp.replace(
  //       /m=audio \d+.*\r\n/,
  //       "m=audio 9 UDP/TLS/RTP/SAVPF 0 8\r\n"
  //     );
  //     // ç¡®ä¿åŒ…含基本编解码器
  //     if (!sdp.includes("PCMU/8000")) sdp += "a=rtpmap:0 PCMU/8000\r\n";
  //     if (!sdp.includes("PCMA/8000")) sdp += "a=rtpmap:8 PCMA/8000\r\n";
  //     // æ·»åŠ å¿…è¦å±žæ€§
  //     sdp += "a=rtcp-mux\r\n";
  //     sdp += "a=sendrecv\r\n";
  //     console.log("标准化后的SDP:", sdp);
  //     return new RTCSessionDescription({
  //       type: offer.type,
  //       sdp: sdp,
  //     });
  //   }
  // åœ¨ SipService ç±»ä¸­æ–°å¢žæ–¹æ³•,用于获取针对特定服务器的SDP处理策略
  getSDPNormalizationStrategy(orgName) {
    const strategies = {
      é¾™æ³‰å¸‚人民医院: "conservative", // ä¿å®ˆç­–略:最小化修改,优先兼容
      ä¸½æ°´å¸‚中医院: "aggressive", // æ¿€è¿›ç­–略:保持原有强标准化逻辑
      // å¯ä»¥ä¸ºå…¶ä»–机构添加更多策略
    };
    return strategies[orgName] || "moderate"; // é»˜è®¤ç­–ç•¥
  }
  /**
   * æ ‡å‡†åŒ–SDP Offer - ä¿®å¤é¾™æ³‰å¸‚人民医院488错误
   * æ ¸å¿ƒæ€è·¯ï¼šä»Žâ€œå¼ºåˆ¶è¦†ç›–”改为“智能修补”,针对不同服务器使用差异化策略
   */
  normalizeSDP(offer) {
    const orgName = localStorage.getItem("orgname");
    const strategy = this.getSDPNormalizationStrategy(orgName);
    let sdp = offer.sdp;
    // æ ‡å‡†åŒ–SDP
    sdp = sdp.replace(/c=IN IP4.*\r\n/, "c=IN IP4 0.0.0.0\r\n");
    sdp = sdp.replace(
      /m=audio \d+.*\r\n/,
      "m=audio 9 UDP/TLS/RTP/SAVPF 0 8\r\n"
    );
    console.log(`[SDP标准化] æœºæž„: ${orgName}, ç­–ç•¥: ${strategy}`);
    console.log("[SDP标准化] åŽŸå§‹SDP:", sdp);
    // ç¡®ä¿åŒ…含基本编解码器
    if (!sdp.includes("PCMU/8000")) sdp += "a=rtpmap:0 PCMU/8000\r\n";
    if (!sdp.includes("PCMA/8000")) sdp += "a=rtpmap:8 PCMA/8000\r\n";
    if (strategy === "conservative") {
      // ==================== ä¿å®ˆç­–略:针对龙泉市人民医院等严格服务器 ====================
      // åŽŸåˆ™ï¼šé™¤éžå¿…è¦ï¼Œå¦åˆ™ä¸ä¿®æ”¹åŽŸæœ‰SDP结构,仅添加缺失的关键属性
    // æ·»åŠ å¿…è¦å±žæ€§
    sdp += "a=rtcp-mux\r\n";
    sdp += "a=sendrecv\r\n";
      // 1. è°¨æ…Žå¤„理连接地址:仅在地址是明显内网地址时才修改
      const privateIPRegex =
        /c=IN IP4 (192\.168|10\.|172\.(1[6-9]|2[0-9]|3[0-1]))/;
      if (privateIPRegex.test(sdp)) {
        sdp = sdp.replace(/c=IN IP4.*\r\n/, "c=IN IP4 0.0.0.0\r\n");
        console.log("[SDP标准化] å·²ä¿®æ”¹è¿žæŽ¥åœ°å€ä¸º 0.0.0.0");
      }
    console.log("标准化后的SDP:", sdp);
      // 2. ä¿æŒåª’体行原样,不强制修改端口和协议
      // sdp = sdp.replace(/m=audio \d+.*\r\n/, "m=audio 9 UDP/TLS/RTP/SAVPF 0 8\r\n");
      // 3. æ™ºèƒ½æ·»åŠ åŸºç¡€ç¼–è§£ç å™¨æ˜ å°„ï¼ˆä»…åœ¨ç¼ºå¤±æ—¶æ·»åŠ ï¼‰
      const codecMappings = [
        { pt: 0, name: "PCMU/8000" },
        { pt: 8, name: "PCMA/8000" },
      ];
      codecMappings.forEach((codec) => {
        const rtpmapPattern = `a=rtpmap:${codec.pt} ${codec.name}`;
        const payloadPattern = ` ${codec.pt} `;
        // åªæœ‰å½“SDP中包含该负载类型但缺少详细映射时才添加
        if (sdp.includes(payloadPattern) && !sdp.includes(rtpmapPattern)) {
          sdp += `${rtpmapPattern}\r\n`;
          console.log(`[SDP标准化] å·²æ·»åŠ ç¼–è§£ç å™¨æ˜ å°„: ${rtpmapPattern}`);
        }
      });
      // 4. æ¡ä»¶æ€§æ·»åŠ å¿…è¦å±žæ€§ï¼ˆé¿å…é‡å¤ï¼‰
      const essentialAttributes = [
        { attr: "a=rtcp-mux", desc: "RTCP复用" },
        { attr: "a=sendrecv", desc: "双向媒体流" },
      ];
      essentialAttributes.forEach((item) => {
        if (!sdp.includes(item.attr)) {
          sdp += `${item.attr}\r\n`;
          console.log(`[SDP标准化] å·²æ·»åŠ å±žæ€§: ${item.desc}`);
        }
      });
    } else if (strategy === "aggressive") {
      // ==================== æ¿€è¿›ç­–略:保持原有逻辑 ====================
      sdp = sdp.replace(/c=IN IP4.*\r\n/, "c=IN IP4 0.0.0.0\r\n");
      sdp = sdp.replace(
        /m=audio \d+.*\r\n/,
        "m=audio 9 UDP/TLS/RTP/SAVPF 0 8\r\n"
      );
      // ç¡®ä¿åŒ…含基础编解码器
      if (!sdp.includes("PCMU/8000")) sdp += "a=rtpmap:0 PCMU/8000\r\n";
      if (!sdp.includes("PCMA/8000")) sdp += "a=rtpmap:8 PCMA/8000\r\n";
      // æ·»åŠ é€šç”¨å±žæ€§
      sdp += "a=rtcp-mux\r\n";
      sdp += "a=sendrecv\r\n";
    } else {
      // ==================== é»˜è®¤ç­–略:平衡方案 ====================
      // é€‚度修改,兼顾兼容性和功能性
      sdp = sdp.replace(/c=IN IP4.*\r\n/, "c=IN IP4 0.0.0.0\r\n");
      // ä»…在媒体行格式明显异常时修改
      if (!sdp.match(/m=audio \d+ RTP\/AVP/)) {
        sdp = sdp.replace(
          /m=audio \d+.*\r\n/,
          "m=audio 9 UDP/TLS/RTP/SAVPF 0 8\r\n"
        );
      }
      // æ™ºèƒ½æ·»åŠ ç¼ºå¤±çš„å±žæ€§
      if (!sdp.includes("a=rtcp-mux")) sdp += "a=rtcp-mux\r\n";
      if (!sdp.includes("a=sendrecv")) sdp += "a=sendrecv\r\n";
    }
    console.log("[SDP标准化] æ ‡å‡†åŒ–后SDP:", sdp);
    return new RTCSessionDescription({
      type: offer.type,
      sdp: sdp,
    });
  }
  /**
   * å¢žå¼ºçš„SDP调试方法 - ç”¨äºŽå¯¹æ¯”分析
   */
  debugSDPComparison(originalOffer, normalizedOffer, context) {
    console.group(`[SDP调试] ${context}`);
    console.log(
      "原始SDP媒体行:",
      originalOffer.sdp.match(/m=audio.*\r\n/)?.[0] || "未找到"
    );
    console.log(
      "标准化后媒体行:",
      normalizedOffer.sdp.match(/m=audio.*\r\n/)?.[0] || "未找到"
    );
    console.log(
      "原始编解码器列表:",
      originalOffer.sdp.match(/a=rtpmap:\d+.*\r\n/g) || []
    );
    console.log(
      "标准化后编解码器列表:",
      normalizedOffer.sdp.match(/a=rtpmap:\d+.*\r\n/g) || []
    );
    console.groupEnd();
  }
  // åœ¨ setupPeerConnection æ–¹æ³•中集成调试功能
  setupPeerConnection(session) {
    session.on("peerconnection", (pc) => {
      const originalCreateOffer = pc.createOffer.bind(pc);
      pc.createOffer = async (offerOptions) => {
        try {
          const offer = await originalCreateOffer(offerOptions);
          const normalizedOffer = this.normalizeSDP(offer);
          // è°ƒè¯•信息输出
          this.debugSDPComparison(offer, normalizedOffer, "Offer创建阶段");
          return normalizedOffer;
        } catch (error) {
          console.error("创建Offer失败:", error);
          throw error;
        }
      };
    });
  }
  handleCallFailure(e, reject) {
    if (e.response?.status_code === 422) {
      const serverMinSE = e.response.headers["Min-SE"]?.[0]?.raw || "未知";
src/views/Satisfaction/configurationmyd/batch.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,922 @@
<template>
  <div class="batch-process">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <div class="header-content">
        <h2 class="page-title">异常批量处理</h2>
        <p class="page-description">批量处理选中的异常反馈</p>
        <div class="header-actions">
          <el-button
            type="primary"
            icon="el-icon-check"
            @click="handleBatchSubmit"
            :loading="batchProcessing"
          >
            æ‰¹é‡æäº¤å¤„理
          </el-button>
          <el-button type="warning" icon="el-icon-back" @click="handleGoBack">
            è¿”回异常列表
          </el-button>
        </div>
      </div>
    </div>
    <!-- å¼‚常列表 -->
    <div class="list-section">
      <el-card shadow="never">
        <div class="filter-section">
          <el-form
            :model="filterParams"
            :inline="true"
            size="medium"
            class="filter-form"
          >
            <el-form-item label="负责科室">
              <el-select
                v-model="filterParams.deptId"
                placeholder="请选择科室"
                clearable
                style="width: 200px"
              >
                <el-option
                  v-for="dept in deptList"
                  :key="dept.id"
                  :label="dept.name"
                  :value="dept.id"
                />
              </el-select>
            </el-form-item>
            <el-form-item label="处理状态">
              <el-select
                v-model="filterParams.status"
                placeholder="请选择状态"
                clearable
                style="width: 200px"
              >
                <el-option label="待处理" :value="0" />
                <el-option label="处理中" :value="1" />
                <el-option label="已处理" :value="2" />
              </el-select>
            </el-form-item>
            <el-form-item>
              <el-button
                type="primary"
                icon="el-icon-search"
                @click="handleFilter"
              >
                ç­›é€‰
              </el-button>
              <el-button icon="el-icon-refresh" @click="handleResetFilter">
                é‡ç½®
              </el-button>
            </el-form-item>
          </el-form>
        </div>
        <el-table
          v-loading="loading"
          :data="exceptionList"
          :border="true"
          style="width: 100%"
          @selection-change="handleSelectionChange"
          class="exception-table"
        >
          <el-table-column type="selection" width="55" align="center" />
          <el-table-column
            label="序号"
            type="index"
            width="60"
            align="center"
          />
          <el-table-column
            label="负责科室"
            prop="responsibilityDept"
            width="120"
            align="center"
          >
            <template slot-scope="{ row }">
              <el-tag type="primary">{{ row.responsibilityDept }}</el-tag>
            </template>
          </el-table-column>
          <el-table-column
            label="不满意详情"
            prop="unsatisfactoryDetail"
            min-width="200"
            align="center"
          >
            <template slot-scope="{ row }">
              <div class="detail-content">
                {{ row.unsatisfactoryDetail }}
              </div>
            </template>
          </el-table-column>
          <el-table-column label="患者信息" width="300" align="center">
            <template slot-scope="{ row }">
              <div class="patient-info">
                <div class="patient-item">
                  <span class="label">姓名:</span>
                  <span class="value">{{ row.patientName }}</span>
                </div>
                <div class="patient-item">
                  <span class="label">性别:</span>
                  <span class="value">{{
                    row.gender === 1 ? "男" : "女"
                  }}</span>
                </div>
                <div class="patient-item">
                  <span class="label">年龄:</span>
                  <span class="value">{{ row.age }}岁</span>
                </div>
                <div class="patient-item">
                  <span class="label">电话:</span>
                  <span class="value">{{ row.phone }}</span>
                </div>
              </div>
            </template>
          </el-table-column>
          <el-table-column label="出院信息" width="250" align="center">
            <template slot-scope="{ row }">
              <div class="discharge-info">
                <div class="info-item">
                  <span class="label">科室:</span>
                  <span class="value">{{ row.dischargeDept }}</span>
                </div>
                <div class="info-item">
                  <span class="label">病区:</span>
                  <span class="value">{{ row.dischargeWard }}</span>
                </div>
                <div class="info-item">
                  <span class="label">填写时间:</span>
                  <span class="value time">{{ row.fillTime }}</span>
                </div>
              </div>
            </template>
          </el-table-column>
          <el-table-column
            label="处理状态"
            prop="processStatus"
            width="100"
            align="center"
          >
            <template slot-scope="{ row }">
              <el-tag :type="getStatusTagType(row.processStatus)" effect="dark">
                {{ getStatusText(row.processStatus) }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column
            label="操作"
            width="210"
            align="center"
            fixed="right"
          >
            <template slot-scope="{ row }">
              <el-button
                type="primary"
                size="small"
                icon="el-icon-view"
                @click="handleViewDetail(row)"
              >
                æŸ¥çœ‹è¯¦æƒ…
              </el-button>
              <el-button
                type="warning"
                size="small"
                icon="el-icon-edit"
                @click="handleProcess(row)"
                :disabled="row.processStatus === 2"
              >
                å¤„理
              </el-button>
            </template>
          </el-table-column>
        </el-table>
        <!-- åˆ†é¡µ -->
        <div class="pagination-section">
          <el-pagination
            background
            layout="total, sizes, prev, pager, next, jumper"
            :current-page="filterParams.pageNum"
            :page-size="filterParams.pageSize"
            :page-sizes="[10, 20, 30, 50]"
            :total="total"
            @size-change="handleSizeChange"
            @current-change="handlePageChange"
          />
        </div>
      </el-card>
    </div>
    <!-- å¤„理对话框 -->
    <el-dialog
      title="处理异常反馈"
      :visible.sync="processDialogVisible"
      width="600px"
      center
    >
      <el-form
        :model="processForm"
        :rules="processRules"
        ref="processForm"
        label-width="100px"
        size="medium"
      >
        <el-form-item label="处理状态" prop="status">
          <el-select
            v-model="processForm.status"
            placeholder="请选择处理状态"
            style="width: 100%"
          >
            <el-option label="处理中" :value="1" />
            <el-option label="已处理" :value="2" />
            <el-option label="已驳回" :value="3" />
          </el-select>
        </el-form-item>
        <el-form-item label="报备科室" prop="reportDepts">
          <el-select
            v-model="processForm.reportDepts"
            placeholder="请选择报备科室"
            multiple
            filterable
            collapse-tags
            style="width: 100%"
          >
            <el-option
              v-for="dept in deptList"
              :key="dept.id"
              :label="dept.name"
              :value="dept.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="处理备注" prop="remark">
          <el-input
            v-model="processForm.remark"
            type="textarea"
            :rows="4"
            placeholder="请输入处理备注(最多500字)"
            maxlength="500"
            show-word-limit
          />
        </el-form-item>
        <el-form-item label="附件上传">
          <el-upload
            class="upload-demo"
            action="#"
            :on-preview="handlePreview"
            :on-remove="handleRemove"
            :before-remove="beforeRemove"
            :limit="3"
            :on-exceed="handleExceed"
            :file-list="fileList"
          >
            <el-button size="small" type="primary">点击上传</el-button>
            <div slot="tip" class="el-upload__tip">
              æ”¯æŒä¸Šä¼ å›¾ç‰‡ã€æ–‡æ¡£ç­‰é™„件,单个文件不超过10MB
            </div>
          </el-upload>
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button @click="processDialogVisible = false">取消</el-button>
        <el-button type="primary" @click="submitProcess" :loading="processing">
          æäº¤å¤„理
        </el-button>
      </span>
    </el-dialog>
    <!-- æ‰¹é‡å¤„理对话框 -->
    <el-dialog
      title="批量处理异常反馈"
      :visible.sync="batchDialogVisible"
      width="600px"
      center
    >
      <el-form
        :model="batchProcessForm"
        :rules="processRules"
        ref="batchProcessForm"
        label-width="100px"
        size="medium"
      >
        <el-form-item label="处理状态" prop="status">
          <el-select
            v-model="batchProcessForm.status"
            placeholder="请选择处理状态"
            style="width: 100%"
          >
            <el-option label="处理中" :value="1" />
            <el-option label="已处理" :value="2" />
            <el-option label="已驳回" :value="3" />
          </el-select>
        </el-form-item>
        <el-form-item label="报备科室" prop="reportDepts">
          <el-select
            v-model="batchProcessForm.reportDepts"
            placeholder="请选择报备科室"
            multiple
            filterable
            collapse-tags
            style="width: 100%"
          >
            <el-option
              v-for="dept in deptList"
              :key="dept.id"
              :label="dept.name"
              :value="dept.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="处理备注" prop="remark">
          <el-input
            v-model="batchProcessForm.remark"
            type="textarea"
            :rows="4"
            placeholder="请输入处理备注(最多500字)"
            maxlength="500"
            show-word-limit
          />
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button @click="batchDialogVisible = false">取消</el-button>
        <el-button
          type="primary"
          @click="submitBatchProcess"
          :loading="batchProcessing"
        >
          æ‰¹é‡æäº¤
        </el-button>
      </span>
    </el-dialog>
    <!-- å¼‚常详情弹框 -->
    <Details-anomaly
      :visible="detailDialogVisible"
      :record-id="selectedRecordId"
      :title="detailDialogTitle"
      @update:visible="handleDetailDialogClose"
      @processed="handleProcessed"
      @close="handleDetailDialogClose"
    />
  </div>
</template>
<script>
import DetailsAnomaly from "./components/DetailsAnomaly.vue";
export default {
  name: "BatchProcess",
  components: {
    DetailsAnomaly,
  },
  data() {
    return {
      // æ·»åŠ ä»¥ä¸‹æ•°æ®
      detailDialogVisible: false,
      selectedRecordId: null,
      detailDialogTitle: "异常反馈详情",
      // å½“前处理的异常ID
      currentExceptionId: null,
      // æ‰¹é‡é€‰ä¸­çš„异常ID
      selectedExceptionIds: [],
      // è¿‡æ»¤å‚æ•°
      filterParams: {
        deptId: "",
        status: "",
        pageNum: 1,
        pageSize: 10,
      },
      // åŠ è½½çŠ¶æ€
      loading: false,
      processing: false,
      batchProcessing: false,
      // ç§‘室列表
      deptList: [
        { id: 1, name: "心血管内科" },
        { id: 2, name: "神经内科" },
        { id: 3, name: "普外科" },
        { id: 4, name: "骨科" },
        { id: 5, name: "妇产科" },
        { id: 6, name: "儿科" },
        { id: 7, name: "急诊科" },
        { id: 8, name: "呼吸内科" },
        { id: 9, name: "消化内科" },
        { id: 10, name: "内分泌科" },
      ],
      // å¼‚常列表数据
      exceptionList: [],
      total: 0,
      // å¤„理对话框
      processDialogVisible: false,
      processForm: {
        status: "",
        reportDepts: [],
        remark: "",
      },
      processRules: {
        status: [
          { required: true, message: "请选择处理状态", trigger: "change" },
        ],
        remark: [
          { required: true, message: "请输入处理备注", trigger: "blur" },
          {
            min: 5,
            max: 500,
            message: "备注长度在 5 åˆ° 500 ä¸ªå­—符",
            trigger: "blur",
          },
        ],
      },
      fileList: [],
      // æ‰¹é‡å¤„理对话框
      batchDialogVisible: false,
      batchProcessForm: {
        status: "",
        reportDepts: [],
        remark: "",
      },
    };
  },
  mounted() {
    this.loadExceptionList();
  },
  methods: {
    // åŠ è½½å¼‚å¸¸åˆ—è¡¨
    async loadExceptionList() {
      this.loading = true;
      try {
        // Mock æ•°æ®
        await new Promise((resolve) => {
          setTimeout(() => {
            this.exceptionList = [
              {
                id: 1,
                responsibilityDept: "心血管内科",
                unsatisfactoryDetail:
                  "医生查房时间太短,沟通不够充分,对病情解释不够详细",
                patientName: "张先生",
                gender: 1,
                age: 45,
                phone: "138****1234",
                dischargeDept: "心血管内科",
                dischargeWard: "内科一病区",
                fillTime: "2024-01-15 10:30:25",
                processStatus: 0,
                questionnaireId: 1001,
              },
              {
                id: 2,
                responsibilityDept: "神经内科",
                unsatisfactoryDetail:
                  "护士打针技术不佳,扎了三次才成功,且态度不够耐心",
                patientName: "李女士",
                gender: 0,
                age: 38,
                phone: "139****5678",
                dischargeDept: "神经内科",
                dischargeWard: "内科二病区",
                fillTime: "2024-01-14 16:20:10",
                processStatus: 0,
                questionnaireId: 1002,
              },
              {
                id: 3,
                responsibilityDept: "普外科",
                unsatisfactoryDetail: "术后换药不及时,伤口疼痛时没有及时处理",
                patientName: "王先生",
                gender: 1,
                age: 52,
                phone: "137****9012",
                dischargeDept: "普外科",
                dischargeWard: "外科一病区",
                fillTime: "2024-01-13 09:15:45",
                processStatus: 1,
                questionnaireId: 1003,
              },
              {
                id: 4,
                responsibilityDept: "骨科",
                unsatisfactoryDetail: "康复指导不够专业,对恢复过程描述不清楚",
                patientName: "刘女士",
                gender: 0,
                age: 65,
                phone: "136****3456",
                dischargeDept: "骨科",
                dischargeWard: "外科二病区",
                fillTime: "2024-01-12 14:40:30",
                processStatus: 0,
                questionnaireId: 1004,
              },
              {
                id: 5,
                responsibilityDept: "妇产科",
                unsatisfactoryDetail:
                  "产前检查排队时间过长,等待期间没有休息座位",
                patientName: "陈女士",
                gender: 0,
                age: 28,
                phone: "135****7890",
                dischargeDept: "妇产科",
                dischargeWard: "妇产科病区",
                fillTime: "2024-01-11 11:25:15",
                processStatus: 2,
                questionnaireId: 1005,
              },
              {
                id: 6,
                responsibilityDept: "儿科",
                unsatisfactoryDetail:
                  "儿童用药剂量交代不清晰,用药注意事项没有说明",
                patientName: "赵宝宝",
                gender: 1,
                age: 5,
                phone: "134****1234",
                dischargeDept: "儿科",
                dischargeWard: "儿科病区",
                fillTime: "2024-01-10 15:50:20",
                processStatus: 0,
                questionnaireId: 1006,
              },
              {
                id: 7,
                responsibilityDept: "急诊科",
                unsatisfactoryDetail: "急诊等待时间过长,病情没有得到及时评估",
                patientName: "孙先生",
                gender: 1,
                age: 40,
                phone: "133****5678",
                dischargeDept: "急诊科",
                dischargeWard: "急诊病区",
                fillTime: "2024-01-09 10:15:40",
                processStatus: 0,
                questionnaireId: 1007,
              },
              {
                id: 8,
                responsibilityDept: "呼吸内科",
                unsatisfactoryDetail: "医生开药较多,费用较高,没有说明必要性",
                patientName: "周女士",
                gender: 0,
                age: 55,
                phone: "132****9012",
                dischargeDept: "呼吸内科",
                dischargeWard: "内科一病区",
                fillTime: "2024-01-08 13:30:55",
                processStatus: 1,
                questionnaireId: 1008,
              },
            ];
            this.total = this.exceptionList.length;
            resolve();
          }, 500);
        });
      } finally {
        this.loading = false;
      }
    },
    // èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
    getStatusTagType(status) {
      switch (status) {
        case 0:
          return "warning"; // å¾…处理
        case 1:
          return "primary"; // å¤„理中
        case 2:
          return "success"; // å·²å¤„理
        default:
          return "info";
      }
    },
    // èŽ·å–çŠ¶æ€æ–‡æœ¬
    getStatusText(status) {
      switch (status) {
        case 0:
          return "待处理";
        case 1:
          return "处理中";
        case 2:
          return "已处理";
        default:
          return "未知";
      }
    },
    // å¤„理筛选
    handleFilter() {
      this.filterParams.pageNum = 1;
      this.loadExceptionList();
    },
    // é‡ç½®ç­›é€‰
    handleResetFilter() {
      this.filterParams = {
        deptId: "",
        status: "",
        pageNum: 1,
        pageSize: 10,
      };
      this.loadExceptionList();
    },
    // å¤„理选择变化
    handleSelectionChange(selection) {
      this.selectedExceptionIds = selection.map((item) => item.id);
    },
    // å¤„理批量提交
    handleBatchSubmit() {
      if (this.selectedExceptionIds.length === 0) {
        this.$message.warning("请先选择要处理的异常反馈");
        return;
      }
      this.batchDialogVisible = true;
    },
    // è¿”回异常列表
    handleGoBack() {
      this.$router.push("/satisfaction/exception/list");
    },
    // æŸ¥çœ‹è¯¦æƒ…
    handleViewDetail(row) {
      this.selectedRecordId = row.id;
      this.detailDialogTitle = `${row.patientName} - å¼‚常反馈详情`;
      this.detailDialogVisible = true;
    },
    // å¤„理详情弹框关闭
    handleDetailDialogClose() {
      this.detailDialogVisible = false;
      this.selectedRecordId = null;
    }, // å¤„理完成后的回调
    handleProcessed() {
      // é‡æ–°åŠ è½½æ•°æ®
      this.loadExceptionList();
    },
    // å¤„理单个异常
    handleProcess(row) {
      this.currentExceptionId = row.id;
      this.processForm = {
        status: row.processStatus === 0 ? 1 : row.processStatus,
        reportDepts: [],
        remark: "",
      };
      this.processDialogVisible = true;
    },
    // æäº¤å¤„理
    async submitProcess() {
      this.$refs.processForm.validate(async (valid) => {
        if (valid) {
          this.processing = true;
          try {
            // Mock API调用
            await new Promise((resolve) => setTimeout(resolve, 1000));
            this.$message.success("处理提交成功");
            this.processDialogVisible = false;
            this.loadExceptionList();
          } finally {
            this.processing = false;
          }
        }
      });
    },
    // æäº¤æ‰¹é‡å¤„理
    async submitBatchProcess() {
      this.$refs.batchProcessForm.validate(async (valid) => {
        if (valid) {
          this.batchProcessing = true;
          try {
            // Mock API调用
            await new Promise((resolve) => setTimeout(resolve, 1500));
            this.$message.success(
              `已批量处理 ${this.selectedExceptionIds.length} æ¡å¼‚常反馈`
            );
            this.batchDialogVisible = false;
            this.selectedExceptionIds = [];
            this.loadExceptionList();
          } finally {
            this.batchProcessing = false;
          }
        }
      });
    },
    // åˆ†é¡µå¤§å°å˜åŒ–
    handleSizeChange(size) {
      this.filterParams.pageSize = size;
      this.filterParams.pageNum = 1;
      this.loadExceptionList();
    },
    // é¡µç å˜åŒ–
    handlePageChange(page) {
      this.filterParams.pageNum = page;
      this.loadExceptionList();
    },
    // æ–‡ä»¶ä¸Šä¼ ç›¸å…³æ–¹æ³•
    handlePreview(file) {
      console.log("预览文件:", file);
    },
    handleRemove(file, fileList) {
      console.log("移除文件:", file, fileList);
    },
    beforeRemove(file) {
      return this.$confirm(`确定移除 ${file.name}?`);
    },
    handleExceed(files, fileList) {
      this.$message.warning(
        `当前限制选择 3 ä¸ªæ–‡ä»¶ï¼Œæœ¬æ¬¡é€‰æ‹©äº† ${files.length} ä¸ªæ–‡ä»¶ï¼Œå…±é€‰æ‹©äº† ${
          files.length + fileList.length
        } ä¸ªæ–‡ä»¶`
      );
    },
  },
};
</script>
<style lang="scss" scoped>
.batch-process {
  padding: 20px;
  background-color: #f5f7fa;
  min-height: 100vh;
  .page-header {
    margin-bottom: 20px;
    padding: 20px;
    background: linear-gradient(135deg, #5788fe 0%, #66b1ff 100%);
    border-radius: 8px;
    color: white;
    .header-content {
      .page-title {
        margin: 0 0 8px 0;
        font-size: 20px;
        font-weight: 600;
      }
      .page-description {
        margin: 0 0 20px 0;
        opacity: 0.9;
        font-size: 14px;
      }
      .header-actions {
        display: flex;
        gap: 10px;
      }
    }
  }
  .list-section {
    .filter-section {
      margin-bottom: 20px;
      .filter-form {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        ::v-deep .el-form-item {
          margin-bottom: 0;
          margin-right: 20px;
          &:last-child {
            margin-right: 0;
          }
        }
      }
    }
    .exception-table {
      ::v-deep .el-table__header-wrapper {
        th {
          background-color: #f8f9fa;
          font-weight: 600;
          color: #333;
        }
      }
      .detail-content {
        font-size: 13px;
        color: #606266;
        line-height: 1.5;
        text-align: left;
      }
      .patient-info {
        .patient-item {
          display: flex;
          justify-content: space-between;
          align-items: center;
          margin-bottom: 5px;
          padding: 2px 0;
          .label {
            font-size: 12px;
            color: #606266;
            min-width: 40px;
          }
          .value {
            font-size: 13px;
            color: #333;
            font-weight: 500;
            text-align: right;
            flex: 1;
          }
        }
      }
      .discharge-info {
        .info-item {
          display: flex;
          justify-content: space-between;
          align-items: center;
          margin-bottom: 5px;
          padding: 2px 0;
          .label {
            font-size: 12px;
            color: #606266;
            min-width: 50px;
          }
          .value {
            font-size: 13px;
            color: #333;
            font-weight: 500;
            text-align: right;
            flex: 1;
            &.time {
              font-size: 12px;
              color: #909399;
            }
          }
        }
      }
    }
    .pagination-section {
      display: flex;
      justify-content: center;
      padding: 20px 0 0 0;
    }
  }
}
@media (max-width: 768px) {
  .batch-process {
    padding: 10px;
    .page-header {
      .header-actions {
        flex-direction: column;
        align-items: stretch;
      }
    }
    .list-section {
      .filter-section {
        .filter-form {
          ::v-deep .el-form-item {
            width: 100%;
            margin-right: 0;
            margin-bottom: 10px;
          }
        }
      }
    }
  }
}
</style>
src/views/Satisfaction/configurationmyd/components/DetailsAnomaly.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,923 @@
<template>
  <el-dialog
    :title="title"
    :visible.sync="dialogVisible"
    width="900px"
    top="5vh"
    class="exception-detail-dialog"
    @close="handleClose"
  >
    <!-- åŸºæœ¬ä¿¡æ¯ -->
    <div class="info-section">
      <div class="section-title">基本信息</div>
      <el-row :gutter="20">
        <el-col :span="8">
          <div class="info-item">
            <span class="label">患者姓名:</span>
            <span class="value">{{ currentRecord.patientName }}</span>
          </div>
        </el-col>
        <el-col :span="8">
          <div class="info-item">
            <span class="label">性别:</span>
            <span class="value">{{ currentRecord.gender === 1 ? '男' : '女' }}</span>
          </div>
        </el-col>
        <el-col :span="8">
          <div class="info-item">
            <span class="label">年龄:</span>
            <span class="value">{{ currentRecord.age }}岁</span>
          </div>
        </el-col>
        <el-col :span="8">
          <div class="info-item">
            <span class="label">联系方式:</span>
            <span class="value">{{ currentRecord.phone }}</span>
          </div>
        </el-col>
        <el-col :span="8">
          <div class="info-item">
            <span class="label">出院科室:</span>
            <span class="value">{{ currentRecord.dischargeDept }}</span>
          </div>
        </el-col>
        <el-col :span="8">
          <div class="info-item">
            <span class="label">出院病区:</span>
            <span class="value">{{ currentRecord.dischargeWard }}</span>
          </div>
        </el-col>
        <el-col :span="8">
          <div class="info-item">
            <span class="label">填写时间:</span>
            <span class="value">{{ currentRecord.fillTime }}</span>
          </div>
        </el-col>
        <el-col :span="8">
          <div class="info-item">
            <span class="label">负责科室:</span>
            <el-tag type="primary">{{ currentRecord.responsibilityDept }}</el-tag>
          </div>
        </el-col>
        <el-col :span="8">
          <div class="info-item">
            <span class="label">处理状态:</span>
            <el-tag
              :type="getStatusTagType(currentRecord.processStatus)"
              effect="dark"
            >
              {{ getStatusText(currentRecord.processStatus) }}
            </el-tag>
          </div>
        </el-col>
      </el-row>
    </div>
    <!-- é—®å·è¯¦æƒ… -->
    <div class="questionnaire-section">
      <div class="section-title">问卷填写详情</div>
      <div class="questionnaire-content">
        <div class="question-item" v-for="(question, index) in questionnaireData" :key="index">
          <div class="question-header">
            <span class="question-index">{{ index + 1 }}.</span>
            <span class="question-text">{{ question.question }}</span>
            <el-tag
              size="mini"
              :type="question.type === 1 ? 'primary' : 'success'"
              class="question-type"
            >
              {{ question.type === 1 ? '单选题' : '多选题' }}
            </el-tag>
          </div>
          <div class="question-options">
            <el-radio-group
              v-model="question.answer"
              v-if="question.type === 1"
              disabled
            >
              <el-radio
                v-for="option in question.options"
                :key="option.value"
                :label="option.value"
                :class="{ 'unsatisfactory-option': isUnsatisfactoryOption(option.value) }"
              >
                {{ option.text }}
              </el-radio>
            </el-radio-group>
            <el-checkbox-group
              v-model="question.answer"
              v-else
              disabled
            >
              <el-checkbox
                v-for="option in question.options"
                :key="option.value"
                :label="option.value"
                :class="{ 'unsatisfactory-option': isUnsatisfactoryOption(option.value) }"
              >
                {{ option.text }}
              </el-checkbox>
            </el-checkbox-group>
          </div>
          <div v-if="question.additional" class="additional-remark">
            <div class="remark-label">补充说明:</div>
            <div class="remark-content">{{ question.additional }}</div>
          </div>
        </div>
      </div>
    </div>
    <!-- å¤„理记录 -->
    <div class="process-section">
      <div class="section-title">处理记录</div>
      <div class="process-timeline" v-if="processRecords.length > 0">
        <el-timeline>
          <el-timeline-item
            v-for="(record, index) in processRecords"
            :key="index"
            :timestamp="record.time"
            placement="top"
          >
            <el-card>
              <div class="process-item">
                <div class="process-header">
                  <span class="process-user">{{ record.user }}</span>
                  <el-tag
                    size="small"
                    :type="getStatusTagType(record.status)"
                  >
                    {{ getStatusText(record.status) }}
                  </el-tag>
                </div>
                <div class="process-content">
                  <div v-if="record.reportDepts && record.reportDepts.length > 0" class="process-depts">
                    <span class="label">报备科室:</span>
                    <el-tag
                      v-for="dept in record.reportDepts"
                      :key="dept"
                      size="small"
                      type="info"
                      class="dept-tag"
                    >
                      {{ dept }}
                    </el-tag>
                  </div>
                  <div v-if="record.remark" class="process-remark">
                    <span class="label">处理备注:</span>
                    <span class="content">{{ record.remark }}</span>
                  </div>
                  <div v-if="record.attachments && record.attachments.length > 0" class="process-attachments">
                    <span class="label">附件:</span>
                    <el-button
                      v-for="file in record.attachments"
                      :key="file.id"
                      type="text"
                      size="small"
                      icon="el-icon-document"
                      @click="handlePreviewFile(file)"
                    >
                      {{ file.name }}
                    </el-button>
                  </div>
                </div>
              </div>
            </el-card>
          </el-timeline-item>
        </el-timeline>
      </div>
      <div v-else class="no-record">
        æš‚无处理记录
      </div>
    </div>
    <span slot="footer" class="dialog-footer">
      <el-button
        type="primary"
        icon="el-icon-edit"
        @click="handleProcess"
        v-if="currentRecord.processStatus !== 2"
      >
        å¤„理异常
      </el-button>
      <el-button @click="dialogVisible = false">关闭</el-button>
    </span>
    <!-- å¤„理对话框 -->
    <el-dialog
      title="处理异常反馈"
      :visible.sync="processDialogVisible"
      width="600px"
      center
      append-to-body
    >
      <el-form
        :model="processForm"
        :rules="processRules"
        ref="processForm"
        label-width="100px"
        size="medium"
      >
        <el-form-item label="处理状态" prop="status">
          <el-select
            v-model="processForm.status"
            placeholder="请选择处理状态"
            style="width: 100%"
          >
            <el-option label="处理中" :value="1" />
            <el-option label="已处理" :value="2" />
            <el-option label="已驳回" :value="3" />
          </el-select>
        </el-form-item>
        <el-form-item label="报备科室" prop="reportDepts">
          <el-select
            v-model="processForm.reportDepts"
            placeholder="请选择报备科室"
            multiple
            filterable
            collapse-tags
            style="width: 100%"
          >
            <el-option
              v-for="dept in deptList"
              :key="dept.id"
              :label="dept.name"
              :value="dept.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="处理备注" prop="remark">
          <el-input
            v-model="processForm.remark"
            type="textarea"
            :rows="4"
            placeholder="请输入处理备注(最多500字)"
            maxlength="500"
            show-word-limit
          />
        </el-form-item>
        <el-form-item label="附件上传">
          <el-upload
            class="upload-demo"
            action="#"
            :on-preview="handleFilePreview"
            :on-remove="handleFileRemove"
            :before-remove="beforeFileRemove"
            :limit="3"
            :on-exceed="handleFileExceed"
            :file-list="fileList"
          >
            <el-button size="small" type="primary">点击上传</el-button>
            <div slot="tip" class="el-upload__tip">支持上传图片、文档等附件,单个文件不超过10MB</div>
          </el-upload>
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button @click="processDialogVisible = false">取消</el-button>
        <el-button
          type="primary"
          @click="submitProcess"
          :loading="processing"
        >
          æäº¤å¤„理
        </el-button>
      </span>
    </el-dialog>
  </el-dialog>
</template>
<script>
export default {
  name: 'ExceptionDetailDialog',
  props: {
    // æ˜¯å¦æ˜¾ç¤ºå¯¹è¯æ¡†
    visible: {
      type: Boolean,
      default: false
    },
    // è®°å½•ID
    recordId: {
      type: [Number, String],
      default: null
    },
    // å¯¹è¯æ¡†æ ‡é¢˜
    title: {
      type: String,
      default: '异常反馈详情'
    }
  },
  data() {
    return {
      // å½“前记录
      currentRecord: {},
      // é—®å·æ•°æ®
      questionnaireData: [],
      // å¤„理记录
      processRecords: [],
      // ç§‘室列表
      deptList: [
        { id: 1, name: '心血管内科' },
        { id: 2, name: '神经内科' },
        { id: 3, name: '普外科' },
        { id: 4, name: '骨科' },
        { id: 5, name: '妇产科' },
        { id: 6, name: '儿科' },
        { id: 7, name: '急诊科' },
        { id: 8, name: '呼吸内科' }
      ],
      // å¤„理对话框
      processDialogVisible: false,
      processing: false,
      processForm: {
        status: '',
        reportDepts: [],
        remark: ''
      },
      processRules: {
        status: [
          { required: true, message: '请选择处理状态', trigger: 'change' }
        ],
        remark: [
          { required: true, message: '请输入处理备注', trigger: 'blur' },
          { min: 5, max: 500, message: '备注长度在 5 åˆ° 500 ä¸ªå­—符', trigger: 'blur' }
        ]
      },
      fileList: [],
      // åŠ è½½çŠ¶æ€
      loading: false
    };
  },
  computed: {
    dialogVisible: {
      get() {
        return this.visible;
      },
      set(val) {
        this.$emit('update:visible', val);
      }
    }
  },
  watch: {
    visible: {
      immediate: true,
      handler(val) {
        if (val && this.recordId) {
          this.loadData();
        }
      }
    }
  },
  methods: {
    // åŠ è½½æ•°æ®
    async loadData() {
      this.loading = true;
      try {
        await Promise.all([
          this.loadRecordDetail(),
          this.loadQuestionnaireData(),
          this.loadProcessRecords()
        ]);
      } finally {
        this.loading = false;
      }
    },
    // åŠ è½½è®°å½•è¯¦æƒ…
    async loadRecordDetail() {
      return new Promise(resolve => {
        setTimeout(() => {
          // æ ¹æ®ä¸åŒçš„recordId返回不同的mock数据
          const mockRecords = {
            1: {
              id: 1,
              patientName: '张先生',
              gender: 1,
              age: 45,
              phone: '13800138000',
              dischargeDept: '心血管内科',
              dischargeWard: '内科一病区',
              fillTime: '2024-01-15 10:30:25',
              responsibilityDept: '心血管内科',
              processStatus: 0
            },
            2: {
              id: 2,
              patientName: '李女士',
              gender: 0,
              age: 38,
              phone: '13900139000',
              dischargeDept: '神经内科',
              dischargeWard: '内科二病区',
              fillTime: '2024-01-14 16:20:10',
              responsibilityDept: '神经内科',
              processStatus: 0
            },
            3: {
              id: 3,
              patientName: '王先生',
              gender: 1,
              age: 52,
              phone: '13700137000',
              dischargeDept: '普外科',
              dischargeWard: '外科一病区',
              fillTime: '2024-01-13 09:15:45',
              responsibilityDept: '普外科',
              processStatus: 1
            }
          };
          this.currentRecord = mockRecords[this.recordId] || {
            id: 1,
            patientName: '张先生',
            gender: 1,
            age: 45,
            phone: '13800138000',
            dischargeDept: '心血管内科',
            dischargeWard: '内科一病区',
            fillTime: '2024-01-15 10:30:25',
            responsibilityDept: '心血管内科',
            processStatus: 0
          };
          resolve();
        }, 300);
      });
    },
    // åŠ è½½é—®å·æ•°æ®
    async loadQuestionnaireData() {
      return new Promise(resolve => {
        setTimeout(() => {
          this.questionnaireData = [
            {
              question: '您对医护人员的服务态度是否满意?',
              type: 1,
              options: [
                { value: '非常满意', text: '非常满意' },
                { value: '满意', text: '满意' },
                { value: '一般', text: '一般' },
                { value: '不满意', text: '不满意' },
                { value: '非常不满意', text: '非常不满意' }
              ],
              answer: '不满意',
              additional: '医生查房时间太短,沟通不够充分,对病情解释不够详细'
            },
            {
              question: '您对医生的诊疗水平和技术能力评价如何?',
              type: 1,
              options: [
                { value: '非常专业', text: '非常专业' },
                { value: '比较专业', text: '比较专业' },
                { value: '一般', text: '一般' },
                { value: '不够专业', text: '不够专业' },
                { value: '非常不专业', text: '非常不专业' }
              ],
              answer: '比较专业',
              additional: ''
            },
            {
              question: '您对医院的环境和卫生状况是否满意?',
              type: 1,
              options: [
                { value: '非常满意', text: '非常满意' },
                { value: '满意', text: '满意' },
                { value: '一般', text: '一般' },
                { value: '不满意', text: '不满意' },
                { value: '非常不满意', text: '非常不满意' }
              ],
              answer: '一般',
              additional: ''
            },
            {
              question: '您认为医护人员与您的沟通是否充分?',
              type: 1,
              options: [
                { value: '非常充分', text: '非常充分' },
                { value: '比较充分', text: '比较充分' },
                { value: '一般', text: '一般' },
                { value: '不够充分', text: '不够充分' },
                { value: '非常不充分', text: '非常不充分' }
              ],
              answer: '不够充分',
              additional: '医生讲解病情时语速太快,没有给足够的时间提问'
            },
            {
              question: '您对等待就诊和治疗的时间是否满意?',
              type: 1,
              options: [
                { value: '非常满意', text: '非常满意' },
                { value: '满意', text: '满意' },
                { value: '一般', text: '一般' },
                { value: '不满意', text: '不满意' },
                { value: '非常不满意', text: '非常不满意' }
              ],
              answer: '不满意',
              additional: '预约的9点,实际10点才见到医生'
            }
          ];
          resolve();
        }, 300);
      });
    },
    // åŠ è½½å¤„ç†è®°å½•
    async loadProcessRecords() {
      return new Promise(resolve => {
        setTimeout(() => {
          this.processRecords = [
            {
              id: 1,
              time: '2024-01-15 14:20:30',
              user: '张医生',
              status: 1, // å¤„理中
              reportDepts: ['医务科', '护理部'],
              remark: '已收到反馈,正在安排相关人员核查情况',
              attachments: [
                { id: 1, name: '初步调查记录.docx' },
                { id: 2, name: '患者沟通记录.jpg' }
              ]
            },
            {
              id: 2,
              time: '2024-01-15 10:45:12',
              user: '系统',
              status: 0, // å¾…处理
              remark: '系统自动识别为异常反馈,已分配到责任科室',
              attachments: []
            }
          ];
          resolve();
        }, 300);
      });
    },
    // åˆ¤æ–­æ˜¯å¦ä¸ºä¸æ»¡æ„é€‰é¡¹
    isUnsatisfactoryOption(value) {
      const unsatisfactoryValues = [
        '不满意',
        '非常不满意',
        '不够专业',
        '非常不专业',
        '不够充分',
        '非常不充分'
      ];
      return unsatisfactoryValues.includes(value);
    },
    // èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
    getStatusTagType(status) {
      switch (status) {
        case 0: return 'warning'; // å¾…处理
        case 1: return 'primary'; // å¤„理中
        case 2: return 'success'; // å·²å¤„理
        case 3: return 'danger';  // å·²é©³å›ž
        default: return 'info';
      }
    },
    // èŽ·å–çŠ¶æ€æ–‡æœ¬
    getStatusText(status) {
      switch (status) {
        case 0: return '待处理';
        case 1: return '处理中';
        case 2: return '已处理';
        case 3: return '已驳回';
        default: return '未知';
      }
    },
    // å¤„理异常
    handleProcess() {
      this.processForm = {
        status: this.currentRecord.processStatus === 0 ? 1 : this.currentRecord.processStatus,
        reportDepts: [],
        remark: ''
      };
      this.processDialogVisible = true;
    },
    // æäº¤å¤„理
    async submitProcess() {
      this.$refs.processForm.validate(async (valid) => {
        if (valid) {
          this.processing = true;
          try {
            // Mock API调用
            await new Promise(resolve => setTimeout(resolve, 1000));
            this.$message.success('处理提交成功');
            this.processDialogVisible = false;
            // é‡æ–°åŠ è½½æ•°æ®
            await this.loadData();
            // è§¦å‘父组件刷新
            this.$emit('processed');
          } finally {
            this.processing = false;
          }
        }
      });
    },
    // é¢„览文件
    handlePreviewFile(file) {
      this.$message.info(`预览文件: ${file.name}`);
    },
    // å¤„理对话框关闭
    handleClose() {
      this.$emit('close');
    },
    // æ–‡ä»¶ä¸Šä¼ ç›¸å…³æ–¹æ³•
    handleFilePreview(file) {
      console.log('预览文件:', file);
    },
    handleFileRemove(file, fileList) {
      console.log('移除文件:', file, fileList);
    },
    beforeFileRemove(file) {
      return this.$confirm(`确定移除 ${file.name}?`);
    },
    handleFileExceed(files, fileList) {
      this.$message.warning(`当前限制选择 3 ä¸ªæ–‡ä»¶ï¼Œæœ¬æ¬¡é€‰æ‹©äº† ${files.length} ä¸ªæ–‡ä»¶ï¼Œå…±é€‰æ‹©äº† ${files.length + fileList.length} ä¸ªæ–‡ä»¶`);
    }
  }
};
</script>
<style lang="scss" scoped>
.exception-detail-dialog {
  ::v-deep .el-dialog {
    max-height: 85vh;
    display: flex;
    flex-direction: column;
    .el-dialog__body {
      flex: 1;
      overflow-y: auto;
      padding: 20px;
    }
  }
  .info-section {
    margin-bottom: 20px;
    padding: 20px;
    background: #f8f9fa;
    border-radius: 8px;
    border: 1px solid #ebeef5;
    .section-title {
      font-size: 16px;
      font-weight: 600;
      color: #303133;
      margin-bottom: 15px;
      padding-bottom: 10px;
      border-bottom: 2px solid #409EFF;
    }
    .info-item {
      margin-bottom: 12px;
      display: flex;
      align-items: center;
      .label {
        font-size: 14px;
        color: #606266;
        min-width: 80px;
        font-weight: 500;
      }
      .value {
        font-size: 14px;
        color: #303133;
        font-weight: 500;
      }
    }
  }
  .questionnaire-section {
    margin-bottom: 20px;
    padding: 20px;
    background: #fff;
    border-radius: 8px;
    border: 1px solid #ebeef5;
    .section-title {
      font-size: 16px;
      font-weight: 600;
      color: #303133;
      margin-bottom: 15px;
      padding-bottom: 10px;
      border-bottom: 2px solid #409EFF;
    }
    .questionnaire-content {
      .question-item {
        margin-bottom: 20px;
        padding: 15px;
        border-radius: 6px;
        border: 1px solid #ebeef5;
        transition: all 0.3s;
        &:hover {
          border-color: #409EFF;
          box-shadow: 0 2px 12px 0 rgba(64, 158, 255, 0.1);
        }
        .question-header {
          display: flex;
          align-items: center;
          margin-bottom: 15px;
          padding-bottom: 10px;
          border-bottom: 1px dashed #dcdfe6;
          .question-index {
            font-weight: 600;
            color: #409EFF;
            margin-right: 8px;
            font-size: 15px;
          }
          .question-text {
            flex: 1;
            font-size: 15px;
            color: #303133;
            font-weight: 500;
            line-height: 1.5;
          }
          .question-type {
            margin-left: 10px;
          }
        }
        .question-options {
          ::v-deep .el-radio-group {
            display: flex;
            flex-direction: column;
            gap: 10px;
          }
          ::v-deep .el-checkbox-group {
            display: flex;
            flex-wrap: wrap;
            gap: 15px;
          }
          ::v-deep .el-radio,
          ::v-deep .el-checkbox {
            margin: 0;
            padding: 8px 12px;
            border-radius: 4px;
            border: 1px solid #ebeef5;
            transition: all 0.3s;
            &:hover {
              background: #f5f7fa;
            }
            &.unsatisfactory-option {
              border-color: #e6a23c;
              background: #fdf6ec;
            }
          }
        }
        .additional-remark {
          margin-top: 15px;
          padding: 12px;
          background: #f0f9ff;
          border-radius: 6px;
          border-left: 4px solid #409EFF;
          .remark-label {
            font-size: 13px;
            color: #606266;
            font-weight: 500;
            margin-bottom: 5px;
          }
          .remark-content {
            font-size: 14px;
            color: #303133;
            line-height: 1.6;
          }
        }
      }
    }
  }
  .process-section {
    .section-title {
      font-size: 16px;
      font-weight: 600;
      color: #303133;
      margin-bottom: 15px;
      padding-bottom: 10px;
      border-bottom: 2px solid #409EFF;
    }
    .process-timeline {
      ::v-deep .el-timeline-item {
        padding-bottom: 20px;
        .el-timeline-item__timestamp {
          font-size: 13px;
          color: #909399;
        }
      }
      .process-item {
        .process-header {
          display: flex;
          justify-content: space-between;
          align-items: center;
          margin-bottom: 10px;
          .process-user {
            font-size: 14px;
            font-weight: 600;
            color: #409EFF;
          }
        }
        .process-content {
          .process-depts {
            margin-bottom: 8px;
            .label {
              font-size: 13px;
              color: #606266;
              margin-right: 5px;
            }
            .dept-tag {
              margin-right: 5px;
              margin-bottom: 5px;
            }
          }
          .process-remark {
            margin-bottom: 8px;
            .label {
              font-size: 13px;
              color: #606266;
              margin-right: 5px;
            }
            .content {
              font-size: 13px;
              color: #303133;
              line-height: 1.5;
            }
          }
          .process-attachments {
            .label {
              font-size: 13px;
              color: #606266;
              margin-right: 5px;
            }
            ::v-deep .el-button {
              margin-right: 8px;
              margin-bottom: 5px;
            }
          }
        }
      }
    }
    .no-record {
      text-align: center;
      padding: 40px 0;
      color: #909399;
      font-style: italic;
      background: #f8f9fa;
      border-radius: 6px;
    }
  }
  .dialog-footer {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    gap: 10px;
  }
}
</style>
src/views/Satisfaction/configurationmyd/dispose.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,980 @@
<template>
  <div class="exception-list">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <div class="header-content">
        <h2 class="page-title">满意度异常处理</h2>
        <p class="page-description">监控和处理满意度问卷中的异常反馈</p>
      </div>
    </div>
    <!-- æŸ¥è¯¢æ¡ä»¶åŒºåŸŸ -->
    <div class="search-section">
      <el-card shadow="never">
        <el-form
          :model="queryParams"
          ref="queryForm"
          size="medium"
          :inline="true"
          label-width="120px"
          class="search-form"
        >
          <el-form-item label="满意度模板" prop="templateId">
            <el-select
              v-model="queryParams.templateId"
              placeholder="请选择模板"
              clearable
              style="width: 200px"
            >
              <el-option
                v-for="template in templateList"
                :key="template.id"
                :label="template.name"
                :value="template.id"
              />
            </el-select>
          </el-form-item>
          <el-form-item label="责任科室" prop="deptIds">
            <el-select
              v-model="queryParams.deptIds"
              placeholder="请选择责任科室"
              clearable
              filterable
              multiple
              collapse-tags
              style="width: 300px"
            >
              <el-option
                v-for="dept in deptList"
                :key="dept.id"
                :label="dept.name"
                :value="dept.id"
              />
            </el-select>
          </el-form-item>
          <el-form-item label="统计时间" prop="dateRange">
            <el-date-picker
              v-model="queryParams.dateRange"
              type="daterange"
              range-separator="至"
              start-placeholder="开始日期"
              end-placeholder="结束日期"
              value-format="yyyy-MM-dd"
              :picker-options="pickerOptions"
              style="width: 380px"
            />
          </el-form-item>
          <el-form-item>
            <el-button
              type="primary"
              icon="el-icon-search"
              @click="handleSearch"
              :loading="loading"
            >
              æŸ¥è¯¢
            </el-button>
            <el-button icon="el-icon-refresh" @click="handleReset">
              é‡ç½®
            </el-button>
          </el-form-item>
        </el-form>
      </el-card>
    </div>
    <!-- æ‰¹é‡æ“ä½œ -->
    <div class="batch-section">
      <el-card shadow="never">
        <div class="batch-actions">
          <el-button
            type="primary"
            icon="el-icon-s-operation"
            :disabled="selectedIds.length === 0"
            @click="handleBatchProcess"
          >
            æ‰¹é‡å¤„理 ({{ selectedIds.length }})
          </el-button>
          <el-button
            type="info"
            icon="el-icon-download"
            @click="handleExport"
          >
            å¯¼å‡ºå¼‚常数据
          </el-button>
          <el-button
            type="warning"
            icon="el-icon-refresh-right"
            @click="refreshData"
          >
            åˆ·æ–°æ•°æ®
          </el-button>
        </div>
      </el-card>
    </div>
    <!-- å¼‚常统计概览 -->
    <div class="overview-section">
      <el-row :gutter="20">
        <el-col :span="6">
          <el-card shadow="never" class="stat-card">
            <div class="stat-content">
              <div class="stat-icon" style="background: #f0f9ff;">
                <i class="el-icon-s-claim" style="color: #5788FE;"></i>
              </div>
              <div class="stat-info">
                <div class="stat-title">总异常数量</div>
                <div class="stat-value">{{ overviewData.totalExceptionCount }}</div>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="6">
          <el-card shadow="never" class="stat-card">
            <div class="stat-content">
              <div class="stat-icon" style="background: #f0f9ff;">
                <i class="el-icon-s-flag" style="color: #E6A23C;"></i>
              </div>
              <div class="stat-info">
                <div class="stat-title">待处理异常</div>
                <div class="stat-value">{{ overviewData.pendingCount }}</div>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="6">
          <el-card shadow="never" class="stat-card">
            <div class="stat-content">
              <div class="stat-icon" style="background: #f0f9ff;">
                <i class="el-icon-check" style="color: #67C23A;"></i>
              </div>
              <div class="stat-info">
                <div class="stat-title">已处理异常</div>
                <div class="stat-value">{{ overviewData.processedCount }}</div>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="6">
          <el-card shadow="never" class="stat-card">
            <div class="stat-content">
              <div class="stat-icon" style="background: #f0f9ff;">
                <i class="el-icon-s-order" style="color: #909399;"></i>
              </div>
              <div class="stat-info">
                <div class="stat-title">今日处理数</div>
                <div class="stat-value">{{ overviewData.todayProcessedCount }}</div>
              </div>
            </div>
          </el-card>
        </el-col>
      </el-row>
    </div>
    <!-- å¼‚常列表 -->
    <div class="list-section">
      <el-card shadow="never">
        <el-table
          v-loading="loading"
          :data="exceptionList"
          :border="true"
          style="width: 100%"
          @selection-change="handleSelectionChange"
          class="exception-table"
        >
          <el-table-column
            type="selection"
            width="55"
            align="center"
          />
          <el-table-column
            label="序号"
            type="index"
            width="60"
            align="center"
          />
          <el-table-column
            label="题目内容"
            prop="questionContent"
            min-width="300"
            align="center"
          >
            <template slot-scope="{ row }">
              <div class="question-content">
                <span class="question-text">{{ row.questionContent }}</span>
                <div class="question-tags">
                  <el-tag
                    size="mini"
                    :type="getQuestionTypeTag(row.questionType)"
                  >
                    {{ row.questionType === 1 ? '单选题' : '多选题' }}
                  </el-tag>
                  <el-tag
                    size="mini"
                    type="info"
                  >
                    {{ row.templateName }}
                  </el-tag>
                </div>
              </div>
            </template>
          </el-table-column>
          <el-table-column
            label="负责科室"
            prop="responsibilityDepts"
            width="180"
            align="center"
          >
            <template slot-scope="{ row }">
              <div class="dept-list">
                <el-tag
                  v-for="dept in row.responsibilityDepts"
                  :key="dept.id"
                  size="small"
                  type="primary"
                  class="dept-tag"
                >
                  {{ dept.name }}
                </el-tag>
              </div>
            </template>
          </el-table-column>
          <el-table-column
            label="填写情况"
            width="200"
            align="center"
          >
            <template slot-scope="{ row }">
              <div class="fill-statistics">
                <div class="stat-item">
                  <span class="stat-label">有效填写:</span>
                  <span class="stat-value">{{ row.validFillCount }}</span>
                </div>
                <div class="stat-item">
                  <span class="stat-label">异常填写:</span>
                  <span class="stat-value exception-count">{{ row.exceptionFillCount }}</span>
                </div>
              </div>
            </template>
          </el-table-column>
          <el-table-column
            label="异常任务"
            width="280"
            align="center"
          >
            <template slot-scope="{ row }">
              <div class="exception-tasks">
                <div class="task-category">
                  <div class="task-title">已处理</div>
                  <div class="task-count processed">{{ row.processedCount }}</div>
                </div>
                <div class="task-category">
                  <div class="task-title">待处理</div>
                  <div class="task-count pending">{{ row.pendingCount }}</div>
                </div>
                <div class="task-category">
                  <div class="task-title">异常总数</div>
                  <div class="task-count total">{{ row.totalExceptionCount }}</div>
                </div>
              </div>
            </template>
          </el-table-column>
          <el-table-column
            label="最近处理"
            prop="lastProcessTime"
            width="180"
            align="center"
          >
            <template slot-scope="{ row }">
              <div v-if="row.lastProcessTime" class="last-process">
                <div class="process-time">{{ row.lastProcessTime }}</div>
                <div class="process-user">{{ row.lastProcessUser }}</div>
              </div>
              <span v-else class="no-process">暂无处理记录</span>
            </template>
          </el-table-column>
          <el-table-column
            label="操作"
            width="180"
            align="center"
            fixed="right"
          >
            <template slot-scope="{ row }">
              <!-- <el-button
                type="primary"
                size="small"
                icon="el-icon-view"
                @click="handleViewDetail(row)"
              >
                è¯¦æƒ…
              </el-button> -->
              <el-button
                type="warning"
                size="small"
                icon="el-icon-s-operation"
                @click="handleBatchQuestion(row)"
              >
                æ‰¹é‡å¤„理
              </el-button>
            </template>
          </el-table-column>
        </el-table>
        <!-- åˆ†é¡µ -->
        <div class="pagination-section">
          <el-pagination
            background
            layout="total, sizes, prev, pager, next, jumper"
            :current-page="queryParams.pageNum"
            :page-size="queryParams.pageSize"
            :page-sizes="[10, 20, 30, 50]"
            :total="total"
            @size-change="handleSizeChange"
            @current-change="handlePageChange"
          />
        </div>
      </el-card>
    </div>
  </div>
</template>
<script>
export default {
  name: 'ExceptionList',
  data() {
    return {
      // æŸ¥è¯¢å‚æ•°
      queryParams: {
        templateId: '',
        deptIds: [],
        dateRange: [],
        pageNum: 1,
        pageSize: 10
      },
      // åŠ è½½çŠ¶æ€
      loading: false,
      // é€‰ä¸­çš„ID
      selectedIds: [],
      // æ¨¡æ¿åˆ—表
      templateList: [
        { id: 1, name: '出院满意度问卷' },
        { id: 2, name: '住院满意度问卷' },
        { id: 3, name: '门诊满意度问卷' },
        { id: 4, name: '常用满意度问卷' }
      ],
      // ç§‘室列表
      deptList: [
        { id: 1, name: '心血管内科' },
        { id: 2, name: '神经内科' },
        { id: 3, name: '普外科' },
        { id: 4, name: '骨科' },
        { id: 5, name: '妇产科' },
        { id: 6, name: '儿科' },
        { id: 7, name: '急诊科' },
        { id: 8, name: '呼吸内科' },
        { id: 9, name: '消化内科' },
        { id: 10, name: '内分泌科' },
        { id: 11, name: '肾内科' },
        { id: 12, name: '肿瘤科' }
      ],
      // å¼‚常列表数据
      exceptionList: [],
      total: 0,
      // ç»Ÿè®¡æ¦‚览数据
      overviewData: {
        totalExceptionCount: 0,
        pendingCount: 0,
        processedCount: 0,
        todayProcessedCount: 0
      },
      // æ—¥æœŸé€‰æ‹©å™¨é€‰é¡¹
      pickerOptions: {
        shortcuts: [
          {
            text: '最近一周',
            onClick(picker) {
              const end = new Date();
              const start = new Date();
              start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
              picker.$emit('pick', [start, end]);
            }
          },
          {
            text: '最近一个月',
            onClick(picker) {
              const end = new Date();
              const start = new Date();
              start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
              picker.$emit('pick', [start, end]);
            }
          },
          {
            text: '最近三个月',
            onClick(picker) {
              const end = new Date();
              const start = new Date();
              start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
              picker.$emit('pick', [start, end]);
            }
          }
        ],
        disabledDate(time) {
          return time.getTime() > Date.now();
        }
      }
    };
  },
  mounted() {
    this.loadData();
  },
  methods: {
    // åŠ è½½æ•°æ®
    async loadData() {
      this.loading = true;
      try {
        await Promise.all([
          this.loadExceptionList(),
          this.loadOverviewData()
        ]);
      } finally {
        this.loading = false;
      }
    },
    // åŠ è½½å¼‚å¸¸åˆ—è¡¨
    async loadExceptionList() {
      return new Promise((resolve) => {
        setTimeout(() => {
          // Mock æ•°æ®
          this.exceptionList = [
            {
              id: 1,
              questionId: 101,
              questionContent: '您对医护人员的服务态度是否满意?',
              questionType: 1, // 1: å•选题, 2: å¤šé€‰é¢˜
              templateName: '出院满意度问卷',
              responsibilityDepts: [
                { id: 1, name: '心血管内科' },
                { id: 2, name: '神经内科' }
              ],
              validFillCount: 145,
              exceptionFillCount: 8,
              processedCount: 5,
              pendingCount: 3,
              totalExceptionCount: 8,
              lastProcessTime: '2024-01-15 10:30:25',
              lastProcessUser: '张医生'
            },
            {
              id: 2,
              questionId: 102,
              questionContent: '您对医生的诊疗水平和技术能力评价如何?',
              questionType: 1,
              templateName: '住院满意度问卷',
              responsibilityDepts: [
                { id: 3, name: '普外科' },
                { id: 4, name: '骨科' }
              ],
              validFillCount: 120,
              exceptionFillCount: 12,
              processedCount: 8,
              pendingCount: 4,
              totalExceptionCount: 12,
              lastProcessTime: '2024-01-14 16:20:10',
              lastProcessUser: '李护士长'
            },
            {
              id: 3,
              questionId: 103,
              questionContent: '您对医院的环境和卫生状况是否满意?',
              questionType: 1,
              templateName: '门诊满意度问卷',
              responsibilityDepts: [
                { id: 5, name: '妇产科' },
                { id: 6, name: '儿科' },
                { id: 7, name: '急诊科' }
              ],
              validFillCount: 180,
              exceptionFillCount: 15,
              processedCount: 10,
              pendingCount: 5,
              totalExceptionCount: 15,
              lastProcessTime: '2024-01-13 09:15:45',
              lastProcessUser: '王主任'
            },
            {
              id: 4,
              questionId: 104,
              questionContent: '您认为医护人员与您的沟通是否充分?',
              questionType: 1,
              templateName: '常用满意度问卷',
              responsibilityDepts: [
                { id: 8, name: '呼吸内科' },
                { id: 9, name: '消化内科' }
              ],
              validFillCount: 95,
              exceptionFillCount: 6,
              processedCount: 4,
              pendingCount: 2,
              totalExceptionCount: 6,
              lastProcessTime: '2024-01-12 14:40:30',
              lastProcessUser: '赵医生'
            },
            {
              id: 5,
              questionId: 105,
              questionContent: '您对等待就诊和治疗的时间是否满意?',
              questionType: 1,
              templateName: '住院满意度问卷',
              responsibilityDepts: [
                { id: 10, name: '内分泌科' },
                { id: 11, name: '肾内科' }
              ],
              validFillCount: 200,
              exceptionFillCount: 25,
              processedCount: 15,
              pendingCount: 10,
              totalExceptionCount: 25,
              lastProcessTime: '2024-01-11 11:25:15',
              lastProcessUser: '孙护士'
            },
            {
              id: 6,
              questionId: 106,
              questionContent: '您对医院收费的透明度和合理性评价如何?',
              questionType: 1,
              templateName: '门诊满意度问卷',
              responsibilityDepts: [
                { id: 12, name: '肿瘤科' }
              ],
              validFillCount: 160,
              exceptionFillCount: 18,
              processedCount: 12,
              pendingCount: 6,
              totalExceptionCount: 18,
              lastProcessTime: '2024-01-10 15:50:20',
              lastProcessUser: '周医生'
            },
            {
              id: 7,
              questionId: 107,
              questionContent: '您会向亲友推荐我们医院吗?',
              questionType: 1,
              templateName: '出院满意度问卷',
              responsibilityDepts: [
                { id: 1, name: '心血管内科' },
                { id: 8, name: '呼吸内科' }
              ],
              validFillCount: 110,
              exceptionFillCount: 7,
              processedCount: 5,
              pendingCount: 2,
              totalExceptionCount: 7,
              lastProcessTime: '2024-01-09 10:15:40',
              lastProcessUser: '吴主任'
            },
            {
              id: 8,
              questionId: 108,
              questionContent: '您对以下哪些方面比较满意(多选)?',
              questionType: 2,
              templateName: '常用满意度问卷',
              responsibilityDepts: [
                { id: 2, name: '神经内科' },
                { id: 3, name: '普外科' },
                { id: 5, name: '妇产科' }
              ],
              validFillCount: 135,
              exceptionFillCount: 9,
              processedCount: 6,
              pendingCount: 3,
              totalExceptionCount: 9,
              lastProcessTime: '2024-01-08 13:30:55',
              lastProcessUser: '郑医生'
            }
          ];
          this.total = this.exceptionList.length;
          resolve();
        }, 500);
      });
    },
    // åŠ è½½æ¦‚è§ˆæ•°æ®
    async loadOverviewData() {
      return new Promise((resolve) => {
        setTimeout(() => {
          // è®¡ç®—统计数据
          const totalExceptionCount = this.exceptionList.reduce((sum, item) => sum + item.totalExceptionCount, 0);
          const pendingCount = this.exceptionList.reduce((sum, item) => sum + item.pendingCount, 0);
          const processedCount = this.exceptionList.reduce((sum, item) => sum + item.processedCount, 0);
          this.overviewData = {
            totalExceptionCount,
            pendingCount,
            processedCount,
            todayProcessedCount: 8 // ä»Šæ—¥å¤„理数 mock
          };
          resolve();
        }, 300);
      });
    },
    // èŽ·å–é¢˜ç›®ç±»åž‹æ ‡ç­¾æ ·å¼
    getQuestionTypeTag(type) {
      return type === 1 ? 'primary' : 'success';
    },
    // å¤„理查询
    handleSearch() {
      this.queryParams.pageNum = 1;
      this.loadData();
    },
    // å¤„理重置
    handleReset() {
      this.$refs.queryForm.resetFields();
      this.queryParams.dateRange = [];
      this.queryParams.pageNum = 1;
      this.loadData();
    },
    // å¤„理批量处理
    handleBatchProcess() {
      if (this.selectedIds.length === 0) {
        this.$message.warning('请先选择要处理的异常题目');
        return;
      }
      // è·³è½¬åˆ°æ‰¹é‡å¤„理页面
      this.$router.push({
        path: '/satisfaction/exception/batch-process',
        query: {
          questionIds: this.selectedIds.join(',')
        }
      });
    },
    // å¤„理导出
    handleExport() {
      this.$message.success('导出功能开发中...');
    },
    // åˆ·æ–°æ•°æ®
    refreshData() {
      this.loadData();
      this.$message.success('数据已刷新');
    },
    // å¤„理选择变化
    handleSelectionChange(selection) {
      this.selectedIds = selection.map(item => item.questionId);
    },
    // å¤„理查看详情
    handleViewDetail(row) {
      this.$router.push({
        path: '/satisfaction/exception/detail',
        query: {
          id: row.questionId
        }
      });
    },
    // å¤„理单个题目批量处理
    handleBatchQuestion(row) {
      this.$router.push({
        path: '/Intelligentcenter/batch',
        query: {
          questionId: row.questionId
        }
      });
    },
    // å¤„理分页大小变化
    handleSizeChange(size) {
      this.queryParams.pageSize = size;
      this.queryParams.pageNum = 1;
      this.loadExceptionList();
    },
    // å¤„理页码变化
    handlePageChange(page) {
      this.queryParams.pageNum = page;
      this.loadExceptionList();
    }
  }
};
</script>
<style lang="scss" scoped>
.exception-list {
  padding: 20px;
  background-color: #f5f7fa;
  min-height: 100vh;
  .page-header {
    margin-bottom: 20px;
    padding: 20px;
    background: linear-gradient(135deg, #5788FE 0%, #66b1ff 100%);
    border-radius: 8px;
    color: white;
    .header-content {
      .page-title {
        margin: 0 0 8px 0;
        font-size: 20px;
        font-weight: 600;
      }
      .page-description {
        margin: 0;
        opacity: 0.9;
        font-size: 14px;
      }
    }
  }
  .search-section {
    margin-bottom: 20px;
    .search-form {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      ::v-deep .el-form-item {
        margin-bottom: 0;
        margin-right: 20px;
        &:last-child {
          margin-right: 0;
        }
      }
    }
  }
  .batch-section {
    margin-bottom: 20px;
    .batch-actions {
      display: flex;
      gap: 10px;
      align-items: center;
    }
  }
  .overview-section {
    margin-bottom: 20px;
    .stat-card {
      .stat-content {
        display: flex;
        align-items: center;
        gap: 20px;
        .stat-icon {
          width: 60px;
          height: 60px;
          border-radius: 12px;
          display: flex;
          align-items: center;
          justify-content: center;
          i {
            font-size: 28px;
          }
        }
        .stat-info {
          flex: 1;
          .stat-title {
            font-size: 14px;
            color: #606266;
            margin-bottom: 8px;
          }
          .stat-value {
            font-size: 24px;
            font-weight: 600;
            color: #303133;
          }
        }
      }
    }
  }
  .list-section {
    .exception-table {
      ::v-deep .el-table__header-wrapper {
        th {
          background-color: #f8f9fa;
          font-weight: 600;
          color: #333;
        }
      }
      .question-content {
        .question-text {
          display: block;
          font-size: 14px;
          color: #303133;
          margin-bottom: 8px;
          line-height: 1.5;
        }
        .question-tags {
          display: flex;
          gap: 5px;
          justify-content: center;
        }
      }
      .dept-list {
        display: flex;
        flex-direction: column;
        gap: 5px;
        align-items: center;
        .dept-tag {
          margin: 2px 0;
        }
      }
      .fill-statistics {
        .stat-item {
          display: flex;
          justify-content: space-between;
          align-items: center;
          margin-bottom: 5px;
          padding: 2px 0;
          .stat-label {
            font-size: 12px;
            color: #606266;
          }
          .stat-value {
            font-size: 14px;
            font-weight: 500;
            color: #333;
          }
          .exception-count {
            color: #e6a23c;
            font-weight: 600;
          }
        }
      }
      .exception-tasks {
        display: flex;
        justify-content: space-around;
        align-items: center;
        .task-category {
          text-align: center;
          padding: 5px 8px;
          border-radius: 6px;
          min-width: 70px;
          .task-title {
            font-size: 12px;
            color: #606266;
            margin-bottom: 5px;
          }
          .task-count {
            font-size: 18px;
            font-weight: 600;
            line-height: 1;
            &.processed {
              color: #67c23a;
            }
            &.pending {
              color: #e6a23c;
            }
            &.total {
              color: #5788FE;
            }
          }
        }
      }
      .last-process {
        .process-time {
          font-size: 12px;
          color: #606266;
          margin-bottom: 4px;
        }
        .process-user {
          font-size: 13px;
          color: #5788FE;
          font-weight: 500;
        }
      }
      .no-process {
        color: #909399;
        font-style: italic;
      }
    }
    .pagination-section {
      display: flex;
      justify-content: center;
      padding: 20px 0 0 0;
    }
  }
}
@media (max-width: 768px) {
  .exception-list {
    padding: 10px;
    .search-section {
      .search-form {
        ::v-deep .el-form-item {
          width: 100%;
          margin-right: 0;
          margin-bottom: 10px;
        }
      }
    }
    .overview-section {
      .el-row {
        margin-left: 0;
        margin-right: 0;
        .el-col {
          padding-left: 0;
          padding-right: 0;
          margin-bottom: 10px;
        }
      }
    }
  }
}
</style>
src/views/Satisfaction/configurationmyd/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2376 @@
<template>
  <div class="satisfaction-exception-config">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <div class="header-content">
        <h2 class="page-title">满意度题目异常处理配置</h2>
        <p class="page-description">
          åŸºäºŽæ¨¡æ¿é…ç½®æ»¡æ„åº¦é¢˜ç›®çš„责任科室和报备科室
        </p>
      </div>
    </div>
    <!-- æ¨¡æ¿é€‰æ‹©åŒºåŸŸ -->
    <div class="template-section">
      <el-card shadow="never">
        <div class="template-header">
          <h3 class="template-title">模板选择</h3>
          <p class="template-tip">请先选择模板类型和具体模板</p>
        </div>
        <el-form
          :model="templateForm"
          :rules="templateRules"
          ref="templateForm"
          label-width="120px"
          size="medium"
        >
          <el-row :gutter="20">
            <el-col :span="8">
              <el-form-item label="模板类型" prop="templateType">
                <el-select
                  v-model="templateForm.templateType"
                  placeholder="请选择模板类型"
                  clearable
                  @change="handleTemplateTypeChange"
                  style="width: 100%"
                >
                  <el-option label="问卷模板" :value="1" />
                  <el-option label="语音模板" :value="2" />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item
                label="选择模板"
                prop="templateId"
                :rules="
                  templateForm.templateType
                    ? [
                        {
                          required: true,
                          message: '请选择模板',
                          trigger: 'change',
                        },
                      ]
                    : []
                "
              >
                <el-select
                  v-model="templateForm.templateId"
                  placeholder="请选择模板"
                  clearable
                  filterable
                  @change="handleTemplateChange"
                  style="width: 100%"
                >
                  <el-option
                    v-for="template in filteredTemplateOptions"
                    :key="template.id"
                    :label="template.templateName"
                    :value="template.id"
                  />
                  <div
                    v-if="templateOptionsLoading"
                    slot="empty"
                    class="select-loading"
                  >
                    <i class="el-icon-loading"></i>
                    <span>加载中...</span>
                  </div>
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item>
                <el-button
                  type="primary"
                  icon="el-icon-search"
                  @click="handleLoadTemplate"
                  :loading="templateLoading"
                  :disabled="!templateForm.templateId"
                >
                  åŠ è½½æ¨¡æ¿é¢˜ç›®
                </el-button>
                <el-button icon="el-icon-refresh" @click="handleResetTemplate">
                  é‡ç½®
                </el-button>
              </el-form-item>
            </el-col>
          </el-row>
        </el-form>
      </el-card>
    </div>
    <!-- æ¨¡æ¿ä¿¡æ¯ -->
    <div v-if="currentTemplateInfo" class="template-info-section">
      <el-card shadow="never">
        <div class="template-info">
          <div class="info-left">
            <h3 class="template-name">
              {{ currentTemplateInfo.templateName }}
            </h3>
            <div class="template-meta">
              <span class="meta-item">
                <i class="el-icon-s-order"></i>
                æ¨¡æ¿ç±»åž‹ï¼š{{
                  templateForm.templateType === 1 ? "问卷模板" : "语音模板"
                }}
              </span>
              <span class="meta-item">
                <i class="el-icon-s-management"></i>
                é¢˜ç›®æ€»æ•°ï¼š{{ currentTemplateInfo.questionCount || 0 }}
              </span>
              <span class="meta-item">
                <i class="el-icon-star-on"></i>
                æ»¡æ„åº¦é¢˜ç›®ï¼š{{ satisfactionQuestionsCount }}
              </span>
            </div>
          </div>
          <div class="info-right">
            <el-tag
              :type="
                currentTemplateInfo.templateStatus === 1 ? 'success' : 'info'
              "
              size="medium"
            >
              {{ currentTemplateInfo.templateStatus === 1 ? "启用" : "停用" }}
            </el-tag>
          </div>
        </div>
      </el-card>
    </div>
    <!-- æœç´¢åŒºåŸŸï¼ˆé¢˜ç›®ç­›é€‰ï¼‰ -->
    <div v-if="questionList.length > 0" class="search-section">
      <el-card shadow="never" class="search-container">
        <el-form :model="queryParams" :inline="true" size="medium">
          <el-form-item label="问题主题">
            <el-input
              v-model="queryParams.scriptTopic"
              placeholder="请输入问题主题"
              clearable
              @keyup.enter.native="handleQuery"
            />
          </el-form-item>
          <el-form-item label="问题内容">
            <el-input
              v-model="queryParams.scriptContent"
              placeholder="请输入问题内容"
              clearable
              @keyup.enter.native="handleQuery"
            />
          </el-form-item>
          <el-form-item>
            <el-button
              type="primary"
              icon="el-icon-search"
              @click="handleQuery"
            >
              ç­›é€‰é¢˜ç›®
            </el-button>
            <el-button icon="el-icon-refresh" @click="resetQuery">
              é‡ç½®ç­›é€‰
            </el-button>
          </el-form-item>
        </el-form>
      </el-card>
    </div>
    <!-- é…ç½®åˆ—表 -->
    <div class="config-content">
      <!-- æ‰¹é‡æ“ä½œæ  -->
      <div v-if="questionList.length > 0" class="batch-actions-card">
        <el-card shadow="never">
          <div class="batch-actions">
            <el-button
              type="success"
              icon="el-icon-check"
              :loading="batchSaving"
              :disabled="!hasChanges || batchSaving"
              @click="handleBatchSave"
              size="medium"
            >
              {{ batchSaving ? "批量保存中..." : "批量保存配置" }}
            </el-button>
            <span v-if="changedCount > 0" class="change-count">
              æœ‰ {{ changedCount }} é¡¹é…ç½®éœ€è¦ä¿å­˜
            </span>
            <div class="total-count">
              å…± {{ filteredQuestionList.length }} æ¡è®°å½•
            </div>
          </div>
        </el-card>
      </div>
      <div v-if="loading" class="loading-wrapper">
        <div class="loading-spinner">
          <i class="el-icon-loading"></i>
          <span>加载中...</span>
        </div>
      </div>
      <div
        v-else-if="questionList.length === 0 && templateForm.templateId"
        class="empty-wrapper"
      >
        <el-empty description="该模板中暂无满意度题目">
          <p class="empty-tip">
            è¯·é€‰æ‹©å…¶ä»–模板或检查模板中是否包含满意度类型题目(分类ID:
            404,405,406)
          </p>
        </el-empty>
      </div>
      <!-- ä¸€è¡Œä¸€è¡Œçš„卡片列表 -->
      <div v-else-if="filteredQuestionList.length > 0" class="question-list">
        <div
          v-for="(question, index) in filteredQuestionList"
          :key="question.id"
          class="question-item"
        >
          <el-card
            shadow="hover"
            class="question-card"
            :class="{ 'has-changes': question.hasChanges }"
          >
            <!-- å¡ç‰‡å¤´éƒ¨ -->
            <div class="card-header">
              <div class="header-left">
                <div class="question-index">
                  <span class="index-number">{{ index + 1 }}</span>
                  <div class="index-line"></div>
                </div>
                <div class="question-basic-info">
                  <div class="question-title-section">
                    <h3 class="question-topic" :title="question.scriptTopic">
                      {{ question.scriptTopic || "无主题" }}
                    </h3>
                    <div class="question-tags">
                      <dict-tag
                        :options="askvaluetype"
                        :value="question.scriptType"
                        size="small"
                      />
                      <el-tag
                        v-if="question.targetname"
                        size="small"
                        type="info"
                      >
                        {{ question.targetname }}
                      </el-tag>
                    </div>
                  </div>
                  <div class="question-content-section">
                    <span class="content-label">题目内容:</span>
                    <span class="content-text">{{
                      question.scriptContent
                    }}</span>
                  </div>
                </div>
              </div>
              <div class="header-right">
                <!-- å¼‚常选项状态 -->
                <div
                  class="option-status"
                  v-if="
                    templateForm.templateType != 3 &&
                    templateForm.templateType != 4
                  "
                >
                  <el-tooltip
                    :content="
                      checkHasAbnormalOptions(question)
                        ? '已有异常选项'
                        : '暂无异常选项'
                    "
                    placement="top"
                  >
                    <el-tag
                      :type="
                        checkHasAbnormalOptions(question) ? 'success' : 'danger'
                      "
                      size="small"
                      class="status-tag"
                    >
                      <i
                        :class="
                          checkHasAbnormalOptions(question)
                            ? 'el-icon-success'
                            : 'el-icon-warning'
                        "
                      ></i>
                      {{
                        checkHasAbnormalOptions(question)
                          ? "异常选项已配置"
                          : "无异常选项"
                      }}
                    </el-tag>
                  </el-tooltip>
                </div>
                <el-button
                  type="text"
                  icon="el-icon-view"
                  @click="previewQuestion(question)"
                  size="small"
                >
                  é¢„览
                </el-button>
                <!-- æ·»åŠ é…ç½®é€‰é¡¹æŒ‰é’® -->
                <el-button
                  v-if="
                    templateForm.templateType != 3 &&
                    templateForm.templateType != 4
                  "
                  type="text"
                  icon="el-icon-setting"
                  @click="openOptionDialog(question)"
                  size="small"
                >
                  é…ç½®é€‰é¡¹
                </el-button>
              </div>
            </div>
            <!-- å¼‚常处理配置 -->
            <div class="config-section">
              <div class="config-title">
                <i class="el-icon-setting"></i>
                <span>异常处理配置</span>
              </div>
              <el-form
                :model="question.exceptionConfig"
                :rules="configRules"
                ref="configForm"
                label-width="100px"
                size="small"
                class="config-form"
              >
                <div class="config-fields">
                  <!-- è´£ä»»ç§‘室(多选) -->
                  <div class="config-field">
                    <el-form-item
                      label="责任科室"
                      prop="responsibilityDept"
                      class="config-item"
                    >
                      <el-select
                        v-model="question.exceptionConfig.responsibilityDept"
                        placeholder="请选择责任科室"
                        filterable
                        clearable
                        multiple
                        style="width: 100%"
                        @change="handleConfigChange(question)"
                      >
                        <el-option
                          v-for="dept in deptOptions"
                          :key="dept.id"
                          :label="dept.label"
                          :value="dept.deptCode"
                        />
                      </el-select>
                      <div class="config-tip">
                        è´Ÿè´£å¤„理该题目反馈的科室,可多选
                      </div>
                    </el-form-item>
                  </div>
                  <!-- æŠ¥å¤‡ç§‘室(多选) -->
                  <div class="config-field">
                    <el-form-item
                      label="报备科室"
                      prop="reportDept"
                      class="config-item"
                    >
                      <el-select
                        v-model="question.exceptionConfig.reportDept"
                        placeholder="请选择报备科室"
                        filterable
                        clearable
                        multiple
                        style="width: 100%"
                        @change="handleConfigChange(question)"
                      >
                        <el-option
                          v-for="dept in deptOptions"
                          :key="dept.id"
                          :label="dept.label"
                          :value="dept.deptCode"
                        />
                      </el-select>
                      <div class="config-tip">
                        éœ€è¦æŽ¥æ”¶å¼‚常反馈的科室,可多选
                      </div>
                    </el-form-item>
                  </div>
                </div>
                <!-- å½“前配置信息 -->
                <div v-if="question.hasChanges" class="current-config">
                  <div class="config-preview">
                    <div class="preview-item">
                      <span class="preview-label">责任科室:</span>
                      <span class="preview-value">
                        {{
                          getDeptNames(
                            question.exceptionConfig.responsibilityDept || []
                          ).join(", ")
                        }}
                      </span>
                    </div>
                    <div class="preview-item">
                      <span class="preview-label">报备科室:</span>
                      <span class="preview-value">
                        {{
                          getDeptNames(
                            question.exceptionConfig.reportDept || []
                          ).join(", ")
                        }}
                      </span>
                    </div>
                  </div>
                </div>
                <!-- é…ç½®çŠ¶æ€å’Œæ“ä½œæŒ‰é’® -->
                <div class="config-footer">
                  <div v-if="question.saveStatus" class="save-status">
                    <el-alert
                      :type="question.saveStatus.type"
                      :title="question.saveStatus.message"
                      :closable="false"
                      show-icon
                      :effect="
                        question.saveStatus.type === 'success'
                          ? 'dark'
                          : 'light'
                      "
                      size="small"
                    />
                  </div>
                  <div class="config-actions">
                    <el-button
                      type="primary"
                      :loading="question.saving"
                      :disabled="!question.hasChanges"
                      @click="saveSingleConfig(question)"
                      size="small"
                      icon="el-icon-check"
                    >
                      {{ question.saving ? "保存中..." : "保存配置" }}
                    </el-button>
                    <el-button
                      v-if="question.hasChanges"
                      type="text"
                      @click="resetSingleConfig(question)"
                      size="small"
                    >
                      é‡ç½®
                    </el-button>
                  </div>
                </div>
              </el-form>
            </div>
          </el-card>
        </div>
      </div>
    </div>
<!-- é€‰é¡¹é…ç½®å¯¹è¯æ¡† -->
<el-dialog
  title="选项异常状态配置"
  :visible.sync="optionDialogVisible"
  width="700px"
  center
  :close-on-click-modal="false"
>
  <div v-if="editingQuestion" class="option-config-wrapper">
    <div class="dialog-header">
      <h4>{{ editingQuestion.scriptTopic || '无主题' }}</h4>
      <p class="dialog-subtitle">{{ editingQuestion.scriptContent }}</p>
    </div>
    <div class="option-list">
      <el-alert
        v-if="!currentOptions.some(opt => opt.isabnormal === 1)"
        title="请至少设置一个异常选项(标记为异常)"
        type="warning"
        :closable="false"
        show-icon
        style="margin-bottom: 20px;"
      />
      <div v-for="(option, index) in currentOptions" :key="index" class="option-item">
        <el-form
          :model="option"
          :rules="optionRules"
          ref="optionForm"
          size="small"
          class="option-form"
        >
          <el-row :gutter="12" align="middle">
            <el-col :span="2">
              <div class="option-index">#{{ index + 1 }}</div>
            </el-col>
            <el-col :span="12">
              <el-form-item prop="targetvalue">
                <el-input
                  v-model="option.targetvalue"
                  placeholder="请输入选项内容"
                  clearable
                  maxlength="200"
                  show-word-limit
                />
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item prop="isabnormal">
                <el-select
                  v-model="option.isabnormal"
                  placeholder="选择状态"
                  style="width: 100%"
                >
                  <el-option
                    v-for="status in abnormalOptions"
                    :key="status.value"
                    :label="status.label"
                    :value="status.value"
                  >
                    <el-tag :type="status.type" size="small">{{ status.label }}</el-tag>
                  </el-option>
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="4">
              <el-button
                type="danger"
                icon="el-icon-delete"
                @click="removeOption(index)"
                size="small"
                circle
                plain
              />
            </el-col>
          </el-row>
        </el-form>
      </div>
      <!-- <el-button
        type="primary"
        icon="el-icon-plus"
        @click="addNewOption"
        size="small"
        plain
        style="width: 100%; margin-top: 10px;"
      >
        æ·»åР选项
      </el-button> -->
    </div>
  </div>
  <span slot="footer" class="dialog-footer">
    <el-button @click="optionDialogVisible = false">取消</el-button>
    <el-button type="primary" @click="saveOptions" :loading="savingOptions">
      ä¿å­˜é…ç½®
    </el-button>
  </span>
</el-dialog>
    <!-- é¢˜ç›®é¢„览对话框 -->
    <el-dialog
      title="题目预览"
      :visible.sync="previewVisible"
      width="600px"
      center
    >
      <div v-if="currentPreview" class="preview-wrapper">
        <div class="preview-header">
          <h4>{{ currentPreview.scriptTopic || "无主题" }}</h4>
          <div class="preview-tags">
            <dict-tag
              :options="askvaluetype"
              :value="currentPreview.scriptType"
              size="small"
            />
            <el-tag v-if="currentPreview.targetname" size="small" type="info">
              {{ currentPreview.targetname }}
            </el-tag>
          </div>
        </div>
        <!-- æ¨¡æ¿é¢˜ç›®å±•示 -->
        <div class="preview-content" v-if="templateForm.templateType == 1">
          <p class="preview-question">{{ currentPreview.scriptContent }}</p>
          <div
            v-if="
              currentPreview.scriptType != 3 && currentPreview.scriptType != 4
            "
            class="preview-options"
          >
            <el-radio-group v-model="previewAnswer">
              <el-radio
                v-for="(
                  option, idx
                ) in currentPreview.svyLibTemplateTargetoptions || []"
                :key="idx"
                :label="option.optioncontent"
                class="option-item"
              >
                {{ option.optioncontent }}
              </el-radio>
            </el-radio-group>
          </div>
          <div v-else class="preview-textarea">
            <el-input
              type="textarea"
              placeholder="请输入回答"
              v-model="previewAnswer"
              :rows="4"
            />
          </div>
        </div>
        <!-- è¯­éŸ³é¢˜ç›®å±•示 -->
        <div class="preview-content" v-else>
          <p class="preview-question">{{ currentPreview.scriptContent }}</p>
          <div
            v-if="
              currentPreview.scriptType != 3 && currentPreview.scriptType != 4
            "
            class="preview-options"
          >
            <el-radio-group v-model="previewAnswer">
              <el-radio
                v-for="(
                  option, idx
                ) in currentPreview.ivrLibaScriptTargetoptionList || []"
                :key="idx"
                :label="option.targetvalue"
                class="option-item"
              >
                {{ option.targetvalue }}
              </el-radio>
            </el-radio-group>
          </div>
          <div v-else class="preview-textarea">
            <el-input
              type="textarea"
              placeholder="请输入回答"
              v-model="previewAnswer"
              :rows="4"
            />
          </div>
        </div>
      </div>
      <span slot="footer" class="dialog-footer">
        <el-button @click="previewVisible = false">关闭</el-button>
      </span>
    </el-dialog>
    <!-- ä¿å­˜æˆåŠŸæç¤º -->
    <el-dialog
      title="保存成功"
      :visible.sync="saveSuccessVisible"
      width="400px"
      center
    >
      <div class="success-content">
        <i class="el-icon-success success-icon"></i>
        <p class="success-text">配置已成功保存!</p>
      </div>
      <span slot="footer" class="dialog-footer">
        <el-button type="primary" @click="saveSuccessVisible = false"
          >确定</el-button
        >
      </span>
    </el-dialog>
  </div>
</template>
<script>
import {
  compileissue,
  compileQtemplate,
  compileFollowup,
  getQtemplatelist,
  getFollowuplist,
  getvFollowup,
  getQtemplateobj,
  selectInfoByConditiony,
} from "@/api/AiCentre/index";
import { deptTreeSelect } from "@/api/system/user";
import store from "@/store";
import Pagination from "@/components/Pagination";
export default {
  name: "SatisfactionExceptionConfig",
  components: { Pagination },
  data() {
    return {
      // æ¨¡æ¿è¡¨å•
      templateForm: {
        templateType: "",
        templateId: "",
      },
      templateRules: {
        templateType: [
          { required: true, message: "请选择模板类型", trigger: "change" },
        ],
      },
      // é€‰é¡¹ç®¡ç†ç›¸å…³
      optionDialogVisible: false,
      currentOptions: [],
      editingQuestion: null,
      optionRules: {
        targetvalue: [
          { required: true, message: "请输入选项内容", trigger: "blur" },
        ],
        isabnormal: [
          { required: true, message: "请选择异常状态", trigger: "change" },
        ],
      },
      // å¼‚常状态选项
      abnormalOptions: [
        { label: "正常", value: 0, type: "success" },
        { label: "异常", value: 1, type: "danger" },
        { label: "警告", value: 2, type: "warning" },
      ],
      // æ¨¡æ¿é€‰é¡¹
      questionnaireTemplates: [], // é—®å·æ¨¡æ¿åˆ—表
      followupTemplates: [], // è¯­éŸ³æ¨¡æ¿åˆ—表
      templateOptionsLoading: false,
      // å½“前模板信息
      currentTemplateInfo: null,
      templateLoading: false,
      // æŸ¥è¯¢å‚æ•°
      queryParams: {
        scriptTopic: "",
        scriptContent: "",
      },
      // æ•°æ®åˆ—表
      questionList: [],
      loading: false,
      batchSaving: false,
      // å­—典数据
      askvaluetype: store.getters.askvaluetype || [],
      qyoptions: store.getters.usable || [],
      // ç§‘室选项
      deptOptions: [],
      // é¢„览相关
      previewVisible: false,
      currentPreview: null,
      previewAnswer: "",
      // ä¿å­˜ç›¸å…³
      saveSuccessVisible: false,
      hasChanges: false,
      changedCount: 0,
      // æ»¡æ„åº¦åˆ†ç±»ID
      satisfactionCategoryIds: ["404", "405", "406", "10039", "10041", "10042"],
      questionnaireCategorys: [],
      voiceCategories: [],
      // è¡¨å•验证规则
      configRules: {
        responsibilityDept: [
          {
            required: true,
            message: "请至少选择一个责任科室",
            trigger: "change",
          },
          {
            validator: (rule, value, callback) => {
              if (!value || value.length === 0) {
                callback(new Error("请至少选择一个责任科室"));
              } else {
                callback();
              }
            },
            trigger: "change",
          },
        ],
        reportDept: [
          {
            required: true,
            message: "请至少选择一个报备科室",
            trigger: "change",
          },
          {
            validator: (rule, value, callback) => {
              if (!value || value.length === 0) {
                callback(new Error("请至少选择一个报备科室"));
              } else {
                callback();
              }
            },
            trigger: "change",
          },
        ],
      },
    };
  },
  computed: {
    // æ ¹æ®æ¨¡æ¿ç±»åž‹è¿‡æ»¤æ¨¡æ¿é€‰é¡¹
    filteredTemplateOptions() {
      if (this.templateForm.templateType === 1) {
        return this.questionnaireTemplates;
      } else if (this.templateForm.templateType === 2) {
        return this.followupTemplates;
      }
      return [];
    },
    // æ»¡æ„åº¦é¢˜ç›®æ•°é‡
    satisfactionQuestionsCount() {
      if (this.templateForm.templateType === 1) {
        return this.questionList.filter((q) =>
          this.questionnaireCategorys.includes(q.categoryid)
        ).length;
      } else if (this.templateForm.templateType === 2) {
        return this.questionList.filter((q) =>
          this.voiceCategories.includes(q.scriptAssortid)
        ).length;
      }
    },
    // æ£€æŸ¥é¢˜ç›®æ˜¯å¦æœ‰å¼‚常选项
    hasAbnormalOption(question) {
      return (question) => {
        if (!question) return false;
        // é—®å·æ¨¡æ¿
        if (this.templateForm.templateType === 1) {
          const options = question.svyLibTemplateTargetoptions || [];
          return options.some((opt) => opt.isabnormal === 1);
        }
        // è¯­éŸ³æ¨¡æ¿
        else if (this.templateForm.templateType === 2) {
          const options = question.ivrLibaScriptTargetoptionList || [];
          return options.some((opt) => opt.isabnormal === 1);
        }
        return false;
      };
    },
    // ç­›é€‰åŽçš„题目列表
    filteredQuestionList() {
      let filtered = this.questionList;
      console.log(this.questionnaireCategorys);
      // ç­›é€‰æ»¡æ„åº¦é¢˜ç›®
      if (this.templateForm.templateType === 1) {
        filtered = filtered.filter((q) =>
          this.questionnaireCategorys.includes(q.categoryid)
        );
      } else if (this.templateForm.templateType === 2) {
        filtered = filtered.filter((q) =>
          this.voiceCategories.includes(q.scriptAssortid)
        );
      }
      // åº”用搜索条件
      if (this.queryParams.scriptTopic) {
        const keyword = this.queryParams.scriptTopic.toLowerCase();
        filtered = filtered.filter(
          (q) => q.scriptTopic && q.scriptTopic.toLowerCase().includes(keyword)
        );
      }
      if (this.queryParams.scriptContent) {
        const keyword = this.queryParams.scriptContent.toLowerCase();
        filtered = filtered.filter(
          (q) =>
            q.scriptContent && q.scriptContent.toLowerCase().includes(keyword)
        );
      }
      return filtered;
    },
  },
  created() {
    if (store.getters.satisfactionCategories) {
      this.questionnaireCategorys =
        store.getters.satisfactionCategories.questionnaireCategorys.map(
          (item) => item.categoryid
        );
      this.voiceCategories =
        store.getters.satisfactionCategories.voiceCategories.map(
          (item) => item.categoryid
        );
    }
    this.getDeptOptions();
    this.loadAllTemplates();
  },
  methods: {
    /** åŠ è½½æ‰€æœ‰æ¨¡æ¿åˆ—è¡¨ */
    loadAllTemplates() {
      this.templateOptionsLoading = true;
      // å¹¶è¡ŒåŠ è½½é—®å·æ¨¡æ¿å’Œè¯­éŸ³æ¨¡æ¿
      Promise.all([
        this.loadQuestionnaireTemplates(),
        this.loadFollowupTemplates(),
      ]).finally(() => {
        this.templateOptionsLoading = false;
      });
    },
    /** æŸ¥è¯¢ç§‘室列表 */
    getDeptOptions() {
      deptTreeSelect()
        .then((res) => {
          if (res.code == 200) {
            this.deptOptions = this.flattenArray(res.data) || [];
          }
        })
        .catch((error) => {
          console.error("获取科室列表失败:", error);
          this.$message.error("获取科室列表失败");
        });
    },
    flattenArray(multiArray) {
      let result = [];
      function flatten(element) {
        if (element.children && element.children.length > 0) {
          element.children.forEach((child) => flatten(child));
        } else {
          let item = JSON.parse(JSON.stringify(element));
          result.push(item);
        }
      }
      multiArray.forEach((element) => flatten(element));
      return result;
    },
    /** æ ¹æ®ç§‘室编码获取科室名称 */
    getDeptName(deptCode) {
      if (!deptCode) return "";
      const dept = this.deptOptions.find((d) => d.deptCode === deptCode);
      return dept ? dept.label : deptCode;
    },
    /** æ ¹æ®ç§‘室编码数组获取科室名称数组 */
    getDeptNames(deptCodes) {
      if (!Array.isArray(deptCodes) || deptCodes.length === 0) return [];
      return deptCodes
        .map((code) => this.getDeptName(code))
        .filter((name) => name && name.trim());
    },
    /** æ¨¡æ¿ç±»åž‹å˜æ›´ */
    handleTemplateTypeChange() {
      this.templateForm.templateId = "";
      this.currentTemplateInfo = null;
      this.questionList = [];
    },
    /** åŠ è½½é—®å·æ¨¡æ¿åˆ—è¡¨ */
    loadQuestionnaireTemplates() {
      return new Promise((resolve) => {
        getQtemplatelist({ pageSize: 1000 })
          .then((res) => {
            if (res.code === 200) {
              this.questionnaireTemplates = (res.rows || []).map((item) => ({
                id: item.svyid,
                templateName: item.svyname,
                isavailable: item.isavailable,
              }));
            } else {
              this.$message.error(res.msg || "加载问卷模板失败");
            }
            resolve();
          })
          .catch((error) => {
            console.error("加载问卷模板失败:", error);
            this.$message.error("加载问卷模板失败");
            resolve();
          });
      });
    },
    /** åŠ è½½è¯­éŸ³æ¨¡æ¿åˆ—è¡¨ */
    loadFollowupTemplates() {
      return new Promise((resolve) => {
        getFollowuplist({ pageSize: 1000 })
          .then((res) => {
            if (res.code === 200) {
              this.followupTemplates = (res.rows || []).map((item) => ({
                id: item.id,
                templateName: item.templateName,
                isavailable: item.isavailable,
              }));
            } else {
              this.$message.error(res.msg || "加载语音模板失败");
            }
            resolve();
          })
          .catch((error) => {
            console.error("加载语音模板失败:", error);
            this.$message.error("加载语音模板失败");
            resolve();
          });
      });
    },
    /** æ¨¡æ¿é€‰æ‹©å˜æ›´ */
    handleTemplateChange(templateId) {
      if (templateId) {
        const selectedTemplate = this.filteredTemplateOptions.find(
          (t) => t.id === templateId
        );
        if (selectedTemplate) {
          this.currentTemplateInfo = {
            templateName: selectedTemplate.templateName,
            templateStatus: selectedTemplate.isavailable,
            questionCount: 0,
          };
        }
        // åŠ è½½æ¨¡æ¿è¯¦æƒ…æ•°æ®
        this.templateLoading = true;
        this.loading = true;
        this.questionList = [];
        if (this.templateForm.templateType === 1) {
          this.loadQuestionnaireTemplateDetail();
        } else if (this.templateForm.templateType === 2) {
          this.loadFollowupTemplateDetail();
        }
      } else {
        this.currentTemplateInfo = null;
        this.questionList = [];
      }
    },
    /** åŠ è½½æ¨¡æ¿è¯¦æƒ…å’Œé¢˜ç›® */
    handleLoadTemplate() {
      this.$refs.templateForm.validate((valid) => {
        if (!valid) {
          this.$message.warning("请先选择模板");
          return;
        }
        this.templateLoading = true;
        this.loading = true;
        this.questionList = [];
        if (this.templateForm.templateType === 1) {
          this.loadQuestionnaireTemplateDetail();
        } else if (this.templateForm.templateType === 2) {
          this.loadFollowupTemplateDetail();
        }
      });
    },
    /** åŠ è½½é—®å·æ¨¡æ¿è¯¦æƒ… */
    loadQuestionnaireTemplateDetail() {
      getQtemplateobj({ svyid: this.templateForm.templateId })
        .then((res) => {
          this.templateLoading = false;
          this.loading = false;
          if (res.code === 200 && res.rows && res.rows.length > 0) {
            const templateDetail = res.rows[0];
            // æ›´æ–°æ¨¡æ¿ä¿¡æ¯
            this.currentTemplateInfo = {
              ...templateDetail,
              templateName: templateDetail.svyname,
              templateStatus: templateDetail.isavailable,
              questionCount: templateDetail.svyTemplateLibScripts?.length || 0,
            };
            // æå–题目列表
            const questions = templateDetail.svyTemplateLibScripts || [];
            this.processQuestions(questions);
            this.$message.success(`成功加载 ${questions.length} ä¸ªé¢˜ç›®`);
          } else {
            this.$message.error(res.msg || "加载模板详情失败");
          }
        })
        .catch((error) => {
          this.templateLoading = false;
          this.loading = false;
          console.error("加载问卷模板详情失败:", error);
          this.$message.error("加载模板详情失败");
        });
    },
    /** åŠ è½½è¯­éŸ³æ¨¡æ¿è¯¦æƒ… */
    loadFollowupTemplateDetail() {
      getvFollowup({ id: this.templateForm.templateId })
        .then((res) => {
          this.templateLoading = false;
          this.loading = false;
          if (res.code === 200) {
            const templateDetail = res.data;
            // æ›´æ–°æ¨¡æ¿ä¿¡æ¯
            this.currentTemplateInfo = {
              ...this.currentTemplateInfo,
              templateName: templateDetail.templateName,
              templateStatus: templateDetail.isavailable,
              questionCount:
                templateDetail.ivrLibaTemplateScriptVOList?.length || 0,
            };
            // æå–题目列表
            const questions = templateDetail.ivrLibaTemplateScriptVOList || [];
            this.processQuestions(questions);
            this.$message.success(`成功加载 ${questions.length} ä¸ªé¢˜ç›®`);
          } else {
            this.$message.error(res.msg || "加载模板详情失败");
          }
        })
        .catch((error) => {
          this.templateLoading = false;
          this.loading = false;
          console.error("加载语音模板详情失败:", error);
          this.$message.error("加载模板详情失败");
        });
    },
    /** å¤„理题目数据 */
    processQuestions(questions) {
      this.questionList = questions.map((question) => {
        // è§£æžè´£ä»»ç§‘室和报备科室
        let exceptionConfig = {
          responsibilityDept: [], // è´£ä»»ç§‘室编码数组
          reportDept: [], // æŠ¥å¤‡ç§‘室编码数组
        };
        // ä»Žé¢˜ç›®é¡¶å±‚字段读取数据
        if (question.dutyDeptCode) {
          // ä»Žé€—号分隔的字符串转为数组
          exceptionConfig.responsibilityDept = question.dutyDeptCode
            .split(",")
            .map((code) => code.trim())
            .filter((code) => code);
        }
        if (question.reportDeptCode) {
          exceptionConfig.reportDept = question.reportDeptCode
            .split(",")
            .map((code) => code.trim())
            .filter((code) => code);
        }
        return {
          ...question,
          // ç»Ÿä¸€å­—段名
          id: question.id || question.scriptId,
          scriptTopic: question.scriptTopic || question.scriptTopic,
          scriptContent: question.scriptContent || question.scriptContent,
          scriptType: question.scriptType,
          isavailable: question.isavailable,
          targetname: question.targetname,
          categoryid: question.categoryid || question.categoryid,
          originalConfig: JSON.parse(JSON.stringify(exceptionConfig)),
          exceptionConfig: exceptionConfig,
          hasChanges: false,
          saving: false,
          saveStatus: null,
        };
      });
      this.updateChangedStatus();
    },
    /** é‡ç½®æ¨¡æ¿é€‰æ‹© */
    handleResetTemplate() {
      this.templateForm = {
        templateType: "",
        templateId: "",
      };
      this.currentTemplateInfo = null;
      this.questionList = [];
      this.resetQuery();
      this.$refs.templateForm?.clearValidate();
    },
    /** é…ç½®å˜æ›´å¤„理 */
    handleConfigChange(question) {
      this.$nextTick(() => {
        const index = this.filteredQuestionList.findIndex(
          (q) => q.id === question.id
        );
        if (index !== -1) {
          const formRef = this.$refs.configForm && this.$refs.configForm[index];
          if (formRef) {
            formRef.validate((valid) => {
              if (valid) {
                question.hasChanges = !this.isConfigEqual(
                  question.exceptionConfig,
                  question.originalConfig
                );
                this.updateChangedStatus();
              }
            });
          }
        }
      });
    },
    /** æ¯”较配置是否改变 */
    isConfigEqual(config1, config2) {
      if (!config1 || !config2) return false;
      const responsibility1 = [...(config1.responsibilityDept || [])]
        .sort()
        .join(",")
        .toLowerCase();
      const responsibility2 = [...(config2.responsibilityDept || [])]
        .sort()
        .join(",")
        .toLowerCase();
      const report1 = [...(config1.reportDept || [])]
        .sort()
        .join(",")
        .toLowerCase();
      const report2 = [...(config2.reportDept || [])]
        .sort()
        .join(",")
        .toLowerCase();
      return responsibility1 === responsibility2 && report1 === report2;
    },
    /** æ›´æ–°å˜æ›´çŠ¶æ€ */
    updateChangedStatus() {
      const changedItems = this.questionList.filter((q) => q.hasChanges);
      this.changedCount = changedItems.length;
      this.hasChanges = changedItems.length > 0;
    },
    /** ä¿å­˜å•个题目配置 */
    async saveSingleConfig(question) {
      if (!question.hasChanges) return;
      const index = this.filteredQuestionList.findIndex(
        (q) => q.id === question.id
      );
      console.log(index, "filteredQuestionList");
      if (index === -1) return;
      const formRef = this.$refs.configForm && this.$refs.configForm[index];
      if (!formRef) return;
      const valid = await formRef.validate();
      if (!valid) {
        this.$message.warning("请先完成必填项");
        return;
      }
      question.saving = true;
      question.saveStatus = null;
      try {
        // èŽ·å–å½“å‰æ¨¡æ¿è¯¦æƒ…
        let templateDetail;
        if (this.templateForm.templateType === 1) {
          // é—®å·æ¨¡æ¿
          const res = await getQtemplateobj({
            svyid: this.templateForm.templateId,
          });
          if (res.code !== 200 || !res.rows || res.rows.length === 0) {
            throw new Error(res.msg || "获取模板详情失败");
          }
          templateDetail = res.rows[0];
        } else if (this.templateForm.templateType === 2) {
          // è¯­éŸ³æ¨¡æ¿
          const res = await getvFollowup({ id: this.templateForm.templateId });
          if (res.code !== 200) {
            throw new Error(res.msg || "获取模板详情失败");
          }
          templateDetail = res.data;
        }
        // æ›´æ–°é¢˜ç›®é…ç½®
        let updatedTemplateDetail = { ...templateDetail };
        let questionsField =
          this.templateForm.templateType === 1
            ? "svyTemplateLibScripts"
            : "ivrLibaTemplateScriptVOList";
        const questions = updatedTemplateDetail[questionsField] || [];
        const questionIndex = questions.findIndex((q) => q.id === question.id);
        if (questionIndex === -1) {
          throw new Error("未找到题目");
        }
        // èŽ·å–ç§‘å®¤åç§°
        const responsibilityDeptNames = this.getDeptNames(
          question.exceptionConfig.responsibilityDept
        );
        const reportDeptNames = this.getDeptNames(
          question.exceptionConfig.reportDept
        );
        // ç›´æŽ¥æ›´æ–°é¢˜ç›®é¡¶å±‚字段
        questions[questionIndex] = {
          ...questions[questionIndex],
          // è®¾ç½®Excel要求的字段
          dutyDeptCode: question.exceptionConfig.responsibilityDept.join(","),
          dutyDeptName: responsibilityDeptNames.join(","),
          reportDeptCode: question.exceptionConfig.reportDept.join(","),
          reportDeptName: reportDeptNames.join(","),
        };
        // æ›´æ–°æ¨¡æ¿
        updatedTemplateDetail[questionsField] = questions;
        // ä¿å­˜æ¨¡æ¿
        let response;
        if (this.templateForm.templateType === 1) {
          response = await compileQtemplate({
            ...updatedTemplateDetail,
            id: this.templateForm.templateId,
            isoperation: 2,
          });
        } else {
          response = await compileFollowup({
            ...updatedTemplateDetail,
            id: this.templateForm.templateId,
            isoperation: 2,
          });
        }
        if (response.code === 200) {
          this.handleSaveSuccess(question);
        } else {
          throw new Error(response.msg || "保存失败");
        }
      } catch (error) {
        console.error("保存失败:", error);
        question.saveStatus = {
          type: "error",
          message: error.message || "保存失败,请稍后重试",
        };
        this.$message.error(error.message || "保存失败,请稍后重试");
      } finally {
        question.saving = false;
      }
    },
    /** å¤„理保存成功 */
    /** å¤„理保存成功 */
    handleSaveSuccess(question) {
      // åŒæ—¶æ›´æ–°é¢˜ç›®é¡¶å±‚字段
      const responsibilityDeptNames = this.getDeptNames(
        question.exceptionConfig.responsibilityDept
      );
      const reportDeptNames = this.getDeptNames(
        question.exceptionConfig.reportDept
      );
      // æ›´æ–°é¢˜ç›®æœ¬èº«çš„字段
      question.dutyDeptCode =
        question.exceptionConfig.responsibilityDept.join(",");
      question.dutyDeptName = responsibilityDeptNames.join(",");
      question.reportDeptCode = question.exceptionConfig.reportDept.join(",");
      question.reportDeptName = reportDeptNames.join(",");
      // æ›´æ–°åŽŸå§‹é…ç½®
      question.originalConfig = JSON.parse(
        JSON.stringify(question.exceptionConfig)
      );
      question.hasChanges = false;
      question.saveStatus = {
        type: "success",
        message: "配置保存成功",
      };
      this.updateChangedStatus();
      this.$message.success("配置保存成功");
      // 5秒后清除成功提示
      setTimeout(() => {
        question.saveStatus = null;
      }, 5000);
    },
    /** é‡ç½®å•个题目配置 */
    resetSingleConfig(question) {
      this.$confirm("确定要重置当前题目的配置吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          question.exceptionConfig = JSON.parse(
            JSON.stringify(question.originalConfig)
          );
          // åŒæ—¶é‡ç½®é¢˜ç›®é¡¶å±‚字段
          const responsibilityDeptNames = this.getDeptNames(
            question.exceptionConfig.responsibilityDept
          );
          const reportDeptNames = this.getDeptNames(
            question.exceptionConfig.reportDept
          );
          question.dutyDeptCode =
            question.exceptionConfig.responsibilityDept.join(",");
          question.dutyDeptName = responsibilityDeptNames.join(",");
          question.reportDeptCode =
            question.exceptionConfig.reportDept.join(",");
          question.reportDeptName = reportDeptNames.join(",");
          question.hasChanges = false;
          question.saveStatus = null;
          this.updateChangedStatus();
          this.$message.success("配置已重置");
        })
        .catch(() => {});
    },
    /** æ‰¹é‡ä¿å­˜é…ç½® */
    async handleBatchSave() {
      if (!this.hasChanges || this.batchSaving) return;
      this.$confirm("确定要保存所有修改过的配置吗?", "批量保存", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(async () => {
          this.batchSaving = true;
          const changedQuestions = this.questionList.filter(
            (q) => q.hasChanges
          );
          const results = [];
          for (const question of changedQuestions) {
            try {
              await this.saveSingleConfig(question);
              results.push({
                id: question.id,
                success:
                  !question.hasChanges &&
                  question.saveStatus?.type === "success",
              });
            } catch (error) {
              results.push({
                id: question.id,
                success: false,
              });
            }
          }
          this.batchSaving = false;
          const successCount = results.filter((r) => r.success).length;
          const failCount = results.length - successCount;
          if (failCount === 0) {
            this.saveSuccessVisible = true;
            this.$message.success(`成功保存 ${successCount} ä¸ªé…ç½®`);
          } else {
            this.$message.warning(
              `成功保存 ${successCount} ä¸ªï¼Œå¤±è´¥ ${failCount} ä¸ª`
            );
          }
        })
        .catch(() => {
          this.batchSaving = false;
        });
    },
    /** é¢„览题目 */
    previewQuestion(question) {
      this.currentPreview = { ...question };
      this.previewAnswer = "";
      this.previewVisible = true;
    },
    /** æ£€æŸ¥é¢˜ç›®æ˜¯å¦æœ‰å¼‚常选项 */
    checkHasAbnormalOptions(question) {
      if (this.templateForm.templateType === 1) {
        return (question.svyLibTemplateTargetoptions || []).some(
          (opt) => opt.isabnormal === 1
        );
      } else if (this.templateForm.templateType === 2) {
        return (question.ivrLibaScriptTargetoptionList || []).some(
          (opt) => opt.isabnormal === 1
        );
      }
      return false;
    },
    /** æ‰“开选项管理对话框 */
    openOptionDialog(question) {
      this.editingQuestion = question;
      // å¤åˆ¶é€‰é¡¹æ•°æ®
      if (this.templateForm.templateType === 1) {
        this.currentOptions = JSON.parse(
          JSON.stringify(question.svyLibTemplateTargetoptions || [])
        ).map((opt) => ({
          ...opt,
          id: opt.id,
          targetvalue: opt.optioncontent || "",
          isabnormal: opt.isabnormal || 0,
        }));
      } else if (this.templateForm.templateType === 2) {
        this.currentOptions = JSON.parse(
          JSON.stringify(question.ivrLibaScriptTargetoptionList || [])
        ).map((opt) => ({
          ...opt,
          targetvalue: opt.targetvalue || "",
          isabnormal: opt.isabnormal || 0,
        }));
      }
      this.optionDialogVisible = true;
    },
    /** æ·»åŠ æ–°é€‰é¡¹ */
    addNewOption() {
      this.currentOptions.push({
        id: Date.now(), // ä¸´æ—¶ID
        targetvalue: "",
        isabnormal: 0,
        isNew: true,
      });
    },
    /** åˆ é™¤é€‰é¡¹ */
    removeOption(index) {
      this.currentOptions.splice(index, 1);
    },
    /** ä¿å­˜é€‰é¡¹é…ç½® */
    async saveOptions() {
      try {
        // éªŒè¯å¿…填项
        for (const option of this.currentOptions) {
          if (!option.targetvalue || option.targetvalue.trim() === "") {
            this.$message.warning("请填写所有选项内容");
            return;
          }
        }
        // æ£€æŸ¥æ˜¯å¦æœ‰å¼‚常选项
        const hasAbnormal = this.currentOptions.some(
          (opt) => opt.isabnormal === 1
        );
        if (!hasAbnormal) {
          this.$message.warning("请至少设置一个异常选项(isabnormal=1)");
          return;
        }
        // ä¿å­˜é€»è¾‘ - æ›´æ–°é¢˜ç›®å¯¹è±¡çš„选项数据
        if (this.templateForm.templateType === 1) {
          this.editingQuestion.svyLibTemplateTargetoptions =
            this.currentOptions.map((opt) => ({
              ...opt,
              optioncontent: opt.targetvalue,
              isabnormal: opt.isabnormal,
            }));
        } else if (this.templateForm.templateType === 2) {
          this.editingQuestion.ivrLibaScriptTargetoptionList =
            this.currentOptions;
        }
        // è§¦å‘配置变更检查
        this.handleConfigChange(this.editingQuestion);
        this.$message.success("选项配置保存成功");
        this.optionDialogVisible = false;
      } catch (error) {
        console.error("保存选项失败:", error);
        this.$message.error("保存选项失败");
      }
    },
    /** ä¿®æ”¹ä¿å­˜å•个题目配置方法,添加异常选项检查 */
    async saveSingleConfig(question) {
      // æ£€æŸ¥æ˜¯å¦æœ‰å¼‚常选项
      if (!this.checkHasAbnormalOptions(question)) {
        this.$confirm("该题目没有设置异常选项,是否先配置选项?", "提示", {
          confirmButtonText: "去配置",
          cancelButtonText: "取消",
          type: "warning",
        })
          .then(() => {
            this.openOptionDialog(question);
          })
          .catch(() => {});
        return;
      }
      // åŽŸæœ‰çš„ä¿å­˜é€»è¾‘...
      if (!question.hasChanges) return;
      const index = this.filteredQuestionList.findIndex(
        (q) => q.id === question.id
      );
      if (index === -1) return;
      const formRef = this.$refs.configForm && this.$refs.configForm[index];
      if (!formRef) return;
      const valid = await formRef.validate();
      if (!valid) {
        this.$message.warning("请先完成必填项");
        return;
      }
      // ç»§ç»­åŽŸæœ‰çš„ä¿å­˜é€»è¾‘...
      question.saving = true;
      question.saveStatus = null;
      try {
        // ... åŽŸæœ‰çš„ä¿å­˜é€»è¾‘ä¸å˜
      } catch (error) {
        // ... é”™è¯¯å¤„理不变
      } finally {
        question.saving = false;
      }
    },
    /** æ‰¹é‡ä¿å­˜æ—¶ä¹Ÿè¦æ£€æŸ¥ */
    async handleBatchSave() {
      if (!this.hasChanges || this.batchSaving) return;
      // æ£€æŸ¥æ‰€æœ‰æœ‰å˜æ›´çš„题目是否都有异常选项
      const changedQuestions = this.questionList.filter((q) => q.hasChanges);
      const questionsWithoutAbnormal = changedQuestions.filter(
        (q) => !this.checkHasAbnormalOptions(q)
      );
      if (questionsWithoutAbnormal.length > 0) {
        this.$confirm(
          `有 ${questionsWithoutAbnormal.length} ä¸ªé¢˜ç›®æ²¡æœ‰è®¾ç½®å¼‚常选项,请先配置选项。是否继续?`,
          "提示",
          {
            confirmButtonText: "ç»§ç»­",
            cancelButtonText: "去配置",
            type: "warning",
          }
        )
          .then(() => {
            // ç»§ç»­æ‰§è¡Œæ‰¹é‡ä¿å­˜
            this.executeBatchSave(changedQuestions);
          })
          .catch(() => {
            // å¯ä»¥åœ¨è¿™é‡Œè·³è½¬åˆ°ç¬¬ä¸€ä¸ªæ²¡æœ‰å¼‚常选项的题目
            if (questionsWithoutAbnormal.length > 0) {
              this.openOptionDialog(questionsWithoutAbnormal[0]);
            }
          });
      } else {
        this.executeBatchSave(changedQuestions);
      }
    },
    /** æ‰§è¡Œæ‰¹é‡ä¿å­˜ */
    async executeBatchSave(changedQuestions) {
      this.$confirm("确定要保存所有修改过的配置吗?", "批量保存", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(async () => {
          this.batchSaving = true;
          const results = [];
          for (const question of changedQuestions) {
            try {
              // è¿™é‡Œè°ƒç”¨ä¿®æ”¹åŽçš„saveSingleConfig方法
              await this.saveSingleConfig(question);
              results.push({
                id: question.id,
                success:
                  !question.hasChanges &&
                  question.saveStatus?.type === "success",
              });
            } catch (error) {
              results.push({
                id: question.id,
                success: false,
              });
            }
          }
          this.batchSaving = false;
          // ... åŽç»­å¤„理不变
        })
        .catch(() => {
          this.batchSaving = false;
        });
    },
    /** èŽ·å–å¼‚å¸¸é€‰é¡¹ç»Ÿè®¡ */
    getAbnormalStats(question) {
      if (this.templateForm.templateType === 1) {
        const options = question.svyLibTemplateTargetoptions || [];
        return {
          total: options.length,
          abnormal: options.filter((opt) => opt.isabnormal === 1).length,
          warning: options.filter((opt) => opt.isabnormal === 2).length,
          normal: options.filter((opt) => opt.isabnormal === 0).length,
        };
      } else if (this.templateForm.templateType === 2) {
        const options = question.ivrLibaScriptTargetoptionList || [];
        return {
          total: options.length,
          abnormal: options.filter((opt) => opt.isabnormal === 1).length,
          warning: options.filter((opt) => opt.isabnormal === 2).length,
          normal: options.filter((opt) => opt.isabnormal === 0).length,
        };
      }
      return { total: 0, abnormal: 0, warning: 0, normal: 0 };
    },
    /** æœç´¢ */
    handleQuery() {
      // ä»…筛选显示,不需要重新加载
    },
    /** é‡ç½®æœç´¢ */
    resetQuery() {
      this.queryParams = {
        scriptTopic: "",
        scriptContent: "",
      };
    },
  },
};
</script>
<style lang="scss" scoped>
.satisfaction-exception-config {
  min-height: 100%;
  background-color: #f5f7fa;
  padding: 20px;
  .page-header {
    margin-bottom: 20px;
    padding: 20px;
    background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
    border-radius: 8px;
    color: white;
    .header-content {
      .page-title {
        margin: 0 0 8px 0;
        font-size: 20px;
        font-weight: 600;
      }
      .page-description {
        margin: 0;
        opacity: 0.9;
        font-size: 14px;
      }
    }
  }
  .template-section {
    margin-bottom: 20px;
    .template-header {
      margin-bottom: 20px;
      .template-title {
        margin: 0 0 8px 0;
        font-size: 16px;
        font-weight: 600;
        color: #303133;
      }
      .template-tip {
        margin: 0;
        color: #909399;
        font-size: 13px;
      }
    }
    .select-loading {
      text-align: center;
      padding: 10px;
      color: #909399;
      i {
        margin-right: 8px;
      }
    }
  }
  .template-info-section {
    margin-bottom: 20px;
    .template-info {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 5px 0;
      .info-left {
        .template-name {
          margin: 0 0 10px 0;
          font-size: 18px;
          font-weight: 600;
          color: #303133;
        }
        .template-meta {
          display: flex;
          gap: 20px;
          flex-wrap: wrap;
          .meta-item {
            display: flex;
            align-items: center;
            gap: 5px;
            font-size: 13px;
            color: #606266;
            i {
              font-size: 14px;
            }
          }
        }
      }
    }
  }
  .search-section {
    margin-bottom: 20px;
    .search-container {
      border-radius: 8px;
      .el-form {
        display: flex;
        flex-wrap: wrap;
        gap: 16px;
        align-items: center;
      }
    }
  }
  .config-content {
    .batch-actions-card {
      margin-bottom: 20px;
      .batch-actions {
        display: flex;
        align-items: center;
        gap: 20px;
        padding: 8px 0;
        .change-count {
          color: #e6a23c;
          font-size: 14px;
          font-weight: 500;
        }
        .total-count {
          margin-left: auto;
          color: #909399;
          font-size: 14px;
        }
      }
    }
    .loading-wrapper {
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 400px;
      .loading-spinner {
        text-align: center;
        color: #409eff;
        i {
          font-size: 24px;
          margin-right: 8px;
        }
        span {
          font-size: 16px;
        }
      }
    }
    .empty-wrapper {
      min-height: 400px;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      .empty-tip {
        margin-top: 10px;
        color: #909399;
        font-size: 13px;
        text-align: center;
      }
    }
    .question-list {
      display: flex;
      flex-direction: column;
      gap: 16px;
    }
    .question-item {
      .question-card {
        border-radius: 8px;
        border: 1px solid #ebeef5;
        transition: all 0.3s ease;
        &.has-changes {
          border-color: #409eff;
          box-shadow: 0 2px 12px 0 rgba(64, 158, 255, 0.1);
        }
        &:hover {
          box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
        }
        .card-header {
          display: flex;
          justify-content: space-between;
          align-items: flex-start;
          margin-bottom: 20px;
          padding-bottom: 20px;
          border-bottom: 1px solid #f0f0f0;
          .header-left {
            display: flex;
            gap: 20px;
            flex: 1;
            .question-index {
              display: flex;
              flex-direction: column;
              align-items: center;
              min-width: 40px;
              .index-number {
                display: flex;
                align-items: center;
                justify-content: center;
                width: 32px;
                height: 32px;
                background: #409eff;
                color: white;
                border-radius: 50%;
                font-size: 14px;
                font-weight: 600;
                margin-bottom: 8px;
              }
              .index-line {
                width: 2px;
                height: 100%;
                background: #e0e0e0;
                border-radius: 1px;
              }
            }
            .question-basic-info {
              flex: 1;
              .question-title-section {
                margin-bottom: 12px;
                .question-topic {
                  margin: 0 0 8px 0;
                  font-size: 16px;
                  font-weight: 600;
                  color: #303133;
                  line-height: 1.4;
                }
                .question-tags {
                  display: flex;
                  gap: 8px;
                  flex-wrap: wrap;
                }
              }
              .question-content-section {
                display: flex;
                align-items: flex-start;
                gap: 8px;
                .content-label {
                  color: #606266;
                  font-size: 13px;
                  font-weight: 500;
                  min-width: 80px;
                }
                .content-text {
                  color: #303133;
                  font-size: 13px;
                  line-height: 1.6;
                  flex: 1;
                }
              }
            }
          }
          .header-right {
            display: flex;
            flex-direction: column;
            align-items: flex-end;
            gap: 8px;
            .option-status {
              .status-tag {
                cursor: default;
                i {
                  margin-right: 4px;
                }
              }
            }
          }
        }
        .config-section {
          .config-title {
            display: flex;
            align-items: center;
            gap: 8px;
            margin-bottom: 20px;
            padding: 8px 12px;
            background: #f8f9fa;
            border-radius: 4px;
            i {
              color: #409eff;
              font-size: 16px;
            }
            span {
              color: #303133;
              font-weight: 600;
              font-size: 14px;
            }
          }
          .config-form {
            .config-fields {
              display: grid;
              grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
              gap: 20px;
              margin-bottom: 20px;
              .config-field {
                .config-item {
                  margin-bottom: 0;
                  :deep(.el-form-item__label) {
                    font-weight: 500;
                    color: #606266;
                    padding-right: 12px;
                  }
                  .config-tip {
                    font-size: 12px;
                    color: #909399;
                    margin-top: 4px;
                    line-height: 1.4;
                  }
                }
              }
            }
            .current-config {
              margin-bottom: 20px;
              padding: 15px;
              background: #f0f9ff;
              border-radius: 6px;
              border: 1px solid #d0ebff;
              .config-preview {
                .preview-item {
                  display: flex;
                  align-items: flex-start;
                  margin-bottom: 8px;
                  &:last-child {
                    margin-bottom: 0;
                  }
                  .preview-label {
                    font-size: 13px;
                    color: #606266;
                    font-weight: 500;
                    min-width: 80px;
                  }
                  .preview-value {
                    font-size: 13px;
                    color: #303133;
                    line-height: 1.5;
                    flex: 1;
                  }
                }
              }
            }
            .config-footer {
              display: flex;
              justify-content: space-between;
              align-items: center;
              padding-top: 20px;
              border-top: 1px dashed #dcdfe6;
              .save-status {
                flex: 1;
                margin-right: 20px;
                .el-alert {
                  padding: 8px 16px;
                  border-radius: 4px;
                }
              }
              .config-actions {
                display: flex;
                align-items: center;
                gap: 12px;
                flex-shrink: 0;
                .el-button {
                  min-width: 100px;
                }
              }
            }
          }
        }
      }
    }
  }
  .preview-wrapper {
    .preview-header {
      margin-bottom: 20px;
      h4 {
        margin: 0 0 12px 0;
        color: #303133;
        font-size: 18px;
        font-weight: 600;
      }
      .preview-tags {
        display: flex;
        gap: 8px;
        flex-wrap: wrap;
      }
    }
    .preview-content {
      .preview-question {
        margin-bottom: 20px;
        padding: 16px;
        background: #f8f9fa;
        border-radius: 4px;
        color: #606266;
        line-height: 1.6;
      }
      .preview-options {
        .option-item {
          display: block;
          margin-bottom: 12px;
          padding: 12px;
          border-radius: 4px;
          border: 1px solid #ebeef5;
          transition: all 0.3s;
          &:hover {
            background: #f5f7fa;
            border-color: #409eff;
          }
          &:last-child {
            margin-bottom: 0;
          }
        }
      }
      .preview-textarea {
        .el-textarea__inner {
          resize: none;
        }
      }
    }
  }
  .success-content {
    text-align: center;
    padding: 20px 0;
    .success-icon {
      color: #67c23a;
      font-size: 48px;
      margin-bottom: 20px;
    }
    .success-text {
      font-size: 16px;
      color: #606266;
      margin: 0;
    }
  }
}
.option-config-wrapper {
  .dialog-header {
    margin-bottom: 20px;
    padding-bottom: 15px;
    border-bottom: 1px solid #ebeef5;
    h4 {
      margin: 0 0 8px 0;
      color: #303133;
      font-size: 16px;
      font-weight: 600;
    }
    .dialog-subtitle {
      margin: 0;
      color: #606266;
      font-size: 13px;
      line-height: 1.4;
    }
  }
  .option-list {
    .option-item {
      margin-bottom: 12px;
      padding: 12px;
      background: #f8f9fa;
      border-radius: 4px;
      border: 1px solid #ebeef5;
      &:hover {
        border-color: #dcdfe6;
      }
      .option-form {
        .option-index {
          display: flex;
          align-items: center;
          justify-content: center;
          height: 100%;
          color: #909399;
          font-weight: 500;
        }
      }
    }
  }
}
@media (max-width: 768px) {
  .satisfaction-exception-config {
    padding: 12px;
    .page-header {
      padding: 16px;
      margin-bottom: 16px;
    }
    .template-info {
      flex-direction: column;
      align-items: flex-start;
      gap: 10px;
    }
    .search-card {
      margin-bottom: 16px;
    }
    .config-content {
      .batch-actions-card {
        margin-bottom: 16px;
      }
      .question-item {
        .question-card {
          .card-header {
            flex-direction: column;
            gap: 12px;
            .header-left {
              flex-direction: column;
              gap: 12px;
              .question-index {
                flex-direction: row;
                align-items: center;
                min-width: auto;
                .index-number {
                  margin-bottom: 0;
                  margin-right: 12px;
                }
                .index-line {
                  width: 100%;
                  height: 2px;
                }
              }
            }
          }
          .config-section {
            .config-form {
              .config-fields {
                grid-template-columns: 1fr;
                gap: 16px;
              }
              .current-config {
                padding: 12px;
              }
              .config-footer {
                flex-direction: column;
                align-items: stretch;
                gap: 12px;
                .save-status {
                  margin-right: 0;
                  margin-bottom: 8px;
                }
              }
            }
          }
        }
      }
    }
  }
}
</style>
src/views/Satisfaction/particulars/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,889 @@
<template>
  <div class="Questionnairemanagement">
    <!-- ä¸Šä¾§æ  -->
    <div class="sidecolumn">
      <div>
        <el-steps simple :active="Editprogress">
          <el-step
            icon="el-icon-edit"
            title="基础信息"
            description="选择宣教模板、形式等基础信息"
          ></el-step>
          <el-step
            icon="el-icon-user"
            title="宣教对象"
            description="在本部选择宣教病人"
          ></el-step>
        </el-steps>
      </div>
    </div>
    <!-- ä¸‹ä¾§æ•°æ® -->
    <div class="leftvlue" style="margin: 0 20px">
      <!-- åŸºæœ¬ä¿¡æ¯ -->
      <div v-if="Editprogress == 1">
        <el-alert
          title="选择宣教模板、形式等基础信息"
          type="success"
          effect="dark"
        >
        </el-alert>
        <div class="leftvlue-jbxx">
          <!-- åŸºç¡€ä¿¡æ¯ -->
          <div class="examine-jic">
            <div class="headline">
              <div>基础信息</div>
            </div>
            <div class="jic-value">
              <el-form ref="form" :model="form" label-width="105px">
                <el-form-item label="发送时间:" v-if="currenttype != 2">
                  <el-date-picker
                    v-model="form.name"
                    type="date"
                    placeholder="选择日期"
                  >
                  </el-date-picker>
                </el-form-item>
                <el-form-item label="发送时间段:" v-if="currenttype != 2">
                  <el-checkbox-group v-model="checkList">
                    <el-checkbox label="上午(8:30-11:30)"></el-checkbox>
                    <el-checkbox label="下午(14:30-16:30)"></el-checkbox>
                    <el-checkbox label="夜间(18:30-20:30)"></el-checkbox>
                  </el-checkbox-group>
                </el-form-item>
                <el-form-item label="服务形式">
                  <el-checkbox-group v-model="checkList">
                    <el-checkbox
                      v-for="(item, index) in checkboxlist"
                      :key="index"
                      :label="item"
                    ></el-checkbox>
                  </el-checkbox-group>
                </el-form-item>
                <el-form-item label="组织形式">
                  <el-radio-group v-model="form.radio">
                    <el-radio :label="3">单人</el-radio>
                    <el-radio :label="6">多人</el-radio>
                  </el-radio-group>
                </el-form-item>
                <el-form-item label="语音模板" prop="region">
                  <el-select v-model="form.region" placeholder="请选择模板">
                    <el-option label="一号模板" value="shanghai"></el-option>
                    <el-option label="二号模板" value="beijing"></el-option>
                  </el-select>
                </el-form-item>
              </el-form>
            </div>
          </div>
          <div class="examine-jic">
            <div class="headline">
              <div>{{ title }}</div>
            </div>
            <div class="examine-jic">
              <div class="jic-value">
                <el-row :gutter="20">
                  <!--用户数据-->
                  <el-form
                    :model="topqueryParams"
                    ref="queryForm"
                    size="small"
                    :inline="true"
                    v-show="showSearch"
                    label-width="98px"
                  >
                    <el-form-item label="执行状态" prop="status">
                      <el-select
                        v-model="topqueryParams.topic"
                        placeholder="请选择"
                      >
                        <el-option
                          v-for="item in taskoptions"
                          :key="item.value"
                          :label="item.label"
                          :value="item.value"
                        >
                        </el-option>
                      </el-select>
                    </el-form-item>
                    <el-form-item
                      label="科室名称"
                      v-if="currenttype == 1 || currenttype == 3"
                    >
                      <el-input
                        v-model="topqueryParams.name"
                      ></el-input> </el-form-item
                    ><el-form-item label="病区名称" v-if="currenttype == 2">
                      <el-input v-model="topqueryParams.name"></el-input>
                    </el-form-item>
                    <el-form-item
                      label="患者姓名"
                      v-if="currenttype == 1 || currenttype == 2"
                    >
                      <el-input v-model="topqueryParams.name"></el-input>
                    </el-form-item>
                    <el-form-item
                      label="主治医生"
                      v-if="currenttype == 1 || currenttype == 2"
                    >
                      <el-input v-model="topqueryParams.name"></el-input>
                    </el-form-item>
                    <el-form-item
                      label="管床护士"
                      v-if="currenttype == 1 || currenttype == 2"
                    >
                      <el-input v-model="topqueryParams.name"></el-input>
                    </el-form-item>
                    <el-form-item>
                      <el-button
                        type="primary"
                        icon="el-icon-search"
                        size="medium"
                        @click="handleQuery"
                        >搜索</el-button
                      >
                      <el-button
                        icon="el-icon-refresh"
                        size="medium"
                        @click="resetQuery"
                        >重置</el-button
                      >
                    </el-form-item>
                  </el-form>
                  <el-divider></el-divider>
                  <!-- é€‰æ‹©ä»»åŠ¡åˆ—è¡¨ -->
                  <SFtable
                    @handleUpdate="handleUpdate"
                    @handleSelectionChange="handleSelectionChange"
                    :currentList="userList"
                    :tableLabel="tableLabelxj"
                    :controlsc="false"
                    :multiplechoice="false"
                  />
                  <pagination
                    v-show="total > 0"
                    :total="total"
                    :page.sync="topqueryParams.pageNum"
                    :limit.sync="topqueryParams.pageSize"
                    @pagination="getList"
                  />
                </el-row>
              </div>
            </div>
          </div>
        </div>
        <el-button type="success" @click="submitForm('ruleForm')">{{
          quote ? "立即创建" : "任务详情设置"
        }}</el-button>
        <el-button @click="resetForm('ruleForm')">重置</el-button>
      </div>
      <!-- ä»»åŠ¡è¯¦æƒ… -->
      <div v-if="Editprogress == 2">
        <el-alert title="在本阶段选择病人" type="success" effect="dark">
        </el-alert>
        <div class="leftvlue-jbxx">
          <div class="examine-jic">
            <div class="headline">
              <div>患者列表</div>
            </div>
            <div class="examine-jic">
              <div class="jic-value">
                <el-row :gutter="20">
                  <!--用户数据-->
                  <el-form
                    :model="topqueryParams"
                    ref="queryForm"
                    size="small"
                    :inline="true"
                    v-show="showSearch"
                    label-width="98px"
                  >
                    <el-form-item label="患者名称">
                      <el-input v-model="topqueryParams.name"></el-input>
                    </el-form-item>
                    <el-form-item label="患者范围" prop="status">
                      <el-select
                        v-model="topqueryParams.searchscope"
                        placeholder="请选择"
                      >
                        <el-option
                          v-for="item in source"
                          :key="item.value"
                          :label="item.label"
                          :value="item.value"
                        >
                        </el-option>
                      </el-select>
                    </el-form-item>
                    <el-form-item label="患者状态" prop="status">
                      <el-select
                        v-model="topqueryParams.topic"
                        placeholder="请选择"
                      >
                        <el-option
                          v-for="item in topicoptions"
                          :key="item.value"
                          :label="item.label"
                          :value="item.value"
                        >
                        </el-option>
                      </el-select>
                    </el-form-item>
                    <el-form-item label="随访结果" prop="status">
                      <el-select
                        v-model="topqueryParams.topic"
                        placeholder="请选择"
                      >
                        <el-option
                          v-for="item in topicoptions"
                          :key="item.value"
                          :label="item.label"
                          :value="item.value"
                        >
                        </el-option>
                      </el-select>
                    </el-form-item>
                    <el-form-item label="患者电话">
                      <el-input v-model="topqueryParams.name"></el-input>
                    </el-form-item>
                    <el-form-item>
                      <el-button
                        type="primary"
                        icon="el-icon-search"
                        size="medium"
                        @click="handleQuery"
                        >搜索</el-button
                      >
                      <el-button
                        icon="el-icon-refresh"
                        size="medium"
                        @click="resetQuery"
                        >重置</el-button
                      >
                      <el-button
                        icon="el-icon-upload2"
                        size="medium"
                        type="warning"
                        >当前患者一键发送</el-button
                      >
                    </el-form-item>
                  </el-form>
                  <el-divider></el-divider>
                  <el-row :gutter="10" class="mb8">
                    <el-col :span="1.5">
                      <el-select
                        v-model="tasktopic"
                        placeholder="请选择新增类型"
                      >
                        <el-option
                          v-for="item in taskoptions"
                          :key="item.value"
                          :label="item.label"
                          :value="item.value"
                        >
                        </el-option>
                      </el-select>
                    </el-col>
                    <el-col :span="1.5">
                      <el-button
                        type="primary"
                        plain
                        icon="el-icon-plus"
                        size="medium"
                        :disabled="!tasktopic"
                        @click="handleAddpatient"
                        >新增</el-button
                      >
                    </el-col>
                    <el-col :span="1.5">
                      <el-button
                        type="danger"
                        plain
                        icon="el-icon-delete"
                        size="medium"
                        :disabled="multiple"
                        @click="handleDelete"
                        >删除</el-button
                      >
                    </el-col>
                    <!-- <el-col :span="1.5"> </el-col> -->
                  </el-row>
                  <!-- é€‰ä¸­æ‚£è€…列表 -->
                  <SFtable
                    @handleUpdate="handleUpdate"
                    @handleSelectionChange="handleSelectionChange"
                    :currentList="sonuserList"
                    :tableLabel="tableLabelhz"
                    :controlxz="false"
                  />
                  <pagination
                    v-show="total > 0"
                    :total="total"
                    :page.sync="topqueryParams.pageNum"
                    :limit.sync="topqueryParams.pageSize"
                    @pagination="getList"
                  />
                </el-row>
              </div>
            </div>
          </div>
        </div>
        <el-button type="primary" @click="laststep()">上一步</el-button>
        <el-button type="success" @click="submitForm('ruleForm')"
          >立即创建</el-button
        >
        <el-button @click="resetForm('ruleForm')">重置</el-button>
      </div>
    </div>
    <!-- æ·»åŠ æ‚£è€… -->
    <el-dialog
      title="选择患者"
      :visible.sync="dialogVisiblepatient"
      width="70%"
      :before-close="handleClosehz"
    >
      <div class="examine-jic">
        <div class="jic-value">
          <el-row :gutter="20">
            <!--用户数据-->
            <el-form
              :model="patientqueryParams"
              ref="queryForm"
              size="small"
              :inline="true"
              v-show="showSearch"
              label-width="98px"
            >
              <el-form-item label="患者名称:">
                <el-input v-model="patientqueryParams.name"></el-input>
              </el-form-item>
              <el-form-item label="患者范围" prop="status">
                <el-select
                  v-model="patientqueryParams.topic"
                  placeholder="请选择"
                >
                  <el-option
                    v-for="item in topicoptions"
                    :key="item.value"
                    :label="item.label"
                    :value="item.value"
                  >
                  </el-option>
                </el-select>
              </el-form-item>
              <el-form-item label="出院科室" prop="status">
                <el-select
                  v-model="patientqueryParams.topic"
                  placeholder="请选择"
                >
                  <el-option
                    v-for="item in topicoptions"
                    :key="item.value"
                    :label="item.label"
                    :value="item.value"
                  >
                  </el-option>
                </el-select>
              </el-form-item>
              <el-form-item label="出院病区" prop="status">
                <el-select
                  v-model="patientqueryParams.topic"
                  placeholder="请选择"
                >
                  <el-option
                    v-for="item in topicoptions"
                    :key="item.value"
                    :label="item.label"
                    :value="item.value"
                  >
                  </el-option>
                </el-select>
              </el-form-item>
              <el-form-item>
                <el-button
                  type="primary"
                  icon="el-icon-search"
                  size="medium"
                  @click="handleQuery"
                  >搜索</el-button
                >
                <el-button
                  icon="el-icon-refresh"
                  size="medium"
                  @click="resetQuery"
                  >取消创建</el-button
                >
              </el-form-item>
            </el-form>
            <!-- é€‰æ‹©æ‚£è€…列表 -->
            <SFtable
              @handleUpdate="handleUpdate"
              @handleSelectionChange="handleSelectionChange"
              :currentList="patientuserList"
              :tableLabel="tableLabelhz"
              :controlsc="false"
            />
          </el-row>
          <pagination
            v-show="patienttotal > 0"
            :total="patienttotal"
            :page.sync="patientqueryParams.pageNum"
            :limit.sync="patientqueryParams.pageSize"
            @pagination="handleAddpatient"
          />
        </div>
      </div>
      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisiblepatient = false">取 æ¶ˆ</el-button>
        <el-button type="primary" @click="AddDispatchpatients"
          >确定添加</el-button
        >
      </span>
    </el-dialog>
  </div>
</template>
<script>
import { messagelistpatient } from "@/api/patient/homepage";
import SFtable from "@/components/SFtable"; //表格组件
export default {
  name: "ServiceDetails",
  data() {
    return {
      title: "宣教内容列表",
      currenttype: 1, //1宣教2门诊3出院4复诊5体检6问卷
      Editprogress: 1, //编辑进度
      loading: false, // é®ç½©å±‚
      patientloading: false, // é®ç½©å±‚
      dialogVisiblepatient: false, //添加患者弹框
      checkboxlist: [],
      tableLabel: [],
      // æ‚£è€…表单
      tableLabelhz: [
        { label: "患者名称", width: "", prop: "name" },
        { label: "性别", width: "", prop: "sex" },
        { label: "年龄", width: "", prop: "age" },
        { label: "就诊科室", width: "", prop: "impTemplate" },
        { label: "入院日期", width: "", prop: "create_time" },
        { label: "创建人", width: "", prop: "update_by" },
      ],
      tableLabelxj: [
        { label: "门诊编号", width: "", prop: "name" },
        { label: "姓名", width: "", prop: "name" },
        { label: "年龄", width: "", prop: "age" },
        { label: "联系电话", width: "", prop: "telcode" },
        { label: "就诊科室", width: "", prop: "impTemplate" },
        { label: "诊断", width: "", prop: "name" },
        { label: "出院时间", width: "", prop: "name" },
        { label: "发起时间", width: "", prop: "create_time" },
        { label: "状态", width: "", prop: "sex" },
        { label: "重复次数", width: "", prop: "update_by" },
        { label: "任务来源", width: "", prop: "update_by" },
        { label: "创建人", width: "", prop: "update_by" },
      ],
      tableLabelmz: [
        { label: "门诊编号", width: "", prop: "name" },
        { label: "姓名", width: "", prop: "name" },
        { label: "年龄", width: "", prop: "age" },
        { label: "联系电话", width: "", prop: "telcode" },
        { label: "就诊科室", width: "", prop: "impTemplate" },
        { label: "诊断", width: "", prop: "name" },
        { label: "出院时间", width: "", prop: "name" },
        { label: "发起时间", width: "", prop: "create_time" },
        { label: "状态", width: "", prop: "sex" },
        { label: "重复次数", width: "", prop: "update_by" },
        { label: "任务来源", width: "", prop: "update_by" },
        { label: "创建人", width: "", prop: "update_by" },
      ],
      tableLabelcy: [
        { label: "员工编号", width: "", prop: "name" },
        { label: "姓名", width: "", prop: "name" },
        { label: "年龄", width: "", prop: "age" },
        { label: "联系电话", width: "", prop: "telcode" },
        { label: "所在科室", width: "", prop: "impTemplate" },
        { label: "完成时间", width: "", prop: "finishtime" },
        { label: "状态", width: "", prop: "sex" },
        { label: "重复次数", width: "", prop: "update_by" },
        { label: "任务来源", width: "", prop: "update_by" },
        { label: "创建人", width: "", prop: "update_by" },
      ],
      topqueryParams: {
        pageNum: 1, //
        pageSize: 10,
        searchscope:2,
      },
      checkList: [],
      deliverytopqueryParams: {
        pageNum: 1, //
        pageSize: 10,
      },
      patientqueryParams: {
        pageNum: 1, //
        pageSize: 10,
      },
      topicoptions: [],
      showSearch: true, //
      total: 0, //
      sontotal: 0, //
      patienttotal: 0, //
      // é€‰ä¸­æ•°ç»„
      ids: [],
      // éžå•个禁用
      single: true,
      // éžå¤šä¸ªç¦ç”¨
      multiple: true,
      // ç”¨æˆ·è¡¨æ ¼æ•°æ®
      userList: [], //模板列表
      patientuserList: [], //选择患者列表
      sonuserList: [], //选中患者列表
      tasktopic: null, //新增类型
      form: {
        name: "",
        region: "",
        date1: "",
        date2: "",
        delivery: false,
        type: [],
        resource: "",
        desc: "",
      },
      source: [
        {
          value: 0,
          label: "所属患者",
        },
        {
          value: 1,
          label: "科室患者",
        },
        {
          value: 2,
          label: "病区患者",
        },
      ],
      options: [
        {
          value: "选项1",
          label: "黄金糕",
        },
        {
          value: "选项2",
          label: "双皮奶",
        },
        {
          value: "选项5",
          label: "北京烤鸭",
        },
      ],
      taskoptions: [
        {
          value: "1",
          label: "通知",
        },
        {
          value: "2",
          label: "随访",
        },
        {
          value: "3",
          label: "问卷",
        },
        {
          value: "4",
          label: "宣教",
        },
      ],
      quote: false,
    };
  },
  components: { SFtable },
  created() {
    this.Addsubtask();
    this.Getsubtask();
    this.Acquisitiontype();
  },
  methods: {
    // èŽ·å–å½“å‰ç±»åž‹
    Acquisitiontype() {
      this.currenttype = this.$route.query.type;
      console.log(this.currenttype);
      if (this.currenttype == 1) {
        this.title = "门诊病人任务";
        this.tableLabel = this.tableLabelxj;
        this.checkboxlist = [
          "当面",
          "多媒体",
          "纸质",
          "电话",
          "短信",
          "微信公众号",
          "微信小程序",
          "钉钉",
        ];
      } else if (this.currenttype == 2) {
        this.title = "出院病人任务";
        this.tableLabel = this.tableLabelmz;
        this.checkboxlist = ["当面", "纸质", "电话", "短信", "微信公众号"];
      } else if (this.currenttype == 3) {
        this.title = "医务人员任务";
        this.tableLabel = this.tableLabelcy;
        this.checkboxlist = ["当面", "纸质", "电话", "短信", "微信公众号"];
      }
    },
    // ä¸‹ä¸€æ­¥
    submitForm(formName) {
      if (this.Editprogress <= 3) {
        return this.Editprogress++;
      }
      // æäº¤
      // this.$refs[formName].validate((valid, object) => {
      //   if (valid) {
      //     alert("submit!");
      //   } else {
      //     console.log("error submit!!", object);
      //     return false;
      //   }
      // });
    },
    // å­ä»»åŠ¡äºŒçº§å¼¹æ¡†
    handleAddpatient(row) {
      console.log(row, "子组件数据");
      messagelistpatient(this.patientqueryParams).then((response) => {
        console.log(response);
        this.patientuserList = response.rows;
        this.patienttotal = response.total;
        this.loading = false;
      });
      this.dialogVisiblepatient = true;
    },
    handleUpdate() {},
    handleDelete() {},
    handleExport() {},
    // å¤šé€‰æ¡†é€‰ä¸­æ•°æ®
    handleSelectionChange(selection) {
      this.ids = null;
      this.ids = selection.map((item) => item.patid).join(",");
      // let result = this.ids.join(",");
      this.multiple = !selection.length;
      console.log(this.ids);
    },
    getList() {},
    handleQuery() {},
    resetQuery() {},
    handleClosehz() {
      this.dialogVisiblepatient = false;
    },
    // ä¸Šä¸€æ­¥
    laststep() {
      this.Editprogress--;
    },
    // æäº¤è¡¨å•
    resetForm(formName) {
      this.$refs[formName].resetFields();
    },
    // é¢„览模板
    PreviewTemplate() {},
    Acknowledgereference() {
      this.quote = true;
    },
    // æ–°å¢žå­ä»»åŠ¡
    Addsubtask() {
      this.topqueryParams.pguid = 2;
      // addsvr_prjtask(this.topqueryParams).then((res) => {
      //   console.log(res);
      // });
    },
    // æ–°å¢žæ´¾é€æ‚£è€…
    AddDispatchpatients() {
      let objictpint = {};
      objictpint.patientes = this.ids;
      objictpint.pguid = 2;
      // Addpatienttask(objictpint).then((res) => {
      //   console.log(res);
      // });
      this.dialogVisiblepatient = false;
    },
    // æŸ¥è¯¢å­ä»»åŠ¡åˆ—è¡¨
    Getsubtask() {
      this.topqueryParams.pguid = 2;
      console.log(this.topqueryParams);
      messagelistpatient(this.topqueryParams).then((res) => {
        this.userList = res.rows;
        this.total = res.total;
        console.log(this.userList);
      });
    },
    /** æŸ¥è¯¢æ‚£è€…列表 */
  },
};
</script>
<style lang="scss" scoped>
.Questionnairemanagement {
}
.leftvlue-jbxx {
  margin-top: 10px;
}
.sidecolumn {
  width: 100%;
  // min-height: 12vh;
  margin: 20px;
  margin-bottom: 0;
  padding: 20px;
  background: #edf1f7;
  border: 1px solid #dcdfe6;
  -webkit-box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12),
    0 0 6px 0 rgba(0, 0, 0, 0.04);
}
.leftvlue {
  //   display: flex;
  //   flex: 1;
  width: 100%;
  margin-top: 20px;
  //   margin: 20px;
  padding: 30px;
  background: #ffff;
  border: 1px solid #dcdfe6;
  -webkit-box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12),
    0 0 6px 0 rgba(0, 0, 0, 0.04);
}
.examine-jic {
  .headline {
    font-size: 24px;
    border-left: 5px solid #41a1be;
    padding-left: 5px;
    margin-bottom: 10px;
    display: flex;
    justify-content: space-between;
    .Add-details {
      font-size: 18px;
      color: #02a7f0;
      cursor: pointer;
    }
  }
  .jic-value {
    font-size: 20px;
    border-top: 1px solid #a7abac;
    padding: 10px;
    margin-bottom: 10px;
    .details-jic {
      padding: 10px 15px;
      border: 1px solid #dcdfe6;
      -webkit-box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12),
        0 0 6px 0 rgba(0, 0, 0, 0.04);
      .details-title {
        display: flex;
        justify-content: space-between;
        margin-bottom: 10px;
        div:nth-child(2) {
          color: #02a7f0;
          cursor: pointer;
        }
      }
      .details-renw {
        background: #e4ebfc;
        padding: 15px 5px;
        border-radius: 5px;
        margin-bottom: 20px;
      }
    }
  }
}
// .leftvlue-jbxx {
//   margin-bottom: 50px;
//   font-size: 20px;
//   span {
//     position: absolute;
//     right: 80px;
//   }
//   .demo-cascader {
//     margin-right: 20px;
//   }
//   .PreviewTemplate {
//     color: #02a7f0;
//     cursor: pointer;
//     font-size: 20px;
//     margin: 0 20px;
//   }
// }
.jic-value {
  font-size: 20px;
  border-top: 1px solid #a7abac;
  padding: 10px;
  margin-bottom: 10px;
  .details-jic {
    padding: 10px 15px;
    border: 1px solid #dcdfe6;
    -webkit-box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12),
      0 0 6px 0 rgba(0, 0, 0, 0.04);
    .details-title {
      display: flex;
      justify-content: space-between;
      margin-bottom: 10px;
      div:nth-child(2) {
        color: #02a7f0;
        cursor: pointer;
      }
    }
    .details-renw {
      background: #e4ebfc;
      padding: 15px 5px;
      border-radius: 5px;
      margin-bottom: 20px;
    }
  }
}
::v-deep .addtopic-input {
  input {
    background: #02a7f0;
    color: #edf1f7;
    width: 150px;
  }
}
::v-deep.el-step.is-vertical .el-step__title {
  font-size: 25px;
}
::v-deep.el-row {
  margin-bottom: 10px;
}
// ::v-deep.el-input--medium {
//   font-size: 24px !important;
// }
::v-deep.ruleFormaa.el-select {
  display: inline-block;
  position: relative;
  width: 700px;
}
.el-select__tags {
  font-size: 20px;
  max-width: 888px !important;
}
::v-deep.el-radio__inner {
  width: 22px;
  height: 22px;
}
// ::v-deep.topic-dev.el-radio__label {
//   font-size: 24px;
// }
::v-deep.el-radio-group {
  span {
    font-size: 24px;
  }
}
::v-deep.el-checkbox-group {
  span {
    font-size: 24px;
  }
}
</style>
src/views/Satisfaction/sfstatistics/IndicatorStatistics.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
<!-- StatisticsMain.vue -->
<template>
  <div class="statistics-main">
    <el-tabs v-model="activeTab" @tab-click="handleTabChange">
      <el-tab-pane label="满意度统计" name="followup">
        <followup-statistics
          v-if="activeTab === 'followup'"
          ref="followupRef"
        />
      </el-tab-pane>
      <el-tab-pane label="复诊通知统计" name="visitStatistics">
        <visit-Statistics
          v-if="activeTab === 'visitStatistics'"
          ref="visitStatisticsRef"
        />
      </el-tab-pane>
    </el-tabs>
  </div>
</template>
<script>
import FollowupStatistics from "./components/FollowupStatistics.vue";
import visitStatistics from "./components/visitStatistics.vue";
import SatisfactionStatistics from "./components/SatisfactionStatistics.vue";
export default {
  name: "StatisticsMain",
  components: {
    FollowupStatistics,
    SatisfactionStatistics,
    visitStatistics,
  },
  data() {
    return {
      activeTab: "followup",
    };
  },
  methods: {
    handleTabChange(tab) {
      console.log("切换到:", tab.name);
    },
  },
};
</script>
<style lang="scss" scoped>
.statistics-main {
  padding: 20px;
  background: #fff;
  min-height: calc(100vh - 84px);
  ::v-deep .el-tabs__header {
    margin-bottom: 20px;
  }
  ::v-deep .el-tabs__item {
    font-size: 16px;
    font-weight: 500;
  }
  ::v-deep .el-tabs__nav-wrap::after {
    height: 1px;
  }
}
</style>
src/views/Satisfaction/sfstatistics/components/FollowupStatistics.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1030 @@
<template>
  <div class="followup-statistics">
    <div class="query-section">
      <el-form
        :model="queryParams"
        ref="queryForm"
        size="medium"
        :inline="true"
        label-width="100px"
        class="query-form"
      >
        <el-form-item label="统计类型" prop="statisticaltype">
          <el-select
            v-model="queryParams.statisticaltype"
            placeholder="请选择统计类型"
            clearable
            @change="handleStatisticalTypeChange"
          >
            <el-option
              v-for="item in Statisticallist"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
        <!-- ç—…区选择 -->
        <el-form-item
          v-if="queryParams.statisticaltype == 1"
          label="病区"
          prop="leavehospitaldistrictcodes"
        >
          <el-select
            v-model="queryParams.leavehospitaldistrictcodes"
            placeholder="请选择病区"
            multiple
            collapse-tags
            filterable
            clearable
            style="width: 300px"
          >
            <el-option
              v-for="item in flatArrayhospit"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
        <!-- ç§‘室选择 -->
        <el-form-item
          v-if="queryParams.statisticaltype == 2"
          label="科室"
          prop="deptcodes"
        >
          <el-select
            v-model="queryParams.deptcodes"
            placeholder="请选择科室"
            multiple
            collapse-tags
            filterable
            clearable
            style="width: 300px"
          >
            <el-option
              v-for="item in flatArraydept"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="服务类型" prop="serviceType">
          <el-select
            v-model="queryParams.serviceType"
            placeholder="请选择服务类型"
            multiple
            collapse-tags
            clearable
            style="width: 300px"
          >
            <el-option
              v-for="item in options"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="随访时间" prop="dateRange">
          <el-date-picker
            v-model="queryParams.dateRange"
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            value-format="yyyy-MM-dd"
            :picker-options="pickerOptions"
            style="width: 380px"
          />
        </el-form-item>
        <el-form-item>
          <el-button
            type="primary"
            icon="el-icon-search"
            @click="handleQuery"
            :loading="loading"
          >
            æœç´¢
          </el-button>
          <el-button icon="el-icon-refresh" @click="resetQuery">
            é‡ç½®
          </el-button>
          <el-button
            type="warning"
            icon="el-icon-download"
            @click="handleExport"
            :disabled="!userList.length"
          >
            å¯¼å‡º
          </el-button>
        </el-form-item>
      </el-form>
    </div>
    <div class="table-section">
      <el-table
        v-loading="loading"
        :data="userList"
        :border="true"
        style="width: 100%"
        @selection-change="handleSelectionChange"
        :row-key="getRowKey"
      >
        <!-- ç—…区列 -->
        <el-table-column
          v-if="queryParams.statisticaltype == 1"
          label="出院病区"
          align="center"
          sortable
          key="leavehospitaldistrictname"
          prop="leavehospitaldistrictname"
          :show-overflow-tooltip="true"
          :sort-method="sortChineseNumber"
          min-width="120"
        />
        <!-- ç§‘室列 -->
        <el-table-column
          v-if="queryParams.statisticaltype == 2"
          label="科室"
          align="center"
          key="deptname"
          prop="deptname"
          :show-overflow-tooltip="true"
          min-width="120"
        />
        <el-table-column
          label="出院人次"
          align="center"
          key="dischargeCount"
          prop="dischargeCount"
          min-width="100"
        />
        <el-table-column
          label="无需随访人次"
          align="center"
          key="nonFollowUp"
          prop="nonFollowUp"
          min-width="120"
        />
        <el-table-column
          label="应随访人次"
          align="center"
          key="followUpNeeded"
          prop="followUpNeeded"
          min-width="120"
        />
        <el-table-column
          label="随访率"
          align="center"
          key="followUpRate"
          prop="followUpRate"
          min-width="100"
        >
          <template slot-scope="scope">
            <span
              v-if="
                scope.row.followUpRate !== null &&
                scope.row.followUpRate !== undefined
              "
            >
              {{ scope.row.followUpRate }}
            </span>
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column
          label="及时率"
          align="center"
          key="rate"
          prop="rate"
          min-width="100"
        >
          <template slot-scope="scope">
            <el-button
              v-if="scope.row.rate !== null && scope.row.rate !== undefined"
              type="text"
              @click="handleSeedetails(scope.row)"
            >
              {{ formatPercent(scope.row.rate) }}
            </el-button>
            <span v-else style="color: #909399">-</span>
          </template>
        </el-table-column>
        <el-table-column
          label="满意度题目总量"
          align="center"
          key="joyAllCount"
          prop="joyAllCount"
          min-width="140"
        />
        <el-table-column
          label="满意度填报量"
          align="center"
          key="joyCount"
          prop="joyCount"
          min-width="120"
        />
        <el-table-column
          label="完成比率"
          align="center"
          key="joyTotal"
          prop="joyTotal"
          min-width="100"
        >
          <template slot-scope="scope">
            <span
              v-if="
                scope.row.joyTotal !== null && scope.row.joyTotal !== undefined
              "
            >
              {{ formatPercent(scope.row.joyTotal) }}
            </span>
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column label="操作" align="center" fixed="right" width="120">
          <template slot-scope="scope">
            <el-button type="text" @click="getinfo(scope.row)">
              <i class="el-icon-s-order" style="margin-right: 4px"></i>
              æŸ¥çœ‹è¯¦æƒ…
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <!-- åˆ†é¡µ -->
    <div class="pagination-section" v-if="total > 0">
      <el-pagination
        background
        layout="total, sizes, prev, pager, next, jumper"
        :current-page="queryParams.pageNum"
        :page-size="queryParams.pageSize"
        :page-sizes="[10, 20, 30, 50]"
        :total="total"
        @size-change="handleSizeChange"
        @current-change="handlePageChange"
      />
    </div>
    <!-- æœªåŠæ—¶éšè®¿è¯¦æƒ…对话框 -->
    <el-dialog
      title="未及时随访患者服务"
      :visible.sync="SeedetailsVisible"
      width="80%"
      :close-on-click-modal="false"
    >
      <SeedetailsDialog
        v-if="SeedetailsVisible"
        :row-data="currentRow"
        :query-params="queryParams"
        @close="SeedetailsVisible = false"
      />
    </el-dialog>
    <!-- æ»¡æ„åº¦è¯¦æƒ…对话框 -->
    <el-dialog
      :visible.sync="topicVisible"
      width="60%"
      :close-on-click-modal="false"
    >
      <template #title>
        <div style="display: flex; align-items: center">
          <i
            class="el-icon-s-data"
            style="margin-right: 8px; color: #409eff"
          ></i>
          <span>{{ topicvalue.name }}</span>
          <span style="margin-left: 10px; color: #666; font-size: 14px"
            >满意度指标详情</span
          >
        </div>
      </template>
      <topic-dialog
        v-if="topicVisible"
        :row-data="currentRow"
        :topicList="topiclist"
        :query-params="queryParams"
        @close="topicVisible = false"
      />
    </el-dialog>
  </div>
</template>
<script>
import {
  getSfStatisticsJoy,
  getSfStatisticsJoyInfo,
  selectTimelyRate,
} from "@/api/system/user";
import ExcelJS from "exceljs";
import { saveAs } from "file-saver";
import SeedetailsDialog from "./components/SeedetailsDialog.vue";
import TopicDialog from "./components/TopicDialog.vue";
export default {
  name: "FollowupStatistics",
  components: {
    SeedetailsDialog,
    TopicDialog,
  },
  data() {
    return {
      // æŸ¥è¯¢å‚æ•°
      queryParams: {
        statisticaltype: 1,
        leavehospitaldistrictcodes: ["all"],
        deptcodes: [],
        serviceType: [2],
        dateRange: [],
        pageNum: 1,
        pageSize: 20,
      },
      // ç»Ÿè®¡ç±»åž‹åˆ—表
      Statisticallist: [
        { label: "病区统计", value: 1 },
        { label: "科室统计", value: 2 },
      ],
      // ç—…区列表
      flatArrayhospit: [],
      // ç§‘室列表
      flatArraydept: [],
      // æœåŠ¡ç±»åž‹é€‰é¡¹
      options: [],
      // è¡¨æ ¼æ•°æ®
      userList: [],
      // æ€»æ¡æ•°
      total: 0,
      // åŠ è½½çŠ¶æ€
      loading: false,
      // é€‰ä¸­çš„行
      ids: [],
      single: true,
      multiple: true,
      // å½“前操作的行
      currentRow: null,
      // å¯¹è¯æ¡†æ˜¾ç¤ºæŽ§åˆ¶
      SeedetailsVisible: false,
      topicVisible: false,
      // æ»¡æ„åº¦è¯¦æƒ…数据
      topiclist: [],
      topicvalue: {
        name: "",
      },
      // æ—¥æœŸé€‰æ‹©å™¨é€‰é¡¹
      pickerOptions: {
        shortcuts: [
          {
            text: "最近一周",
            onClick(picker) {
              const end = new Date();
              const start = new Date();
              start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
              picker.$emit("pick", [start, end]);
            },
          },
          {
            text: "最近一个月",
            onClick(picker) {
              const end = new Date();
              const start = new Date();
              start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
              picker.$emit("pick", [start, end]);
            },
          },
          {
            text: "最近三个月",
            onClick(picker) {
              const end = new Date();
              const start = new Date();
              start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
              picker.$emit("pick", [start, end]);
            },
          },
        ],
        disabledDate(time) {
          return time.getTime() > Date.now();
        },
      },
    };
  },
  created() {
    this.initData();
  },
  methods: {
    // åˆå§‹åŒ–数据
    async initData() {
      await this.getDeptTree();
      await this.getList();
    },
    // èŽ·å–ç§‘å®¤æ ‘
    getDeptTree() {
      // èŽ·å–æœåŠ¡ç±»åž‹
      this.options = this.$store.getters.tasktypes || [];
      // èŽ·å–ç§‘å®¤åˆ—è¡¨
      this.flatArraydept = (this.$store.getters.belongDepts || []).map(
        (dept) => {
          return {
            label: dept.deptName,
            value: dept.deptCode,
          };
        }
      );
      // èŽ·å–ç—…åŒºåˆ—è¡¨
      this.flatArrayhospit = (this.$store.getters.belongWards || []).map(
        (ward) => {
          return {
            label: ward.districtName,
            value: ward.districtCode,
          };
        }
      );
      // æ·»åŠ å…¨éƒ¨é€‰é¡¹
      this.flatArraydept.push({ label: "全部", value: "all" });
      this.flatArrayhospit.push({ label: "全部", value: "all" });
    },
    // èŽ·å–ç»Ÿè®¡åˆ—è¡¨
    async getList() {
      this.loading = true;
      try {
        // å¤„理查询参数
        const params = {
          configKey: "joyCount",
          ...this.queryParams,
        };
        // å¤„理日期范围
        if (
          this.queryParams.dateRange &&
          this.queryParams.dateRange.length === 2
        ) {
          params.startTime = this.queryParams.dateRange[0];
          params.endTime = this.queryParams.dateRange[1];
        }
        // å¤„理病区/科室选择
        if (params.statisticaltype == 1) {
          // ç—…区统计
          if (params.leavehospitaldistrictcodes.includes("all")) {
            // å¦‚果选择了"全部",则移除"all"值
            params.leavehospitaldistrictcodes =
              params.leavehospitaldistrictcodes.filter(
                (item) => item !== "all"
              );
            // å¦‚果需要传所有病区代码,可以从store中获取
            params.leavehospitaldistrictcodes = (
              this.$store.getters.belongWards || []
            ).map((ward) => ward.districtCode);
          }
        } else if (params.statisticaltype == 2) {
          // ç§‘室统计
          if (params.deptcodes.includes("all")) {
            // å¦‚果选择了"全部",则移除"all"值
            params.deptcodes = params.deptcodes.filter(
              (item) => item !== "all"
            );
            // å¦‚果需要传所有科室代码,可以从store中获取
            params.deptcodes = (this.$store.getters.belongDepts || []).map(
              (dept) => dept.deptCode
            );
          }
        }
        const response = await getSfStatisticsJoy(params);
        this.userList = this.customSort(response.data) || [];
        this.total = response.total || 0;
      } catch (error) {
        console.error("获取统计列表失败:", error);
        this.$message.error("获取数据失败");
      } finally {
        this.loading = false;
      }
    },
    sortChineseNumber(aRow, bRow) {
      const a = aRow.leavehospitaldistrictname;
      const b = bRow.leavehospitaldistrictname;
      // ä¸­æ–‡æ•°å­—到阿拉伯数字的映射(扩展到45)
      const chineseNumMap = {
        ä¸€: 1,
        äºŒ: 2,
        ä¸‰: 3,
        å››: 4,
        äº”: 5,
        å…­: 6,
        ä¸ƒ: 7,
        å…«: 8,
        ä¹: 9,
        å: 10,
        åä¸€: 11,
        åäºŒ: 12,
        åä¸‰: 13,
        åå››: 14,
        åäº”: 15,
        åå…­: 16,
        åä¸ƒ: 17,
        åå…«: 18,
        åä¹: 19,
        äºŒå: 20,
        äºŒåä¸€: 21,
        äºŒåäºŒ: 22,
        äºŒåä¸‰: 23,
        äºŒåå››: 24,
        äºŒåäº”: 25,
        äºŒåå…­: 26,
        äºŒåä¸ƒ: 27,
        äºŒåå…«: 28,
        äºŒåä¹: 29,
        ä¸‰å: 30,
        ä¸‰åä¸€: 31,
        ä¸‰åäºŒ: 32,
        ä¸‰åä¸‰: 33,
        ä¸‰åå››: 34,
        ä¸‰åäº”: 35,
        ä¸‰åå…­: 36,
        ä¸‰åä¸ƒ: 37,
        ä¸‰åå…«: 38,
        ä¸‰åä¹: 39,
        å››å: 40,
        å››åä¸€: 41,
        å››åäºŒ: 42,
        å››åä¸‰: 43,
        å››åå››: 44,
        å››åäº”: 45,
      };
      // æå–中文数字
      const getNumberFromText = (text) => {
        if (!text || typeof text !== "string") return -1;
        // åŒ¹é…ä¸­æ–‡æ•°å­—,支持一到四十五
        const match = text.match(/^([一二三四五六七八九十]+)/);
        if (match && match[1]) {
          const chineseNum = match[1];
          return chineseNumMap[chineseNum] !== undefined
            ? chineseNumMap[chineseNum]
            : -1;
        }
        // å¦‚果没有匹配到中文数字,尝试匹配阿拉伯数字
        const arabicMatch = text.match(/^(\d+)/);
        if (arabicMatch && arabicMatch[1]) {
          const num = parseInt(arabicMatch[1], 10);
          return num >= 1 && num <= 45 ? num : -1;
        }
        return -1;
      };
      const numA = getNumberFromText(a);
      const numB = getNumberFromText(b);
      // å¤„理无法解析的情况
      if (numA === -1 && numB === -1) {
        return (a || "").localeCompare(b || "");
      }
      if (numA === -1) return 1;
      if (numB === -1) return -1;
      return numA - numB;
    },
    customSort(data) {
      // å®šä¹‰æ‚¨æœŸæœ›çš„病区顺序(扩展到四十五)
      const order = [
        "一",
        "二",
        "三",
        "四",
        "五",
        "六",
        "七",
        "八",
        "九",
        "十",
        "十一",
        "十二",
        "十三",
        "十四",
        "十五",
        "十六",
        "十七",
        "十八",
        "十九",
        "二十",
        "二十一",
        "二十二",
        "二十三",
        "二十四",
        "二十五",
        "二十六",
        "二十七",
        "二十八",
        "二十九",
        "三十",
        "三十一",
        "三十二",
        "三十三",
        "三十四",
        "三十五",
        "三十六",
        "三十七",
        "三十八",
        "三十九",
        "四十",
        "四十一",
        "四十二",
        "四十三",
        "四十四",
        "四十五",
      ];
      return data.sort((a, b) => {
        // æå–病区名称中的中文数字部分
        const getIndex = (name) => {
          if (!name || typeof name !== "string") return -1;
          // åŒ¹é…ä¸­æ–‡æ•°å­—
          const chineseMatch = name.match(/^([一二三四五六七八九十]+)/);
          if (chineseMatch && chineseMatch[1]) {
            return order.indexOf(chineseMatch[1]);
          }
          // åŒ¹é…é˜¿æ‹‰ä¼¯æ•°å­—
          const arabicMatch = name.match(/^(\d+)/);
          if (arabicMatch && arabicMatch[1]) {
            const num = parseInt(arabicMatch[1], 10);
            if (num >= 1 && num <= 45) {
              return num - 1; // å› ä¸ºæ•°ç»„索引从0开始
            }
          }
          return -1;
        };
        const indexA = getIndex(a.leavehospitaldistrictname);
        const indexB = getIndex(b.leavehospitaldistrictname);
        // æŽ’序逻辑
        if (indexA === -1 && indexB === -1) {
          return (a.leavehospitaldistrictname || "").localeCompare(
            b.leavehospitaldistrictname || ""
          );
        }
        if (indexA === -1) return 1;
        if (indexB === -1) return -1;
        return indexA - indexB;
      });
    },
    // å¤„理统计类型变化
    handleStatisticalTypeChange(value) {
      if (value === 1) {
        this.queryParams.deptcodes = [];
      } else {
        this.queryParams.leavehospitaldistrictcodes = [];
      }
      this.queryParams.pageNum = 1;
      this.getList();
    },
    // å¤„理查询
    handleQuery() {
      this.queryParams.pageNum = 1;
      this.getList();
    },
    // é‡ç½®æŸ¥è¯¢
    resetQuery() {
      this.queryParams = {
        statisticaltype: 1,
        leavehospitaldistrictcodes: [],
        deptcodes: [],
        serviceType: [2],
        dateRange: [],
        pageNum: 1,
        pageSize: 20,
      };
      this.getList();
    },
    // å¤„理分页大小变化
    handleSizeChange(size) {
      this.queryParams.pageSize = size;
      this.queryParams.pageNum = 1;
      this.getList();
    },
    // å¤„理页码变化
    handlePageChange(page) {
      this.queryParams.pageNum = page;
      this.getList();
    },
    // å¤„理行选择
    handleSelectionChange(selection) {
      this.ids = selection.map((item) => item.id);
      this.single = selection.length !== 1;
      this.multiple = !selection.length;
    },
    // èŽ·å–è¡Œkey
    getRowKey(row) {
      return row.statisticaltype === 1
        ? row.leavehospitaldistrictcode
        : row.deptcode;
    },
    // æ ¼å¼åŒ–百分比
    formatPercent(value) {
      if (value === null || value === undefined) return "-";
      const num = parseFloat(value);
      if (isNaN(num)) return "-";
      return `${(num * 100).toFixed(2)}%`;
    },
    // æŸ¥çœ‹æœªåŠæ—¶éšè®¿è¯¦æƒ…
    handleSeedetails(row) {
      this.currentRow = row;
      this.SeedetailsVisible = true;
    },
    // æŸ¥çœ‹æ»¡æ„åº¦è¯¦æƒ…
    async getinfo(row) {
      this.currentRow = row;
      try {
        // å¤„理查询参数
        const params = {
          configKey: "joyCount",
          ...this.queryParams,
        };
        // å¤„理日期范围
        if (
          this.queryParams.dateRange &&
          this.queryParams.dateRange.length === 2
        ) {
          params.startTime = this.queryParams.dateRange[0];
          params.endTime = this.queryParams.dateRange[1];
        }
        if (this.queryParams.statisticaltype == 1) {
          this.topicvalue.name = row.leavehospitaldistrictname;
          params.leavehospitaldistrictcodes = [row.leavehospitaldistrictcode];
        } else {
          this.topicvalue.name = row.deptname;
          params.deptcodes = [row.deptcode];
        }
        const response = await getSfStatisticsJoyInfo(params);
        this.topiclist = response.data || [];
      this.topicVisible = true;
      } catch (error) {
        console.error("获取满意度详情失败:", error);
        this.$message.error("获取详情失败");
      }
    },
    // å¯¼å‡ºæ•°æ®
    async handleExport() {
      if (!this.userList.length) {
        this.$message.warning("没有数据可导出");
        return;
      }
      try {
        this.loading = true;
        // æž„建日期范围字符串
        let dateRangeString = "";
        let sheetNameSuffix = "";
        if (
          this.queryParams.dateRange &&
          this.queryParams.dateRange.length === 2
        ) {
          const startDateFormatted = this.queryParams.dateRange[0];
          const endDateFormatted = this.queryParams.dateRange[1];
          dateRangeString = `${startDateFormatted}至${endDateFormatted}`;
          sheetNameSuffix = `${startDateFormatted}至${endDateFormatted}`;
        } else {
          const now = new Date();
          const currentMonth = now.getMonth() + 1;
          dateRangeString = `${currentMonth}月`;
          sheetNameSuffix = `${currentMonth}月`;
        }
        const excelName = `随访统计表_${dateRangeString}.xlsx`;
        const worksheetName = `随访统计_${sheetNameSuffix}`;
        // åˆ›å»ºExcel工作簿
        const workbook = new ExcelJS.Workbook();
        const worksheet = workbook.addWorksheet(worksheetName);
        // å®šä¹‰æ ·å¼
        const titleStyle = {
          font: { name: "微软雅黑", size: 16, bold: true },
          fill: {
            type: "pattern",
            pattern: "solid",
            fgColor: { argb: "FFE6F3FF" },
          },
          alignment: { vertical: "middle", horizontal: "center" },
          border: {
            top: { style: "thin", color: { argb: "FFD0D0D0" } },
            left: { style: "thin", color: { argb: "FFD0D0D0" } },
            bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
            right: { style: "thin", color: { argb: "FFD0D0D0" } },
          },
        };
        const headerStyle = {
          font: { name: "微软雅黑", size: 11, bold: true },
          fill: {
            type: "pattern",
            pattern: "solid",
            fgColor: { argb: "FFF5F7FA" },
          },
          alignment: { vertical: "middle", horizontal: "center" },
          border: {
            top: { style: "thin", color: { argb: "FFD0D0D0" } },
            left: { style: "thin", color: { argb: "FFD0D0D0" } },
            bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
            right: { style: "thin", color: { argb: "FFD0D0D0" } },
          },
        };
        const cellStyle = {
          font: { name: "宋体", size: 10 },
          alignment: { vertical: "middle", horizontal: "center" },
          border: {
            top: { style: "thin", color: { argb: "FFD0D0D0" } },
            left: { style: "thin", color: { argb: "FFD0D0D0" } },
            bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
            right: { style: "thin", color: { argb: "FFD0D0D0" } },
          },
        };
        // æ·»åŠ æ€»æ ‡é¢˜
        worksheet.mergeCells(1, 1, 1, 10);
        const titleCell = worksheet.getCell(1, 1);
        titleCell.value = `随访统计表(${sheetNameSuffix})`;
        titleCell.style = titleStyle;
        worksheet.getRow(1).height = 35;
        // æ·»åŠ è¡¨å¤´
        const headers = [
          this.queryParams.statisticaltype == 1 ? "出院病区" : "科室",
          "出院人次",
          "无需随访人次",
          "应随访人次",
          "随访率",
          "及时率",
          "满意度题目总量",
          "满意度填报量",
          "完成比率",
        ];
        const headerRow = worksheet.addRow(headers);
        headerRow.eachCell((cell) => {
          cell.style = headerStyle;
        });
        headerRow.height = 25;
        // æ·»åŠ æ•°æ®è¡Œ
        this.userList.forEach((item) => {
          const dataRow = worksheet.addRow([
            this.queryParams.statisticaltype == 1
              ? item.leavehospitaldistrictname
              : item.deptname,
            item.dischargeCount || 0,
            item.nonFollowUp || 0,
            item.followUpNeeded || 0,
            item.followUpRate || "0%",
            item.rate ? this.formatPercent(item.rate) : "0%",
            item.joyAllCount || 0,
            item.joyCount || 0,
            item.joyTotal ? this.formatPercent(item.joyTotal) : "0%",
          ]);
          dataRow.eachCell((cell) => {
            cell.style = cellStyle;
          });
          dataRow.height = 22;
        });
        // è®¾ç½®åˆ—宽
        worksheet.columns = [
          { width: 20 },
          { width: 12 },
          { width: 12 },
          { width: 12 },
          { width: 12 },
          { width: 12 },
          { width: 15 },
          { width: 15 },
          { width: 12 },
        ];
        // ç”Ÿæˆå¹¶ä¸‹è½½æ–‡ä»¶
        const buffer = await workbook.xlsx.writeBuffer();
        const blob = new Blob([buffer], {
          type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        });
        saveAs(blob, excelName);
        this.$message.success("导出成功");
      } catch (error) {
        console.error("导出失败:", error);
        this.$message.error(`导出失败: ${error.message}`);
      } finally {
        this.loading = false;
      }
    },
  },
};
</script>
<style lang="scss" scoped>
.followup-statistics {
  .query-section {
    background: #fff;
    padding: 20px;
    border-radius: 4px;
    margin-bottom: 20px;
    .query-form {
      display: flex;
      flex-wrap: wrap;
      ::v-deep .el-form-item {
        margin-bottom: 20px;
        &:not(:last-child) {
          margin-right: 20px;
        }
      }
    }
  }
  .table-section {
    background: #fff;
    padding: 20px;
    border-radius: 4px;
    margin-bottom: 20px;
    ::v-deep .el-table {
      th {
        background-color: #f8f9fa;
        font-weight: 600;
        color: #333;
      }
    }
  }
  .pagination-section {
    display: flex;
    justify-content: flex-end;
    background: #fff;
    padding: 20px;
    border-radius: 4px;
  }
}
</style>
src/views/Satisfaction/sfstatistics/components/SatisfactionStatistics.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1809 @@
<template>
  <div class="satisfaction-statistics">
    <!-- æŸ¥è¯¢æ¡ä»¶åŒºåŸŸ -->
    <div class="query-section">
      <el-card shadow="never">
        <el-form
          :model="queryParams"
          ref="queryForm"
          size="medium"
          :inline="true"
          label-width="100px"
          class="query-form"
        >
          <el-form-item label="统计类型" prop="patientSource">
            <el-select
              v-model="queryParams.type"
              placeholder="请选择统计类型"
              clearable
              style="width: 100%"
            >
              <el-option label="问卷类型" :value="2" />
              <el-option label="语音类型" :value="1" />
              <el-option label="全部" :value="null" />
            </el-select>
          </el-form-item>
          <el-form-item label="科室" prop="deptCode">
            <el-select
              v-model="queryParams.deptCode"
              placeholder="请选择科室"
              clearable
              filterable
              style="width: 200px"
              @change="handleDeptChange"
            >
              <el-option
                v-for="dept in deptList"
                :key="dept.value"
                :label="dept.label"
                :value="dept.value"
              />
            </el-select>
          </el-form-item>
          <el-form-item label="病区" prop="wardCode">
            <el-select
              v-model="queryParams.wardCode"
              placeholder="请选择病区"
              clearable
              filterable
              style="width: 200px"
              @change="handleWardChange"
            >
              <el-option
                v-for="ward in wardList"
                :key="ward.value"
                :label="ward.label"
                :value="ward.value"
              />
            </el-select>
          </el-form-item>
          <el-form-item label="统计时间" prop="dateRange">
            <el-date-picker
              v-model="queryParams.dateRange"
              type="daterange"
              range-separator="至"
              start-placeholder="开始日期"
              end-placeholder="结束日期"
              value-format="yyyy-MM-dd"
              :picker-options="pickerOptions"
              style="width: 380px"
            />
          </el-form-item>
          <el-form-item>
            <el-button
              type="primary"
              icon="el-icon-search"
              @click="handleSearch"
              :loading="loading"
            >
              æŸ¥è¯¢
            </el-button>
            <el-button icon="el-icon-refresh" @click="handleReset">
              é‡ç½®
            </el-button>
          </el-form-item>
        </el-form>
      </el-card>
    </div>
    <!-- æ»¡æ„åº¦ç±»åž‹ç»Ÿè®¡å›¾è¡¨ -->
    <div class="chart-section">
      <el-card shadow="never">
        <div class="chart-container">
          <div class="chart-header">
            <h3 class="chart-title">满意度类型填报比例统计</h3>
            <div class="statistic-info">
              <div class="statistic-item">
                <span class="statistic-label">发送问卷总量:</span>
                <span class="statistic-value">{{
                  totalSendCount.toLocaleString()
                }}</span>
              </div>
              <div class="statistic-item">
                <span class="statistic-label">回收问卷总量:</span>
                <span class="statistic-value">{{
                  totalReceiveCount.toLocaleString()
                }}</span>
              </div>
              <div class="statistic-item">
                <span class="statistic-label">总体回收率:</span>
                <span class="statistic-value">{{
                  formatPercent(overallRecoveryRate)
                }}</span>
              </div>
            </div>
          </div>
          <div
            id="satisfactionBarChart"
            style="width: 100%; height: 400px"
          ></div>
        </div>
      </el-card>
    </div>
    <!-- Tab标签页 -->
    <div class="tab-section">
      <el-card shadow="never">
        <el-tabs v-model="activeTab" @tab-click="handleTabClick">
          <el-tab-pane label="题目明细统计" name="questionDetail">
            <!-- é¢˜ç›®æ˜Žç»†è¡¨æ ¼ -->
            <div class="detail-table-section">
              <el-table
                v-loading="detailLoading"
                :data="questionDetailData"
                :border="true"
                style="width: 100%"
                row-class-name="question-row"
              >
                <el-table-column type="expand" width="60">
                  <template slot-scope="{ row }">
                    <div class="option-detail">
                      <el-table
                        :data="row.options"
                        :border="true"
                        style="width: 100%"
                        class="inner-table"
                      >
                        <el-table-column
                          label="选项"
                          prop="optionText"
                          align="center"
                          min-width="200"
                        />
                        <el-table-column
                          label="选择人数"
                          prop="chosenQuantity"
                          align="center"
                          min-width="120"
                        />
                        <el-table-column
                          label="选择比例"
                          prop="chosenPercentage"
                          align="center"
                          min-width="120"
                        >
                          <template slot-scope="{ row: option }">
                            {{ formatPercent(option.chosenPercentage) }}
                          </template>
                        </el-table-column>
                      </el-table>
                    </div>
                  </template>
                </el-table-column>
                <el-table-column
                  label="序号"
                  type="index"
                  align="center"
                  width="60"
                />
                <el-table-column
                  label="题目"
                  prop="scriptContent"
                  align="center"
                  min-width="300"
                >
                  <template slot-scope="{ row }">
                    <span>{{ row.scriptContent }}</span>
                    <el-tag
                      :type="row.scriptType === 1 ? 'primary' : 'success'"
                      size="mini"
                      style="margin-left: 5px"
                    >
                      {{ row.scriptType === 1 ? "单选题" : "多选题" }}
                    </el-tag>
                  </template>
                </el-table-column>
                <el-table-column
                  label="平均得分"
                  prop="averageScore"
                  align="center"
                  width="120"
                >
                  <template slot-scope="{ row }">
                    <span class="score-text">{{
                      row.averageScore.toFixed(1)
                    }}</span>
                  </template>
                </el-table-column>
                <el-table-column
                  label="最高得分"
                  prop="maxScore"
                  align="center"
                  width="120"
                >
                  <template slot-scope="{ row }">
                    <span class="score-text">{{
                      row.maxScore.toFixed(1)
                    }}</span>
                  </template>
                </el-table-column>
                <el-table-column
                  label="最低得分"
                  prop="minScore"
                  align="center"
                  width="120"
                >
                  <template slot-scope="{ row }">
                    <span class="score-text">{{
                      row.minScore.toFixed(1)
                    }}</span>
                  </template>
                </el-table-column>
                <el-table-column
                  label="答题人数"
                  prop="answerCount"
                  align="center"
                  width="100"
                />
                <el-table-column
                  label="未答题人数"
                  prop="unanswerCount"
                  align="center"
                  width="100"
                >
                  <template slot-scope="{ row }">
                    {{ row.noAnswerPerson }}
                  </template>
                </el-table-column>
                <el-table-column
                  label="答题率"
                  prop="answerRate"
                  align="center"
                  width="100"
                >
                  <template slot-scope="{ row }">
                    {{ formatPercent(row.answerRate) }}
                  </template>
                </el-table-column>
              </el-table>
              <!-- ç»¼åˆå¾—分行 -->
              <div class="summary-row">
                <div class="summary-content">
                  <div class="summary-item">
                    <span class="label">总答题人数:</span>
                    <span class="value">{{ totalAnswerCount }}</span>
                  </div>
                  <div class="summary-item">
                    <span class="label">总答题率:</span>
                    <span class="value">{{
                      formatPercent(totalAnswerRate)
                    }}</span>
                  </div>
                </div>
              </div>
              <!-- åˆ†é¡µ -->
              <div
                class="pagination-section"
                v-if="questionDetailData.length > 0"
              >
                <el-pagination
                  background
                  layout="total, sizes, prev, pager, next, jumper"
                  :current-page="detailQueryParams.pageNum"
                  :page-size="detailQueryParams.pageSize"
                  :page-sizes="[10, 20, 30]"
                  :total="detailTotal"
                  @size-change="handleDetailSizeChange"
                  @current-change="handleDetailPageChange"
                />
              </div>
            </div>
          </el-tab-pane>
          <el-tab-pane label="各类型统计明细" name="typeDetail">
            <!-- å„类型统计明细表格 -->
            <div class="type-detail-section">
              <el-table
                v-loading="typeDetailLoading"
                :data="typeDetailData"
                :border="true"
                style="width: 100%"
                class="type-detail-table"
              >
                <el-table-column
                  label="序号"
                  type="index"
                  align="center"
                  width="60"
                />
                <el-table-column
                  label="满意度类型"
                  prop="typeName"
                  align="center"
                  min-width="150"
                >
                  <template slot-scope="{ row }">
                    <div class="type-name-cell">
                      <span class="type-name">{{ row.typeName }}</span>
                      <el-tag
                        v-if="row.isSpecial"
                        type="warning"
                        size="mini"
                        style="margin-left: 5px"
                      >
                        ç‰¹æ®Š
                      </el-tag>
                    </div>
                  </template>
                </el-table-column>
                <el-table-column
                  label="发送问卷数"
                  prop="sendCount"
                  align="center"
                  width="120"
                >
                  <template slot-scope="{ row }">
                    <span class="number-text">{{
                      row.sendCount.toLocaleString()
                    }}</span>
                  </template>
                </el-table-column>
                <el-table-column
                  label="回收问卷数"
                  prop="receiveCount"
                  align="center"
                  width="120"
                >
                  <template slot-scope="{ row }">
                    <span class="number-text">{{
                      row.receiveCount.toLocaleString()
                    }}</span>
                  </template>
                </el-table-column>
                <el-table-column
                  label="回收率"
                  prop="recoveryRate"
                  align="center"
                  width="120"
                >
                  <template slot-scope="{ row }">
                    <span
                      class="rate-text"
                      :class="getRateClass(row.recoveryRate)"
                    >
                      {{ formatPercent(row.recoveryRate) }}
                    </span>
                  </template>
                </el-table-column>
                <el-table-column
                  label="平均分"
                  prop="averageScore"
                  align="center"
                  width="120"
                >
                  <template slot-scope="{ row }">
                    <span class="score-text">{{
                      row.averageScore.toFixed(1)
                    }}</span>
                  </template>
                </el-table-column>
                <el-table-column
                  label="满意度等级"
                  prop="satisfactionLevel"
                  align="center"
                  width="120"
                >
                  <template slot-scope="{ row }">
                    <el-tag
                      :type="getLevelTagType(row.satisfactionLevel)"
                      effect="dark"
                      size="small"
                    >
                      {{ row.satisfactionLevel }}
                    </el-tag>
                  </template>
                </el-table-column>
                <el-table-column
                  label="趋势"
                  prop="trend"
                  align="center"
                  width="120"
                >
                  <template slot-scope="{ row }">
                    <div class="trend-cell">
                      <i
                        v-if="row.trend === 'up'"
                        class="el-icon-top trend-up"
                        :style="{ color: '#67C23A' }"
                      />
                      <i
                        v-else-if="row.trend === 'down'"
                        class="el-icon-bottom trend-down"
                        :style="{ color: '#F56C6C' }"
                      />
                      <i
                        v-else
                        class="el-icon-minus trend-stable"
                        :style="{ color: '#909399' }"
                      />
                      <span class="trend-text">{{
                        row.trend === "up"
                          ? "上升"
                          : row.trend === "down"
                          ? "下降"
                          : "稳定"
                      }}</span>
                    </div>
                  </template>
                </el-table-column>
                <el-table-column
                  label="操作"
                  align="center"
                  width="120"
                  fixed="right"
                >
                  <template slot-scope="{ row }">
                    <el-button
                      type="text"
                      size="small"
                      @click="handleTypeDetail(row)"
                    >
                      è¯¦æƒ…
                    </el-button>
                    <el-button
                      type="text"
                      size="small"
                      @click="handleExportData(row)"
                    >
                      å¯¼å‡º
                    </el-button>
                  </template>
                </el-table-column>
              </el-table>
              <!-- ç±»åž‹ç»Ÿè®¡æ±‡æ€» -->
              <div class="type-summary-row">
                <div class="type-summary-content">
                  <div class="type-summary-item">
                    <span class="label">类型总数:</span>
                    <span class="value">{{ typeDetailData.length }}</span>
                  </div>
                  <div class="type-summary-item">
                    <span class="label">平均回收率:</span>
                    <span class="value">{{
                      formatPercent(averageRecoveryRate)
                    }}</span>
                  </div>
                  <div class="type-summary-item">
                    <span class="label">类型平均分:</span>
                    <span class="value">{{ averageTypeScore.toFixed(1) }}</span>
                  </div>
                  <div class="type-summary-item">
                    <span class="label">高满意度类型:</span>
                    <span class="value high-count"
                      >{{ highSatisfactionCount }} ä¸ª</span
                    >
                  </div>
                </div>
              </div>
            </div>
          </el-tab-pane>
        </el-tabs>
      </el-card>
    </div>
  </div>
</template>
<script>
import * as echarts from "echarts";
import { statistics, satisfactionGraph } from "@/api/system/user";
import store from "@/store";
export default {
  name: "SatisfactionStatistics",
  data() {
    return {
      // æŸ¥è¯¢å‚æ•°
      queryParams: {
        type: 2,
        patientSource: "",
        deptCode: "",
        wardCode: "",
        dateRange: [],
      },
      // å½“前激活的tab
      activeTab: "questionDetail",
      // æ‚£è€…来源选项
      patientSourceList: [
        { value: "1", label: "门诊" },
        { value: "2", label: "住院" },
        { value: "3", label: "急诊" },
        { value: "4", label: "出院" },
      ],
      // ç§‘室列表
      deptList: [],
      // ç—…区列表
      wardList: [],
      // å›¾è¡¨å®žä¾‹
      barChart: null,
      // åŠ è½½çŠ¶æ€
      loading: false,
      detailLoading: false,
      typeDetailLoading: false,
      // é¢˜ç›®æ˜Žç»†æ•°æ®
      questionDetailData: [],
      // é¢˜ç›®æ˜Žç»†æŸ¥è¯¢å‚æ•°
      detailQueryParams: {
        pageNum: 1,
        pageSize: 10,
      },
      // é¢˜ç›®æ˜Žç»†æ€»æ•°
      detailTotal: 0,
      // ç»¼åˆå¾—分
      totalScore: 0,
      totalAnswerCount: 0,
      totalAnswerRate: 0,
      // å„类型统计明细数据
      typeDetailData: [],
      // æŸ±çŠ¶å›¾æ•°æ®
      chartData: [],
      // ç»Ÿè®¡ä¿¡æ¯
      totalSendCount: 0,
      totalReceiveCount: 0,
      overallRecoveryRate: 0,
      // ç±»åž‹ç»Ÿè®¡æ±‡æ€»
      averageRecoveryRate: 0,
      averageTypeScore: 0,
      highSatisfactionCount: 0,
      // æ—¥æœŸé€‰æ‹©å™¨é€‰é¡¹
      pickerOptions: {
        shortcuts: [
          {
            text: "最近一周",
            onClick(picker) {
              const end = new Date();
              const start = new Date();
              start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
              picker.$emit("pick", [start, end]);
            },
          },
          {
            text: "最近一个月",
            onClick(picker) {
              const end = new Date();
              const start = new Date();
              start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
              picker.$emit("pick", [start, end]);
            },
          },
          {
            text: "最近三个月",
            onClick(picker) {
              const end = new Date();
              const start = new Date();
              start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
              picker.$emit("pick", [start, end]);
            },
          },
        ],
        disabledDate(time) {
          return time.getTime() > Date.now();
        },
      },
      // æ»¡æ„åº¦ç±»åž‹æ•°æ®
      satisfactionTypes: [
        { id: 401, name: "出院满意度", color: "#36B37E" },
        { id: 402, name: "住院满意度", color: "#4CAF50" },
        { id: 403, name: "门诊满意度", color: "#409EFF" },
        { id: 404, name: "常用满意度", color: "#FF9D4D" },
      ],
      // æ–°å¢žï¼šé»˜è®¤æœåŠ¡ç±»åž‹æ•°ç»„
      defaultServiceTypes: ["6", "14", "15", "16"],
      // æ–°å¢žï¼šåŸºç¡€æ¨¡æ¿é—®é¢˜ID集合
      scriptIds: [],
      // æ–°å¢žï¼šæ¨¡æ¿ID
      templateId: null,
    };
  },
  computed: {
    // è®¡ç®—查询开始时间
    startTime() {
      if (this.queryParams.dateRange && this.queryParams.dateRange[0]) {
        return this.queryParams.dateRange[0];
      }
      // é»˜è®¤æœ€è¿‘7天
      const date = new Date();
      date.setDate(date.getDate() - 7);
      return this.formatDate(date);
    },
    // è®¡ç®—查询结束时间
    endTime() {
      if (this.queryParams.dateRange && this.queryParams.dateRange[1]) {
        return this.queryParams.dateRange[1];
      }
      // é»˜è®¤ä»Šå¤©
      return this.formatDate(new Date());
    },
    // è®¡ç®—科室编码数组
    deptCodes() {
      if (this.queryParams.deptCode) {
        return [this.queryParams.deptCode];
      }
      return this.deptList.map((dept) => dept.value);
    },
    // è®¡ç®—病区编码数组
    hospitalDistrictCodes() {
      if (this.queryParams.wardCode) {
        return [this.queryParams.wardCode];
      }
      return this.wardList.map((ward) => ward.value);
    },
  },
  mounted() {
    this.initData();
  },
  beforeDestroy() {
    if (this.barChart) {
      this.barChart.dispose();
      this.barChart = null;
    }
  },
  methods: {
    // æ ¼å¼åŒ–日期
    formatDate(date) {
      const year = date.getFullYear();
      const month = String(date.getMonth() + 1).padStart(2, "0");
      const day = String(date.getDate()).padStart(2, "0");
      return `${year}-${month}-${day}`;
    },
    // åˆå§‹åŒ–数据
    async initData() {
      await this.getDeptList();
      await this.getWardList();
      this.initChart();
      await this.loadData();
    },
    // èŽ·å–ç§‘å®¤åˆ—è¡¨
    getDeptList() {
      return new Promise((resolve) => {
        this.deptList = (this.$store.getters.belongDepts || []).map((dept) => {
          return {
            label: dept.deptName,
            value: dept.deptCode,
          };
        });
        resolve();
      });
    },
    // èŽ·å–ç—…åŒºåˆ—è¡¨
    getWardList() {
      return new Promise((resolve) => {
        this.wardList = (this.$store.getters.belongWards || []).map((ward) => {
          return {
            label: ward.districtName,
            value: ward.districtCode,
          };
        });
        resolve();
      });
    },
    // åŠ è½½æ•°æ®
    async loadData() {
      await Promise.all([
        this.loadChartData(),
        this.loadQuestionDetailData(),
        this.loadTypeDetailData(),
      ]);
    },
    // åŠ è½½å›¾è¡¨æ•°æ®
    async loadChartData() {
      this.loading = true;
      try {
        const params = {
          type: this.queryParams.type,
          startTime: this.startTime,
          endTime: this.endTime,
          deptcodes: this.deptCodes,
          hospitaldistrictcodes: this.hospitalDistrictCodes,
          templateid: this.templateId,
        };
        const response = await satisfactionGraph(params);
        if (response.code === 200) {
          this.processChartData(response);
        } else {
          this.$message.error(response.msg || "获取图表数据失败");
          // ä½¿ç”¨mock数据
          await this.generateMockChartData();
        }
      } catch (error) {
        console.error("获取图表数据出错:", error);
        this.$message.error("获取图表数据失败");
        // é”™è¯¯æ—¶ä½¿ç”¨mock数据
        await this.generateMockChartData();
      } finally {
        this.loading = false;
      }
    },
    // å¤„理图表数据
    processChartData(apiData) {
      if (!apiData || !apiData.rows || Object.keys(apiData.rows).length === 0) {
        this.chartData = [];
        this.totalSendCount = 0;
        this.totalReceiveCount = 0;
        this.overallRecoveryRate = 0;
        this.renderChart([]);
        return;
      }
      const chartData = [];
      let totalSend = 0;
      let totalReceive = 0;
      let index = 0;
      // å¤„理接口返回的满意度类型统计
      Object.entries(apiData.rows).forEach(([typeName, typeStat]) => {
        const sendCount = typeStat.subidAll || 0;
        const receiveCount = typeStat.fillCountAll || 0;
        const recoveryRate = typeStat.receiveRate || 0;
        chartData.push({
          name: typeName,
          value: recoveryRate * 100, // è½¬æ¢ä¸ºç™¾åˆ†æ¯”
          sendCount: sendCount,
          receiveCount: receiveCount,
          averageScore: typeStat.averageScore || 0,
          itemStyle: { color: this.getChartColor(index) },
        });
        totalSend += sendCount;
        totalReceive += receiveCount;
        index++;
      });
      this.totalSendCount = totalSend;
      this.totalReceiveCount = totalReceive;
      this.overallRecoveryRate = totalSend > 0 ? totalReceive / totalSend : 0;
      this.chartData = chartData;
      this.renderChart(chartData);
    },
    // åŠ è½½é¢˜ç›®æ˜Žç»†æ•°æ®
    async loadQuestionDetailData() {
      this.detailLoading = true;
      try {
        const params = {
          type: this.queryParams.type,
          startTime: this.startTime,
          endTime: this.endTime,
          scriptids: this.scriptIds,
          templateid: this.templateId,
        };
        const response = await statistics(params);
        if (response.code === 200) {
          this.processQuestionDetailData(response.rows);
        } else {
          this.$message.error(response.msg || "获取题目明细数据失败");
          const mockData = await this.generateMockQuestionDetail();
          this.questionDetailData = mockData.list;
          this.detailTotal = mockData.total;
          this.calculateSummary(mockData);
        }
      } catch (error) {
        console.error("获取题目明细数据出错:", error);
        this.$message.error("获取题目明细数据失败");
        const mockData = await this.generateMockQuestionDetail();
        this.questionDetailData = mockData.list;
        this.detailTotal = mockData.total;
        this.calculateSummary(mockData);
      } finally {
        this.detailLoading = false;
      }
    },
    // å¤„理接口返回的题目明细数据
    processQuestionDetailData(apiData) {
      if (!apiData || !apiData.patSatisfactionDetailEntities) {
        this.questionDetailData = [];
        this.detailTotal = 0;
        this.totalAnswerCount = 0;
        this.totalAnswerRate = 0;
        return;
      }
      const detailData = apiData.patSatisfactionDetailEntities.map((item) => {
        const options = [];
        if (item.matchedtextStats) {
          Object.keys(item.matchedtextStats).forEach((key) => {
            const stat = item.matchedtextStats[key];
            options.push({
              optionText: key,
              chosenQuantity: stat.count || 0,
              chosenPercentage: (stat.ratio || 0) / 100,
            });
          });
        }
        return {
          scriptContent: item.scriptContent || "",
          scriptType: 1,
          answerPerson: item.answerPerson || 0,
          noAnswerPerson: item.noAnswerPerson || 0,
          answerCount: item.answerPerson || 0,
          averageScore: item.averageScore || 0,
          maxScore: item.maxScore || 0,
          minScore: item.minScore || 0,
          answerRate: item.answerRate || 0,
          totalCount: (item.answerPerson || 0) + (item.noAnswerPerson || 0),
          options: options,
        };
      });
      const startIndex =
        (this.detailQueryParams.pageNum - 1) * this.detailQueryParams.pageSize;
      const endIndex = startIndex + this.detailQueryParams.pageSize;
      const paginatedData = detailData.slice(startIndex, endIndex);
      this.questionDetailData = paginatedData;
      this.detailTotal = detailData.length;
      this.totalAnswerCount = apiData.totalPerson || 0;
      this.totalAnswerRate = apiData.totalAnswerRate || 0;
    },
    // åŠ è½½ç±»åž‹æ˜Žç»†æ•°æ®
    async loadTypeDetailData() {
      this.typeDetailLoading = true;
      try {
        const params = {
          type: this.queryParams.type,
          startTime: this.startTime,
          endTime: this.endTime,
          deptcodes: this.deptCodes,
          hospitaldistrictcodes: this.hospitalDistrictCodes,
          templateid: this.templateId,
        };
        const response = await satisfactionGraph(params);
        if (response.code === 200) {
          this.processTypeDetailData(response.data);
        } else {
          this.$message.error(response.msg || "获取类型明细数据失败");
          const mockData = await this.generateMockTypeDetail();
          this.typeDetailData = mockData;
          this.calculateTypeSummary(mockData);
        }
      } catch (error) {
        console.error("获取类型明细数据出错:", error);
        this.$message.error("获取类型明细数据失败");
        const mockData = await this.generateMockTypeDetail();
        this.typeDetailData = mockData;
        this.calculateTypeSummary(mockData);
      } finally {
        this.typeDetailLoading = false;
      }
    },
    // å¤„理类型明细数据
    processTypeDetailData(apiData) {
      if (!apiData || !apiData.rows || Object.keys(apiData.rows).length === 0) {
        this.typeDetailData = [];
        this.calculateTypeSummary([]);
        return;
      }
      const typeDetail = [];
      Object.entries(apiData.rows).forEach(([typeName, typeStat], index) => {
        const sendCount = typeStat.subidAll || 0;
        const receiveCount = typeStat.fillCountAll || 0;
        const recoveryRate = typeStat.receiveRate || 0;
        const averageScore = typeStat.averageScore || 0;
        typeDetail.push({
          id: index + 1,
          typeName: typeName,
          isSpecial: false, // æ ¹æ®å®žé™…情况判断
          sendCount: sendCount,
          receiveCount: receiveCount,
          recoveryRate: recoveryRate,
          averageScore: averageScore,
          maxScore: 5, // é»˜è®¤å€¼
          minScore: 0, // é»˜è®¤å€¼
          satisfactionLevel: this.getSatisfactionLevel(averageScore),
          trend: "stable", // é»˜è®¤ç¨³å®š
        });
      });
      this.typeDetailData = typeDetail;
      this.calculateTypeSummary(typeDetail);
    },
    // æ ¹æ®å¹³å‡åˆ†èŽ·å–æ»¡æ„åº¦ç­‰çº§
    getSatisfactionLevel(score) {
      if (score >= 4.5) return "优秀";
      if (score >= 4.0) return "良好";
      if (score >= 3.0) return "一般";
      if (score >= 2.0) return "较差";
      return "å·®";
    },
    // èŽ·å–è¶‹åŠ¿
    getTrend(trend) {
      if (trend > 0.1) return "up";
      if (trend < -0.1) return "down";
      return "stable";
    },
    // è®¡ç®—综合得分
    calculateSummary(data) {
      let totalScore = 0;
      let totalAnswerCount = 0;
      let totalCount = 0;
      data.list.forEach((item) => {
        totalScore += item.averageScore;
        totalAnswerCount += item.answerCount;
        totalCount += item.totalCount;
      });
      this.totalScore =
        data.list.length > 0 ? totalScore / data.list.length : 0;
      this.totalAnswerCount = totalAnswerCount;
      this.totalAnswerRate = totalCount > 0 ? totalAnswerCount / totalCount : 0;
    },
    // è®¡ç®—类型统计汇总
    calculateTypeSummary(data) {
      if (data.length === 0) {
        this.averageRecoveryRate = 0;
        this.averageTypeScore = 0;
        this.highSatisfactionCount = 0;
        return;
      }
      let totalRecoveryRate = 0;
      let totalTypeScore = 0;
      let highCount = 0;
      data.forEach((item) => {
        totalRecoveryRate += item.recoveryRate;
        totalTypeScore += item.averageScore;
        if (
          item.satisfactionLevel === "优秀" ||
          item.satisfactionLevel === "良好"
        ) {
          highCount++;
        }
      });
      this.averageRecoveryRate = totalRecoveryRate / data.length;
      this.averageTypeScore = totalTypeScore / data.length;
      this.highSatisfactionCount = highCount;
    },
    // åˆå§‹åŒ–图表
    initChart() {
      const chartDom = document.getElementById("satisfactionBarChart");
      if (!chartDom) return;
      this.barChart = echarts.init(chartDom);
      window.addEventListener("resize", this.handleChartResize);
    },
    // æ¸²æŸ“图表
    renderChart(chartData) {
      if (!this.barChart) return;
 if (!chartData || chartData.length === 0) {
    const emptyOption = {
      title: {
        text: "暂无数据",
        left: "center",
        top: "center",
        textStyle: {
          color: "#999",
          fontSize: 16,
          fontWeight: "normal"
        }
      },
      xAxis: { show: false },
      yAxis: { show: false }
    };
    this.barChart.setOption(emptyOption);
    return;
  }
      const option = {
        title: {
          text: "",
          left: "center",
        },
        tooltip: {
          trigger: "axis",
          axisPointer: {
            type: "shadow",
          },
          formatter: (params) => {
            const data = params[0];
            return `
              <div style="margin-bottom: 5px; font-weight: bold; color: #333;">
                ${data.name}
              </div>
              <div style="margin: 2px 0;">
                <span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:${
                  data.color
                };margin-right:5px;"></span>
                å¡«æŠ¥æ¯”例: <strong>${data.value.toFixed(1)}%</strong>
              </div>
              <div style="margin: 2px 0;">
                <span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#eee;margin-right:5px;"></span>
                å‘送问卷: <strong>${data.data.sendCount.toLocaleString()}</strong>
              </div>
              <div style="margin: 2px 0;">
                <span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#eee;margin-right:5px;"></span>
                å›žæ”¶é—®å·: <strong>${data.data.receiveCount.toLocaleString()}</strong>
              </div>
            `;
          },
        },
        grid: {
          left: "3%",
          right: "4%",
          bottom: "3%",
          top: 60,
          containLabel: true,
        },
        xAxis: {
          type: "category",
          data: chartData.map((item) => item.name),
          axisLabel: {
            interval: 0,
            rotate: 0,
            fontSize: 12,
            color: "#666",
          },
          axisLine: {
            lineStyle: {
              color: "#DCDFE6",
            },
          },
          axisTick: {
            alignWithLabel: true,
          },
        },
        yAxis: {
          type: "value",
          name: "填报比例 (%)",
          min: 0,
          max: 100,
          axisLabel: {
            formatter: "{value}%",
            color: "#666",
          },
          axisLine: {
            lineStyle: {
              color: "#DCDFE6",
            },
          },
          splitLine: {
            lineStyle: {
              type: "dashed",
              color: "#E4E7ED",
            },
          },
        },
        series: [
          {
            name: "填报比例",
            type: "bar",
            barWidth: 40,
            data: chartData,
            itemStyle: {
              color: (params) => {
                return params.data.itemStyle.color;
              },
            },
            label: {
              show: true,
              position: "top",
              formatter: "{c}%",
              fontSize: 12,
              color: "#333",
            },
          },
        ],
      };
      this.barChart.setOption(option);
    },
    // ç”ŸæˆMock图表数据
    generateMockChartData() {
      return new Promise((resolve) => {
        setTimeout(() => {
          const data = this.satisfactionTypes.map((type, index) => ({
            name: type.name,
            recoveryRate: Math.random() * 0.3 + 0.6,
            sendCount: Math.floor(Math.random() * 3000) + 1500,
            receiveCount: 0,
            color: type.color,
          }));
          data.forEach((item) => {
            item.receiveCount = Math.floor(item.sendCount * item.recoveryRate);
          });
          this.totalSendCount = data.reduce(
            (sum, item) => sum + item.sendCount,
            0
          );
          this.totalReceiveCount = data.reduce(
            (sum, item) => sum + item.receiveCount,
            0
          );
          this.overallRecoveryRate =
            this.totalSendCount > 0
              ? this.totalReceiveCount / this.totalSendCount
              : 0;
          const chartData = data.map((item) => ({
            name: item.name,
            value: item.recoveryRate * 100,
            sendCount: item.sendCount,
            receiveCount: item.receiveCount,
            itemStyle: { color: item.color },
          }));
          this.chartData = chartData;
          this.renderChart(chartData);
          resolve();
        }, 300);
      });
    },
    // ç”ŸæˆMock题目详情数据
    generateMockQuestionDetail() {
      return new Promise((resolve) => {
        setTimeout(() => {
          const questions = [
            {
              scriptContent: "您对本次就医的整体满意程度?",
              scriptType: 1,
              answerPerson: 120,
              noAnswerPerson: 30,
              answerCount: 120,
              totalCount: 150,
              averageScore: 4.2,
              maxScore: 5.0,
              minScore: 1.0,
              answerRate: 0.8,
              options: [
                {
                  optionText: "非常满意",
                  chosenQuantity: 60,
                  chosenPercentage: 0.5,
                },
                {
                  optionText: "满意",
                  chosenQuantity: 36,
                  chosenPercentage: 0.3,
                },
                {
                  optionText: "一般",
                  chosenQuantity: 18,
                  chosenPercentage: 0.15,
                },
                {
                  optionText: "不满意",
                  chosenQuantity: 6,
                  chosenPercentage: 0.05,
                },
              ],
            },
            {
              scriptContent: "您对医护人员的服务态度是否满意?",
              scriptType: 1,
              answerPerson: 145,
              noAnswerPerson: 11,
              answerCount: 145,
              totalCount: 156,
              averageScore: 4.5,
              maxScore: 5,
              minScore: 3,
              answerRate: 0.93,
              options: [
                {
                  optionText: "非常满意",
                  chosenQuantity: 89,
                  chosenPercentage: 0.61,
                },
                {
                  optionText: "满意",
                  chosenQuantity: 45,
                  chosenPercentage: 0.31,
                },
                {
                  optionText: "一般",
                  chosenQuantity: 8,
                  chosenPercentage: 0.06,
                },
                {
                  optionText: "不满意",
                  chosenQuantity: 2,
                  chosenPercentage: 0.01,
                },
                {
                  optionText: "非常不满意",
                  chosenQuantity: 1,
                  chosenPercentage: 0.01,
                },
              ],
            },
          ];
          const startIndex =
            (this.detailQueryParams.pageNum - 1) *
            this.detailQueryParams.pageSize;
          const endIndex = startIndex + this.detailQueryParams.pageSize;
          const paginatedData = questions.slice(startIndex, endIndex);
          resolve({
            list: paginatedData,
            total: questions.length,
          });
        }, 300);
      });
    },
    // ç”ŸæˆMock类型明细数据
    generateMockTypeDetail() {
      return new Promise((resolve) => {
        setTimeout(() => {
          const types = [
            {
              id: 401,
              typeName: "出院满意度",
              isSpecial: false,
              sendCount: 2850,
              receiveCount: 2680,
              recoveryRate: 0.94,
              averageScore: 4.8,
              maxScore: 5,
              minScore: 3.8,
              satisfactionLevel: "优秀",
              trend: "up",
            },
            {
              id: 402,
              typeName: "住院满意度",
              isSpecial: false,
              sendCount: 2620,
              receiveCount: 2405,
              recoveryRate: 0.918,
              averageScore: 4.6,
              maxScore: 5,
              minScore: 3.5,
              satisfactionLevel: "优秀",
              trend: "stable",
            },
            {
              id: 403,
              typeName: "门诊满意度",
              isSpecial: false,
              sendCount: 3780,
              receiveCount: 3220,
              recoveryRate: 0.852,
              averageScore: 4.3,
              maxScore: 5,
              minScore: 2.5,
              satisfactionLevel: "良好",
              trend: "up",
            },
            {
              id: 404,
              typeName: "常用满意度",
              isSpecial: true,
              sendCount: 1950,
              receiveCount: 1780,
              recoveryRate: 0.913,
              averageScore: 4.5,
              maxScore: 5,
              minScore: 3.2,
              satisfactionLevel: "良好",
              trend: "stable",
            },
          ];
          resolve(types);
        }, 300);
      });
    },
    // èŽ·å–å›¾è¡¨é¢œè‰²
    getChartColor(index) {
      const colors = [
        "#36B37E",
        "#4CAF50",
        "#409EFF",
        "#FF9D4D",
        "#9B8DFF",
        "#FF6B6B",
      ];
      return colors[index % colors.length];
    },
    // å¤„理图表响应式
    handleChartResize() {
      if (this.barChart) {
        this.barChart.resize();
      }
    },
    // å¤„理查询
    handleSearch() {
      this.detailQueryParams.pageNum = 1;
      this.loadData();
      // å¼ºåˆ¶é‡æ–°æ¸²æŸ“图表
      setTimeout(() => {
        if (this.chartData.length === 0) {
          this.renderChart([]);
        }
      }, 100);
    },
    // å¤„理重置
    handleReset() {
      this.$refs.queryForm.resetFields();
      this.queryParams.dateRange = [];
      this.detailQueryParams.pageNum = 1;
      this.loadData();
    },
    // å¤„理Tab切换
    handleTabClick(tab) {
      if (tab.name === "typeDetail" && this.typeDetailData.length === 0) {
        this.loadTypeDetailData();
      }
    },
    // å¤„理明细分页大小变化
    handleDetailSizeChange(size) {
      this.detailQueryParams.pageSize = size;
      this.detailQueryParams.pageNum = 1;
      this.loadQuestionDetailData();
    },
    // å¤„理明细页码变化
    handleDetailPageChange(page) {
      this.detailQueryParams.pageNum = page;
      this.loadQuestionDetailData();
    },
    // å¤„理类型详情
    handleTypeDetail(row) {
      this.$message.info(`查看类型详情:${row.typeName}`);
    },
    // å¤„理导出数据
    handleExportData(row) {
      this.$message.success(`正在导出 ${row.typeName} æ•°æ®...`);
    },
    // æ ¼å¼åŒ–百分比
    formatPercent(value) {
   if (value === null || value === undefined) return "-";
  const num = parseFloat(value);
  if (isNaN(num)) return "-";
  // å¦‚果值小于1,认为是小数比例,需要乘以100
  const percentValue = num < 1 ? num * 100 : num;
  return `${percentValue.toFixed(2)}%`;
    },
    // èŽ·å–å›žæ”¶çŽ‡æ ·å¼ç±»
    getRateClass(rate) {
      if (rate >= 0.9) return "rate-high";
      if (rate >= 0.8) return "rate-medium";
      return "rate-low";
    },
    // èŽ·å–æ»¡æ„åº¦ç­‰çº§æ ‡ç­¾ç±»åž‹
    getLevelTagType(level) {
      const levelMap = {
        ä¼˜ç§€: "success",
        è‰¯å¥½: "primary",
        ä¸€èˆ¬: "warning",
        è¾ƒå·®: "danger",
        å·®: "info",
      };
      return levelMap[level] || "info";
    },
    // å¤„理科室选择变化
    handleDeptChange() {
      this.loadData();
    },
    // å¤„理病区选择变化
    handleWardChange() {
      this.loadData();
    },
  },
};
</script>
<style lang="scss" scoped>
.satisfaction-statistics {
  padding: 20px;
  background-color: #f5f7fa;
  min-height: 100vh;
  .query-section {
    margin-bottom: 20px;
    .query-form {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      ::v-deep .el-form-item {
        margin-bottom: 0;
        margin-right: 20px;
        &:last-child {
          margin-right: 0;
        }
      }
    }
  }
  .chart-section {
    margin-bottom: 20px;
    .chart-container {
      .chart-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 20px;
        padding-bottom: 15px;
        border-bottom: 1px solid #f0f0f0;
        .chart-title {
          margin: 0;
          font-size: 16px;
          font-weight: 600;
          color: #303133;
        }
        .statistic-info {
          display: flex;
          gap: 30px;
          align-items: center;
          .statistic-item {
            display: flex;
            align-items: center;
            gap: 8px;
            .statistic-label {
              font-size: 14px;
              color: #606266;
            }
            .statistic-value {
              font-size: 18px;
              font-weight: 600;
              color: #409eff;
            }
          }
        }
      }
    }
  }
  .tab-section {
    ::v-deep .el-tabs__header {
      margin-bottom: 0;
    }
    ::v-deep .el-tabs__content {
      padding: 20px 0 0 0;
    }
  }
  .detail-table-section {
    .option-detail {
      padding: 15px;
      background: #f8f9fa;
      border-radius: 4px;
      margin: 10px 0;
    }
    ::v-deep .el-table {
      th {
        background-color: #f8f9fa;
        font-weight: 600;
        color: #333;
        padding: 12px 0;
      }
      td {
        padding: 12px 0;
      }
      .question-row {
        td {
          background-color: #fff;
        }
        &:hover {
          td {
            background-color: #f5f7fa;
          }
        }
      }
    }
    .score-text {
      font-weight: 600;
      color: #1890ff;
      font-size: 16px;
    }
    .summary-row {
      margin-top: 20px;
      padding: 20px;
      background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
      border-radius: 8px;
      border: 1px solid #dee2e6;
      .summary-content {
        display: flex;
        justify-content: space-around;
        align-items: center;
        .summary-item {
          text-align: center;
          .label {
            font-size: 16px;
            color: #606266;
            margin-right: 8px;
          }
          .value {
            font-size: 24px;
            font-weight: 600;
            color: #409eff;
          }
        }
      }
    }
    .pagination-section {
      display: flex;
      justify-content: center;
      padding: 20px 0 0 0;
    }
  }
  .type-detail-section {
    .type-detail-table {
      ::v-deep .el-table__header-wrapper {
        th {
          background-color: #f0f7ff;
          font-weight: 600;
          color: #333;
        }
      }
      .type-name-cell {
        display: flex;
        align-items: center;
        justify-content: center;
        .type-name {
          font-weight: 500;
        }
      }
      .number-text {
        font-weight: 600;
        color: #333;
      }
      .rate-text {
        font-weight: 600;
        font-size: 14px;
        &.rate-high {
          color: #67c23a;
        }
        &.rate-medium {
          color: #e6a23c;
        }
        &.rate-low {
          color: #f56c6c;
        }
      }
      .score-text {
        font-weight: 600;
        color: #409eff;
        font-size: 15px;
      }
      .trend-cell {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 5px;
        .trend-up,
        .trend-down,
        .trend-stable {
          font-size: 16px;
        }
        .trend-text {
          font-size: 13px;
          color: #666;
        }
      }
    }
    .type-summary-row {
      margin-top: 20px;
      padding: 20px;
      background: linear-gradient(135deg, #f0f9ff 0%, #e6f7ff 100%);
      border-radius: 8px;
      border: 1px solid #d0ebff;
      .type-summary-content {
        display: flex;
        justify-content: space-around;
        align-items: center;
        flex-wrap: wrap;
        gap: 20px;
        .type-summary-item {
          text-align: center;
          min-width: 150px;
          .label {
            font-size: 14px;
            color: #606266;
            margin-right: 8px;
          }
          .value {
            font-size: 20px;
            font-weight: 600;
            color: #409eff;
          }
          .high-count {
            color: #67c23a;
          }
        }
      }
    }
  }
  // å†…层表格样式
  .inner-table {
    ::v-deep .el-table__header-wrapper {
      th {
        background-color: #f0f7ff !important;
        color: #333;
        font-weight: 600;
      }
    }
    ::v-deep .el-table__body-wrapper {
      tr {
        background-color: #fff;
        &:hover {
          background-color: #f5f7fa;
        }
      }
    }
  }
}
@media (max-width: 768px) {
  .satisfaction-statistics {
    padding: 10px;
    .query-section {
      .query-form {
        ::v-deep .el-form-item {
          width: 100%;
          margin-right: 0;
          margin-bottom: 10px;
        }
      }
    }
    .chart-section {
      .chart-container {
        .chart-header {
          flex-direction: column;
          align-items: flex-start;
          gap: 15px;
          .statistic-info {
            width: 100%;
            justify-content: space-between;
            flex-wrap: wrap;
            gap: 10px;
          }
        }
      }
    }
    .detail-table-section {
      .summary-content {
        flex-direction: column;
        gap: 15px;
      }
    }
    .type-detail-section {
      .type-summary-content {
        flex-direction: column;
        gap: 15px;
      }
    }
  }
}
</style>
src/views/Satisfaction/sfstatistics/components/components/SeedetailsDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,401 @@
<template>
  <div class="seedetails-dialog">
    <div class="examine-jic">
      <div class="jic-value">
        <!-- æŸ¥è¯¢è¡¨å• -->
        <el-form
          :model="patientqueryParams"
          ref="patientQueryForm"
          size="small"
          :inline="true"
          class="detail-query-form"
        >
          <el-form-item label="患者:" prop="name">
            <el-input
              v-model="patientqueryParams.name"
              placeholder="请输入患者姓名"
              clearable
              @keyup.enter.native="handleSearch"
              style="width: 180px"
            />
          </el-form-item>
          <el-form-item label="患者诊断:" prop="leavediagname">
            <el-input
              v-model="patientqueryParams.leavediagname"
              placeholder="请输入患者诊断"
              clearable
              @keyup.enter.native="handleSearch"
              style="width: 200px"
            />
          </el-form-item>
          <el-form-item>
            <el-button
              type="primary"
              icon="el-icon-search"
              @click="handleSearch"
              :loading="loading"
            >
              æœç´¢
            </el-button>
            <el-button icon="el-icon-refresh" @click="handleReset">
              é‡ç½®
            </el-button>
          </el-form-item>
        </el-form>
        <!-- æ‚£è€…列表表格 -->
        <el-table
          v-loading="loading"
          :data="logsheetlist"
          style="width: 100%"
          :border="true"
          class="patient-table"
        >
          <el-table-column
            prop="sendname"
            label="姓名"
            align="center"
            width="100"
          />
          <el-table-column
            prop="taskName"
            label="任务名称"
            align="center"
            width="200"
            show-overflow-tooltip
          />
          <el-table-column
            prop="sendstate"
            label="任务状态"
            align="center"
            width="120"
          >
            <template slot-scope="{ row }">
              <div v-if="row.sendstate == 1">
                <el-tag type="primary" size="small">表单已领取</el-tag>
              </div>
              <div v-if="row.sendstate == 2">
                <el-tag type="primary" size="small">待随访</el-tag>
              </div>
              <div v-if="row.sendstate == 3">
                <el-tag type="success" size="small">表单已发送</el-tag>
              </div>
              <div v-if="row.sendstate == 4">
                <el-tag type="info" size="small">不执行</el-tag>
              </div>
              <div v-if="row.sendstate == 5">
                <el-tag type="danger" size="small">发送失败</el-tag>
              </div>
              <div v-if="row.sendstate == 6">
                <el-tag type="success" size="small">已完成</el-tag>
              </div>
            </template>
          </el-table-column>
          <el-table-column
            prop="visitTime"
            label="应随访时间"
            align="center"
            width="180"
            show-overflow-tooltip
          />
          <el-table-column
            prop="finishtime"
            label="随访完成时间"
            align="center"
            width="180"
            show-overflow-tooltip
          >
            <template slot-scope="{ row }">
              <span v-if="row.finishtime">{{ row.finishtime }}</span>
              <span v-else style="color: #f56c6c">未完成</span>
            </template>
          </el-table-column>
          <el-table-column
            label="出院日期"
            width="120"
            align="center"
            key="endtime"
            prop="endtime"
          >
            <template slot-scope="{ row }">
              <span v-if="row.endtime">{{ formatTime(row.endtime) }}</span>
              <span v-else>-</span>
            </template>
          </el-table-column>
          <el-table-column
            label="责任护士"
            width="120"
            align="center"
            key="nurseName"
            prop="nurseName"
          />
          <el-table-column
            label="主治医生"
            width="120"
            align="center"
            key="drname"
            prop="drname"
          />
          <el-table-column
            label="结果状态"
            align="center"
            key="excep"
            prop="excep"
            width="120"
          >
            <template slot-scope="{ row }">
              <dict-tag
                :options="dict.type.sys_yujing"
                :value="row.excep"
                size="small"
              />
            </template>
          </el-table-column>
          <el-table-column
            label="处理意见"
            align="center"
            key="suggest"
            prop="suggest"
            width="120"
          >
            <template slot-scope="{ row }">
              <dict-tag
                :options="dict.type.sys_suggest"
                :value="row.suggest"
                size="small"
              />
            </template>
          </el-table-column>
          <el-table-column
            prop="templatename"
            label="服务模板"
            width="150"
            align="center"
            show-overflow-tooltip
          />
          <el-table-column
            prop="remark"
            label="服务记录"
            width="150"
            align="center"
            show-overflow-tooltip
          />
          <el-table-column
            prop="bankcardno"
            label="呼叫状态"
            width="120"
            align="center"
          />
          <el-table-column
            label="操作"
            fixed="right"
            align="center"
            width="100"
          >
            <template slot-scope="{ row }">
              <el-button
                type="text"
                size="small"
                @click="handleViewDetail(row)"
              >
                <i class="el-icon-view"></i> æŸ¥çœ‹
              </el-button>
            </template>
          </el-table-column>
        </el-table>
        <!-- åˆ†é¡µ -->
        <div class="pagination" v-if="patienttotal > 0">
          <el-pagination
            background
            layout="total, sizes, prev, pager, next, jumper"
            :current-page="patientqueryParams.pn"
            :page-size="patientqueryParams.ps"
            :page-sizes="[10, 20, 30]"
            :total="patienttotal"
            @size-change="handleSizeChange"
            @current-change="handlePageChange"
          />
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import { selectTimelyRate } from "@/api/system/user";
export default {
  name: 'SeedetailsDialog',
  dicts: ['sys_yujing', 'sys_suggest'],
  props: {
    rowData: {
      type: Object,
      default: () => ({})
    },
    queryParams: {
      type: Object,
      default: () => ({})
    }
  },
  data() {
    return {
      // æŸ¥è¯¢å‚æ•°
      patientqueryParams: {
        pn: 1,
        ps: 10,
        name: '',
        leavediagname: ''
      },
      // åŠ è½½çŠ¶æ€
      loading: false,
      // æ‚£è€…列表
      logsheetlist: [],
      // æ€»æ¡æ•°
      patienttotal: 0
    };
  },
  mounted() {
    this.loadData();
  },
  methods: {
    // åŠ è½½æ•°æ®
    async loadData() {
      this.loading = true;
      try {
        const params = {
          ...this.patientqueryParams,
          deptcode: this.rowData.deptcode || '',
          starttime: this.queryParams.dateRange?.[0] ? this.parseTime(this.queryParams.dateRange[0]) : '',
          endtime: this.queryParams.dateRange?.[1] ? this.parseTime(this.queryParams.dateRange[1]) : ''
        };
        const response = await selectTimelyRate(params);
        this.logsheetlist = response.data?.detail || [];
        this.patienttotal = response.data?.total || 0;
      } catch (error) {
        console.error('获取未及时随访详情失败:', error);
        this.$message.error('获取数据失败');
      } finally {
        this.loading = false;
      }
    },
    // å¤„理搜索
    handleSearch() {
      this.patientqueryParams.pn = 1;
      this.loadData();
    },
    // å¤„理重置
    handleReset() {
      this.patientqueryParams = {
        pn: 1,
        ps: 10,
        name: '',
        leavediagname: ''
      };
      this.loadData();
    },
    // å¤„理分页大小变化
    handleSizeChange(size) {
      this.patientqueryParams.ps = size;
      this.patientqueryParams.pn = 1;
      this.loadData();
    },
    // å¤„理页码变化
    handlePageChange(page) {
      this.patientqueryParams.pn = page;
      this.loadData();
    },
    // æ ¼å¼åŒ–æ—¶é—´
    formatTime(time) {
      if (!time) return '-';
      return time;
    },
    // è§£æžæ—¶é—´
    parseTime(time) {
      if (!time) return '';
      return time;
    },
    // æŸ¥çœ‹è¯¦æƒ…
    handleViewDetail(row) {
      this.$emit('close');
      let type = "";
      if (row.preachformson && row.preachformson.includes("3")) {
        type = 1;
      }
      setTimeout(() => {
        this.$router.push({
          path: "/followvisit/record/detailpage/",
          query: {
            taskid: row.taskid,
            patid: row.patid,
            id: row.id,
            Voicetype: type
          }
        });
      }, 300);
    }
  }
};
</script>
<style lang="scss" scoped>
.seedetails-dialog {
  .detail-query-form {
    margin-bottom: 20px;
    padding-bottom: 20px;
    border-bottom: 1px solid #f0f0f0;
    ::v-deep .el-form-item {
      margin-bottom: 0;
      margin-right: 20px;
    }
  }
  .patient-table {
    margin-bottom: 20px;
    ::v-deep .el-table__header-wrapper th {
      background-color: #f8f9fa;
      font-weight: 600;
      color: #333;
    }
  }
  .pagination {
    display: flex;
    justify-content: flex-end;
    padding-top: 20px;
    border-top: 1px solid #f0f0f0;
  }
}
</style>
src/views/Satisfaction/sfstatistics/components/components/TopicDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,208 @@
<template>
  <div class="topic-dialog">
    <div class="topicdia">
      <div style="overflow-x: hidden; overflow-y: auto; max-height: 65vh">
        <!-- ä¿®æ”¹è¿™é‡Œï¼šä½¿ç”¨ processedTopicList è€Œä¸æ˜¯ topicList -->
        <div
          v-for="(item, index) in processedTopicList"
          :key="item.scriptid"
          class="ttaabbcc"
        >
          <div class="describe">
            ç¬¬{{ index + 1 }}题: {{ item.scriptContent }}
            <span>[{{ item.scriptType == 1 ? "单选题" : "多选题" }}]</span>
          </div>
          <div>
            <el-table :data="item.details" style="width: 100%">
              <el-table-column
                prop="optionText"
                label="问题选项"
                align="center"
                min-width="200"
              />
              <el-table-column
                prop="chosenQuantity"
                label="选择人数"
                align="center"
                min-width="120"
              >
                <template slot-scope="{ row }">
                  {{ row.chosenQuantity || 0 }}
                </template>
              </el-table-column>
              <el-table-column
                prop="chosenPercentage"
                label="比例"
                align="center"
                min-width="120"
              >
                <template slot-scope="{ row }">
                  <span
                    v-if="
                      row.chosenPercentage !== null &&
                      row.chosenPercentage !== undefined
                    "
                  >
                    {{ (Number(row.chosenPercentage) * 100).toFixed(2) }}%
                  </span>
                  <span v-else>-</span>
                </template>
              </el-table-column>
            </el-table>
          </div>
        </div>
      </div>
    </div>
    <!-- å¦‚果没有数据 -->
    <div
      v-if="!processedTopicList.length"
      class="no-data"
      style="text-align: center; padding: 50px 0"
    >
      <el-empty description="暂无数据"></el-empty>
    </div>
    <div
      slot="footer"
      class="dialog-footer"
      style="text-align: center; padding-top: 20px"
    >
      <el-button @click="handleClose">关 é—­</el-button>
    </div>
  </div>
</template>
<script>
export default {
  name: "TopicDialog",
  props: {
    rowData: {
      type: Object,
      default: () => ({}),
    },
    queryParams: {
      type: Object,
      default: () => ({}),
    },
    topicList: {
      type: [Array, Object],
      default: () => ({}),
    },
  },
  data() {
    return {
      processedTopicList: [], // å¤„理后的数据
    };
  },
  watch: {
    // ç›‘听父组件传递的数据变化
    topicList: {
      immediate: true,
      handler(newVal) {
        console.log("TopicDialog接收到父组件数据:", newVal);
        this.processTopicList(newVal);
      },
    },
  },
  mounted() {
    console.log("TopicDialog mounted, props:", this.$props);
  },
  methods: {
    // å¤„理topicList数据
    processTopicList(data) {
      console.log("开始处理数据:", data);
      if (!data || typeof data !== "object") {
        this.processedTopicList = [];
        return;
      }
      // å°†å¯¹è±¡è½¬æ¢ä¸ºæ•°ç»„
      const result = [];
      Object.keys(data).forEach((key) => {
        const item = data[key];
        if (item && item.scriptContent) {
          // æ·±æ‹·è´item,避免修改原数据
          const processedItem = JSON.parse(JSON.stringify(item));
          // è¿‡æ»¤details,只保留有选项文本的
          if (processedItem.details && Array.isArray(processedItem.details)) {
            processedItem.details = processedItem.details.filter(
              (detail) => detail && detail.optionText
            );
          }
          result.push(processedItem);
        }
      });
      console.log("处理后的数据:", result);
      this.processedTopicList = result;
    },
    // æ ¼å¼åŒ–百分比
    formatPercent(value) {
      if (value === null || value === undefined) return "-";
      const num = parseFloat(value);
      if (isNaN(num)) return "-";
      return `${num.toFixed(2)}%`; // æ³¨æ„ï¼šä½ çš„æ•°æ®ä¸­ç™¾åˆ†æ¯”已经是0-100的形式
    },
    // å…³é—­å¯¹è¯æ¡†
    handleClose() {
      this.$emit("close");
    },
  },
};
</script>
<style lang="scss" scoped>
.topic-dialog {
  .topicdia {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
      "Helvetica Neue", Arial, sans-serif;
    color: #333;
  }
  .ttaabbcc {
    background: #fafafa;
    border-radius: 6px;
    padding: 16px;
    margin-bottom: 20px;
    border-left: 4px solid #4794c5;
  }
  .describe {
    font-size: 15px;
    line-height: 1.6;
    margin-bottom: 12px;
    color: #1f2d3d;
  }
  .describe span {
    font-size: 13px;
    color: #999;
    font-style: italic;
    margin-left: 8px;
  }
  ::v-deep .el-table {
    border-radius: 4px;
    overflow: hidden;
    font-size: 14px;
  }
  ::v-deep .el-table th {
    background-color: #f1f5f9;
    color: #333;
    font-weight: 600;
  }
  ::v-deep .el-table td {
    border-bottom: 1px solid #f0f0f0;
    padding: 12px 0;
  }
}
</style>
src/views/Satisfaction/sfstatistics/components/visitStatistics.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1030 @@
<template>
  <div class="followup-statistics">
    <div class="query-section">
      <el-form
        :model="queryParams"
        ref="queryForm"
        size="medium"
        :inline="true"
        label-width="100px"
        class="query-form"
      >
        <el-form-item label="统计类型" prop="statisticaltype">
          <el-select
            v-model="queryParams.statisticaltype"
            placeholder="请选择统计类型"
            clearable
            @change="handleStatisticalTypeChange"
          >
            <el-option
              v-for="item in Statisticallist"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
        <!-- ç—…区选择 -->
        <el-form-item
          v-if="queryParams.statisticaltype == 1"
          label="病区"
          prop="leavehospitaldistrictcodes"
        >
          <el-select
            v-model="queryParams.leavehospitaldistrictcodes"
            placeholder="请选择病区"
            multiple
            collapse-tags
            filterable
            clearable
            style="width: 300px"
          >
            <el-option
              v-for="item in flatArrayhospit"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
        <!-- ç§‘室选择 -->
        <el-form-item
          v-if="queryParams.statisticaltype == 2"
          label="科室"
          prop="deptcodes"
        >
          <el-select
            v-model="queryParams.deptcodes"
            placeholder="请选择科室"
            multiple
            collapse-tags
            filterable
            clearable
            style="width: 300px"
          >
            <el-option
              v-for="item in flatArraydept"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="服务类型" prop="serviceType">
          <el-select
            v-model="queryParams.serviceType"
            placeholder="请选择服务类型"
            multiple
            collapse-tags
            clearable
            style="width: 300px"
          >
            <el-option
              v-for="item in options"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="随访时间" prop="dateRange">
          <el-date-picker
            v-model="queryParams.dateRange"
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            value-format="yyyy-MM-dd"
            :picker-options="pickerOptions"
            style="width: 380px"
          />
        </el-form-item>
        <el-form-item>
          <el-button
            type="primary"
            icon="el-icon-search"
            @click="handleQuery"
            :loading="loading"
          >
            æœç´¢
          </el-button>
          <el-button icon="el-icon-refresh" @click="resetQuery">
            é‡ç½®
          </el-button>
          <el-button
            type="warning"
            icon="el-icon-download"
            @click="handleExport"
            :disabled="!userList.length"
          >
            å¯¼å‡º
          </el-button>
        </el-form-item>
      </el-form>
    </div>
    <div class="table-section">
      <el-table
        v-loading="loading"
        :data="userList"
        :border="true"
        style="width: 100%"
        @selection-change="handleSelectionChange"
        :row-key="getRowKey"
      >
        <!-- ç—…区列 -->
        <el-table-column
          v-if="queryParams.statisticaltype == 1"
          label="出院病区"
          align="center"
          sortable
          key="leavehospitaldistrictname"
          prop="leavehospitaldistrictname"
          :show-overflow-tooltip="true"
          :sort-method="sortChineseNumber"
          min-width="120"
        />
        <!-- ç§‘室列 -->
        <el-table-column
          v-if="queryParams.statisticaltype == 2"
          label="科室"
          align="center"
          key="deptname"
          prop="deptname"
          :show-overflow-tooltip="true"
          min-width="120"
        />
        <el-table-column
          label="出院人次"
          align="center"
          key="dischargeCount"
          prop="dischargeCount"
          min-width="100"
        />
        <el-table-column
          label="无需随访人次"
          align="center"
          key="nonFollowUp"
          prop="nonFollowUp"
          min-width="120"
        />
        <el-table-column
          label="应随访人次"
          align="center"
          key="followUpNeeded"
          prop="followUpNeeded"
          min-width="120"
        />
        <el-table-column
          label="随访率"
          align="center"
          key="followUpRate"
          prop="followUpRate"
          min-width="100"
        >
          <template slot-scope="scope">
            <span
              v-if="
                scope.row.followUpRate !== null &&
                scope.row.followUpRate !== undefined
              "
            >
              {{ scope.row.followUpRate }}
            </span>
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column
          label="及时率"
          align="center"
          key="rate"
          prop="rate"
          min-width="100"
        >
          <template slot-scope="scope">
            <el-button
              v-if="scope.row.rate !== null && scope.row.rate !== undefined"
              type="text"
              @click="handleSeedetails(scope.row)"
            >
              {{ formatPercent(scope.row.rate) }}
            </el-button>
            <span v-else style="color: #909399">-</span>
          </template>
        </el-table-column>
        <el-table-column
          label="复诊通知题目总量"
          align="center"
          key="joyAllCount"
          prop="joyAllCount"
          min-width="140"
        />
        <el-table-column
          label="复诊通知填报量"
          align="center"
          key="joyCount"
          prop="joyCount"
          min-width="120"
        />
        <el-table-column
          label="完成比率"
          align="center"
          key="joyTotal"
          prop="joyTotal"
          min-width="100"
        >
          <template slot-scope="scope">
            <span
              v-if="
                scope.row.joyTotal !== null && scope.row.joyTotal !== undefined
              "
            >
              {{ formatPercent(scope.row.joyTotal) }}
            </span>
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column label="操作" align="center" fixed="right" width="120">
          <template slot-scope="scope">
            <el-button type="text" @click="getinfo(scope.row)">
              <i class="el-icon-s-order" style="margin-right: 4px"></i>
              æŸ¥çœ‹è¯¦æƒ…
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <!-- åˆ†é¡µ -->
    <div class="pagination-section" v-if="total > 0">
      <el-pagination
        background
        layout="total, sizes, prev, pager, next, jumper"
        :current-page="queryParams.pageNum"
        :page-size="queryParams.pageSize"
        :page-sizes="[10, 20, 30, 50]"
        :total="total"
        @size-change="handleSizeChange"
        @current-change="handlePageChange"
      />
    </div>
    <!-- æœªåŠæ—¶éšè®¿è¯¦æƒ…对话框 -->
    <el-dialog
      title="未及时随访患者服务"
      :visible.sync="SeedetailsVisible"
      width="80%"
      :close-on-click-modal="false"
    >
      <SeedetailsDialog
        v-if="SeedetailsVisible"
        :row-data="currentRow"
        :query-params="queryParams"
        @close="SeedetailsVisible = false"
      />
    </el-dialog>
    <!-- å¤è¯Šé€šçŸ¥è¯¦æƒ…对话框 -->
    <el-dialog
      :visible.sync="topicVisible"
      width="60%"
      :close-on-click-modal="false"
    >
      <template #title>
        <div style="display: flex; align-items: center">
          <i
            class="el-icon-s-data"
            style="margin-right: 8px; color: #409eff"
          ></i>
          <span>{{ topicvalue.name }}</span>
          <span style="margin-left: 10px; color: #666; font-size: 14px"
            >复诊通知指标详情</span
          >
        </div>
      </template>
      <topic-dialog
        v-if="topicVisible"
        :row-data="currentRow"
        :topicList="topiclist"
        :query-params="queryParams"
        @close="topicVisible = false"
      />
    </el-dialog>
  </div>
</template>
<script>
import {
  getSfStatisticsJoy,
  getSfStatisticsJoyInfo,
  selectTimelyRate,
} from "@/api/system/user";
import ExcelJS from "exceljs";
import { saveAs } from "file-saver";
import SeedetailsDialog from "./components/SeedetailsDialog.vue";
import TopicDialog from "./components/TopicDialog.vue";
export default {
  name: "FollowupStatistics",
  components: {
    SeedetailsDialog,
    TopicDialog,
  },
  data() {
    return {
      // æŸ¥è¯¢å‚æ•°
      queryParams: {
        statisticaltype: 1,
        leavehospitaldistrictcodes: ["all"],
        deptcodes: [],
        serviceType: [2],
        dateRange: [],
        pageNum: 1,
        pageSize: 20,
      },
      // ç»Ÿè®¡ç±»åž‹åˆ—表
      Statisticallist: [
        { label: "病区统计", value: 1 },
        { label: "科室统计", value: 2 },
      ],
      // ç—…区列表
      flatArrayhospit: [],
      // ç§‘室列表
      flatArraydept: [],
      // æœåŠ¡ç±»åž‹é€‰é¡¹
      options: [],
      // è¡¨æ ¼æ•°æ®
      userList: [],
      // æ€»æ¡æ•°
      total: 0,
      // åŠ è½½çŠ¶æ€
      loading: false,
      // é€‰ä¸­çš„行
      ids: [],
      single: true,
      multiple: true,
      // å½“前操作的行
      currentRow: null,
      // å¯¹è¯æ¡†æ˜¾ç¤ºæŽ§åˆ¶
      SeedetailsVisible: false,
      topicVisible: false,
      // å¤è¯Šé€šçŸ¥è¯¦æƒ…数据
      topiclist: [],
      topicvalue: {
        name: "",
      },
      // æ—¥æœŸé€‰æ‹©å™¨é€‰é¡¹
      pickerOptions: {
        shortcuts: [
          {
            text: "最近一周",
            onClick(picker) {
              const end = new Date();
              const start = new Date();
              start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
              picker.$emit("pick", [start, end]);
            },
          },
          {
            text: "最近一个月",
            onClick(picker) {
              const end = new Date();
              const start = new Date();
              start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
              picker.$emit("pick", [start, end]);
            },
          },
          {
            text: "最近三个月",
            onClick(picker) {
              const end = new Date();
              const start = new Date();
              start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
              picker.$emit("pick", [start, end]);
            },
          },
        ],
        disabledDate(time) {
          return time.getTime() > Date.now();
        },
      },
    };
  },
  created() {
    this.initData();
  },
  methods: {
    // åˆå§‹åŒ–数据
    async initData() {
      await this.getDeptTree();
      await this.getList();
    },
    // èŽ·å–ç§‘å®¤æ ‘
    getDeptTree() {
      // èŽ·å–æœåŠ¡ç±»åž‹
      this.options = this.$store.getters.tasktypes || [];
      // èŽ·å–ç§‘å®¤åˆ—è¡¨
      this.flatArraydept = (this.$store.getters.belongDepts || []).map(
        (dept) => {
          return {
            label: dept.deptName,
            value: dept.deptCode,
          };
        }
      );
      // èŽ·å–ç—…åŒºåˆ—è¡¨
      this.flatArrayhospit = (this.$store.getters.belongWards || []).map(
        (ward) => {
          return {
            label: ward.districtName,
            value: ward.districtCode,
          };
        }
      );
      // æ·»åŠ å…¨éƒ¨é€‰é¡¹
      this.flatArraydept.push({ label: "全部", value: "all" });
      this.flatArrayhospit.push({ label: "全部", value: "all" });
    },
    // èŽ·å–ç»Ÿè®¡åˆ—è¡¨
    async getList() {
      this.loading = true;
      try {
        // å¤„理查询参数
        const params = {
          configKey: "returnVisitCount",
          ...this.queryParams,
        };
        // å¤„理日期范围
        if (
          this.queryParams.dateRange &&
          this.queryParams.dateRange.length === 2
        ) {
          params.startTime = this.queryParams.dateRange[0];
          params.endTime = this.queryParams.dateRange[1];
        }
        // å¤„理病区/科室选择
        if (params.statisticaltype == 1) {
          // ç—…区统计
          if (params.leavehospitaldistrictcodes.includes("all")) {
            // å¦‚果选择了"全部",则移除"all"值
            params.leavehospitaldistrictcodes =
              params.leavehospitaldistrictcodes.filter(
                (item) => item !== "all"
              );
            // å¦‚果需要传所有病区代码,可以从store中获取
            params.leavehospitaldistrictcodes = (
              this.$store.getters.belongWards || []
            ).map((ward) => ward.districtCode);
          }
        } else if (params.statisticaltype == 2) {
          // ç§‘室统计
          if (params.deptcodes.includes("all")) {
            // å¦‚果选择了"全部",则移除"all"值
            params.deptcodes = params.deptcodes.filter(
              (item) => item !== "all"
            );
            // å¦‚果需要传所有科室代码,可以从store中获取
            params.deptcodes = (this.$store.getters.belongDepts || []).map(
              (dept) => dept.deptCode
            );
          }
        }
        const response = await getSfStatisticsJoy(params);
        this.userList = this.customSort(response.data) || [];
        this.total = response.total || 0;
      } catch (error) {
        console.error("获取统计列表失败:", error);
        this.$message.error("获取数据失败");
      } finally {
        this.loading = false;
      }
    },
    sortChineseNumber(aRow, bRow) {
      const a = aRow.leavehospitaldistrictname;
      const b = bRow.leavehospitaldistrictname;
      // ä¸­æ–‡æ•°å­—到阿拉伯数字的映射(扩展到45)
      const chineseNumMap = {
        ä¸€: 1,
        äºŒ: 2,
        ä¸‰: 3,
        å››: 4,
        äº”: 5,
        å…­: 6,
        ä¸ƒ: 7,
        å…«: 8,
        ä¹: 9,
        å: 10,
        åä¸€: 11,
        åäºŒ: 12,
        åä¸‰: 13,
        åå››: 14,
        åäº”: 15,
        åå…­: 16,
        åä¸ƒ: 17,
        åå…«: 18,
        åä¹: 19,
        äºŒå: 20,
        äºŒåä¸€: 21,
        äºŒåäºŒ: 22,
        äºŒåä¸‰: 23,
        äºŒåå››: 24,
        äºŒåäº”: 25,
        äºŒåå…­: 26,
        äºŒåä¸ƒ: 27,
        äºŒåå…«: 28,
        äºŒåä¹: 29,
        ä¸‰å: 30,
        ä¸‰åä¸€: 31,
        ä¸‰åäºŒ: 32,
        ä¸‰åä¸‰: 33,
        ä¸‰åå››: 34,
        ä¸‰åäº”: 35,
        ä¸‰åå…­: 36,
        ä¸‰åä¸ƒ: 37,
        ä¸‰åå…«: 38,
        ä¸‰åä¹: 39,
        å››å: 40,
        å››åä¸€: 41,
        å››åäºŒ: 42,
        å››åä¸‰: 43,
        å››åå››: 44,
        å››åäº”: 45,
      };
      // æå–中文数字
      const getNumberFromText = (text) => {
        if (!text || typeof text !== "string") return -1;
        // åŒ¹é…ä¸­æ–‡æ•°å­—,支持一到四十五
        const match = text.match(/^([一二三四五六七八九十]+)/);
        if (match && match[1]) {
          const chineseNum = match[1];
          return chineseNumMap[chineseNum] !== undefined
            ? chineseNumMap[chineseNum]
            : -1;
        }
        // å¦‚果没有匹配到中文数字,尝试匹配阿拉伯数字
        const arabicMatch = text.match(/^(\d+)/);
        if (arabicMatch && arabicMatch[1]) {
          const num = parseInt(arabicMatch[1], 10);
          return num >= 1 && num <= 45 ? num : -1;
        }
        return -1;
      };
      const numA = getNumberFromText(a);
      const numB = getNumberFromText(b);
      // å¤„理无法解析的情况
      if (numA === -1 && numB === -1) {
        return (a || "").localeCompare(b || "");
      }
      if (numA === -1) return 1;
      if (numB === -1) return -1;
      return numA - numB;
    },
    customSort(data) {
      // å®šä¹‰æ‚¨æœŸæœ›çš„病区顺序(扩展到四十五)
      const order = [
        "一",
        "二",
        "三",
        "四",
        "五",
        "六",
        "七",
        "八",
        "九",
        "十",
        "十一",
        "十二",
        "十三",
        "十四",
        "十五",
        "十六",
        "十七",
        "十八",
        "十九",
        "二十",
        "二十一",
        "二十二",
        "二十三",
        "二十四",
        "二十五",
        "二十六",
        "二十七",
        "二十八",
        "二十九",
        "三十",
        "三十一",
        "三十二",
        "三十三",
        "三十四",
        "三十五",
        "三十六",
        "三十七",
        "三十八",
        "三十九",
        "四十",
        "四十一",
        "四十二",
        "四十三",
        "四十四",
        "四十五",
      ];
      return data.sort((a, b) => {
        // æå–病区名称中的中文数字部分
        const getIndex = (name) => {
          if (!name || typeof name !== "string") return -1;
          // åŒ¹é…ä¸­æ–‡æ•°å­—
          const chineseMatch = name.match(/^([一二三四五六七八九十]+)/);
          if (chineseMatch && chineseMatch[1]) {
            return order.indexOf(chineseMatch[1]);
          }
          // åŒ¹é…é˜¿æ‹‰ä¼¯æ•°å­—
          const arabicMatch = name.match(/^(\d+)/);
          if (arabicMatch && arabicMatch[1]) {
            const num = parseInt(arabicMatch[1], 10);
            if (num >= 1 && num <= 45) {
              return num - 1; // å› ä¸ºæ•°ç»„索引从0开始
            }
          }
          return -1;
        };
        const indexA = getIndex(a.leavehospitaldistrictname);
        const indexB = getIndex(b.leavehospitaldistrictname);
        // æŽ’序逻辑
        if (indexA === -1 && indexB === -1) {
          return (a.leavehospitaldistrictname || "").localeCompare(
            b.leavehospitaldistrictname || ""
          );
        }
        if (indexA === -1) return 1;
        if (indexB === -1) return -1;
        return indexA - indexB;
      });
    },
    // å¤„理统计类型变化
    handleStatisticalTypeChange(value) {
      if (value === 1) {
        this.queryParams.deptcodes = [];
      } else {
        this.queryParams.leavehospitaldistrictcodes = [];
      }
      this.queryParams.pageNum = 1;
      this.getList();
    },
    // å¤„理查询
    handleQuery() {
      this.queryParams.pageNum = 1;
      this.getList();
    },
    // é‡ç½®æŸ¥è¯¢
    resetQuery() {
      this.queryParams = {
        statisticaltype: 1,
        leavehospitaldistrictcodes: [],
        deptcodes: [],
        serviceType: [2],
        dateRange: [],
        pageNum: 1,
        pageSize: 20,
      };
      this.getList();
    },
    // å¤„理分页大小变化
    handleSizeChange(size) {
      this.queryParams.pageSize = size;
      this.queryParams.pageNum = 1;
      this.getList();
    },
    // å¤„理页码变化
    handlePageChange(page) {
      this.queryParams.pageNum = page;
      this.getList();
    },
    // å¤„理行选择
    handleSelectionChange(selection) {
      this.ids = selection.map((item) => item.id);
      this.single = selection.length !== 1;
      this.multiple = !selection.length;
    },
    // èŽ·å–è¡Œkey
    getRowKey(row) {
      return row.statisticaltype === 1
        ? row.leavehospitaldistrictcode
        : row.deptcode;
    },
    // æ ¼å¼åŒ–百分比
    formatPercent(value) {
      if (value === null || value === undefined) return "-";
      const num = parseFloat(value);
      if (isNaN(num)) return "-";
      return `${(num * 100).toFixed(2)}%`;
    },
    // æŸ¥çœ‹æœªåŠæ—¶éšè®¿è¯¦æƒ…
    handleSeedetails(row) {
      this.currentRow = row;
      this.SeedetailsVisible = true;
    },
    // æŸ¥çœ‹å¤è¯Šé€šçŸ¥è¯¦æƒ…
    async getinfo(row) {
      this.currentRow = row;
      try {
        // å¤„理查询参数
        const params = {
          configKey: "returnVisitCount",
          ...this.queryParams,
        };
        // å¤„理日期范围
        if (
          this.queryParams.dateRange &&
          this.queryParams.dateRange.length === 2
        ) {
          params.startTime = this.queryParams.dateRange[0];
          params.endTime = this.queryParams.dateRange[1];
        }
        if (this.queryParams.statisticaltype == 1) {
          this.topicvalue.name = row.leavehospitaldistrictname;
          params.leavehospitaldistrictcodes = [row.leavehospitaldistrictcode];
        } else {
          this.topicvalue.name = row.deptname;
          params.deptcodes = [row.deptcode];
        }
        const response = await getSfStatisticsJoyInfo(params);
        this.topiclist = response.data || [];
        console.log(this.topiclist);
        this.topicVisible = true;
      } catch (error) {
        console.error("获取复诊通知详情失败:", error);
        this.$message.error("获取详情失败");
      }
    },
    // å¯¼å‡ºæ•°æ®
    async handleExport() {
      if (!this.userList.length) {
        this.$message.warning("没有数据可导出");
        return;
      }
      try {
        this.loading = true;
        // æž„建日期范围字符串
        let dateRangeString = "";
        let sheetNameSuffix = "";
        if (
          this.queryParams.dateRange &&
          this.queryParams.dateRange.length === 2
        ) {
          const startDateFormatted = this.queryParams.dateRange[0];
          const endDateFormatted = this.queryParams.dateRange[1];
          dateRangeString = `${startDateFormatted}至${endDateFormatted}`;
          sheetNameSuffix = `${startDateFormatted}至${endDateFormatted}`;
        } else {
          const now = new Date();
          const currentMonth = now.getMonth() + 1;
          dateRangeString = `${currentMonth}月`;
          sheetNameSuffix = `${currentMonth}月`;
        }
        const excelName = `随访统计表_${dateRangeString}.xlsx`;
        const worksheetName = `随访统计_${sheetNameSuffix}`;
        // åˆ›å»ºExcel工作簿
        const workbook = new ExcelJS.Workbook();
        const worksheet = workbook.addWorksheet(worksheetName);
        // å®šä¹‰æ ·å¼
        const titleStyle = {
          font: { name: "微软雅黑", size: 16, bold: true },
          fill: {
            type: "pattern",
            pattern: "solid",
            fgColor: { argb: "FFE6F3FF" },
          },
          alignment: { vertical: "middle", horizontal: "center" },
          border: {
            top: { style: "thin", color: { argb: "FFD0D0D0" } },
            left: { style: "thin", color: { argb: "FFD0D0D0" } },
            bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
            right: { style: "thin", color: { argb: "FFD0D0D0" } },
          },
        };
        const headerStyle = {
          font: { name: "微软雅黑", size: 11, bold: true },
          fill: {
            type: "pattern",
            pattern: "solid",
            fgColor: { argb: "FFF5F7FA" },
          },
          alignment: { vertical: "middle", horizontal: "center" },
          border: {
            top: { style: "thin", color: { argb: "FFD0D0D0" } },
            left: { style: "thin", color: { argb: "FFD0D0D0" } },
            bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
            right: { style: "thin", color: { argb: "FFD0D0D0" } },
          },
        };
        const cellStyle = {
          font: { name: "宋体", size: 10 },
          alignment: { vertical: "middle", horizontal: "center" },
          border: {
            top: { style: "thin", color: { argb: "FFD0D0D0" } },
            left: { style: "thin", color: { argb: "FFD0D0D0" } },
            bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
            right: { style: "thin", color: { argb: "FFD0D0D0" } },
          },
        };
        // æ·»åŠ æ€»æ ‡é¢˜
        worksheet.mergeCells(1, 1, 1, 10);
        const titleCell = worksheet.getCell(1, 1);
        titleCell.value = `随访统计表(${sheetNameSuffix})`;
        titleCell.style = titleStyle;
        worksheet.getRow(1).height = 35;
        // æ·»åŠ è¡¨å¤´
        const headers = [
          this.queryParams.statisticaltype == 1 ? "出院病区" : "科室",
          "出院人次",
          "无需随访人次",
          "应随访人次",
          "随访率",
          "及时率",
          "复诊通知题目总量",
          "复诊通知填报量",
          "完成比率",
        ];
        const headerRow = worksheet.addRow(headers);
        headerRow.eachCell((cell) => {
          cell.style = headerStyle;
        });
        headerRow.height = 25;
        // æ·»åŠ æ•°æ®è¡Œ
        this.userList.forEach((item) => {
          const dataRow = worksheet.addRow([
            this.queryParams.statisticaltype == 1
              ? item.leavehospitaldistrictname
              : item.deptname,
            item.dischargeCount || 0,
            item.nonFollowUp || 0,
            item.followUpNeeded || 0,
            item.followUpRate || "0%",
            item.rate ? this.formatPercent(item.rate) : "0%",
            item.joyAllCount || 0,
            item.joyCount || 0,
            item.joyTotal ? this.formatPercent(item.joyTotal) : "0%",
          ]);
          dataRow.eachCell((cell) => {
            cell.style = cellStyle;
          });
          dataRow.height = 22;
        });
        // è®¾ç½®åˆ—宽
        worksheet.columns = [
          { width: 20 },
          { width: 12 },
          { width: 12 },
          { width: 12 },
          { width: 12 },
          { width: 12 },
          { width: 15 },
          { width: 15 },
          { width: 12 },
        ];
        // ç”Ÿæˆå¹¶ä¸‹è½½æ–‡ä»¶
        const buffer = await workbook.xlsx.writeBuffer();
        const blob = new Blob([buffer], {
          type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        });
        saveAs(blob, excelName);
        this.$message.success("导出成功");
      } catch (error) {
        console.error("导出失败:", error);
        this.$message.error(`导出失败: ${error.message}`);
      } finally {
        this.loading = false;
      }
    },
  },
};
</script>
<style lang="scss" scoped>
.followup-statistics {
  .query-section {
    background: #fff;
    padding: 20px;
    border-radius: 4px;
    margin-bottom: 20px;
    .query-form {
      display: flex;
      flex-wrap: wrap;
      ::v-deep .el-form-item {
        margin-bottom: 20px;
        &:not(:last-child) {
          margin-right: 20px;
        }
      }
    }
  }
  .table-section {
    background: #fff;
    padding: 20px;
    border-radius: 4px;
    margin-bottom: 20px;
    ::v-deep .el-table {
      th {
        background-color: #f8f9fa;
        font-weight: 600;
        color: #333;
      }
    }
  }
  .pagination-section {
    display: flex;
    justify-content: flex-end;
    background: #fff;
    padding: 20px;
    border-radius: 4px;
  }
}
</style>
src/views/Satisfaction/sfstatistics/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,63 @@
<!-- StatisticsMain.vue -->
<template>
  <div class="statistics-main">
    <el-tabs v-model="activeTab" @tab-click="handleTabChange">
      <el-tab-pane label="随访统计" name="followup">
        <followup-statistics
          v-if="activeTab === 'followup'"
          ref="followupRef"
        />
      </el-tab-pane>
      <el-tab-pane label="满意度统计" name="satisfaction">
        <satisfaction-statistics
          v-if="activeTab === 'satisfaction'"
          ref="satisfactionRef"
        />
      </el-tab-pane>
    </el-tabs>
  </div>
</template>
<script>
import FollowupStatistics from './components/FollowupStatistics.vue';
import SatisfactionStatistics from './components/SatisfactionStatistics.vue';
export default {
  name: 'StatisticsMain',
  components: {
    FollowupStatistics,
    SatisfactionStatistics
  },
  data() {
    return {
      activeTab: 'followup'
    };
  },
  methods: {
    handleTabChange(tab) {
      console.log('切换到:', tab.name);
    }
  }
};
</script>
<style lang="scss" scoped>
.statistics-main {
  padding: 20px;
  background: #fff;
  min-height: calc(100vh - 84px);
  ::v-deep .el-tabs__header {
    margin-bottom: 20px;
  }
  ::v-deep .el-tabs__item {
    font-size: 16px;
    font-weight: 500;
  }
  ::v-deep .el-tabs__nav-wrap::after {
    height: 1px;
  }
}
</style>
src/views/followvisit/Continue/ContinueFordetails.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2389 @@
<template>
  <!-- å»¶ç»­æŠ¤ç†é¡µé¢ -->
  <div class="ContinuityCarePage" id="app-container">
    <!-- ç¬¬ä¸€éƒ¨åˆ†ï¼šæ‚£è€…基础信息 -->
    <div class="patient-info-section">
      <div class="headline">
        <div>患者基础信息</div>
      </div>
      <div class="patient-info-form">
        <el-form
          ref="patientForm"
          :model="patientForm"
          :rules="patientRules"
          label-width="120px"
        >
          <el-row :gutter="20">
            <el-col :span="8">
              <el-form-item label="患者姓名" prop="name">
                <el-input
                  v-model="patientForm.name"
                  placeholder="请输入患者姓名"
                  maxlength="30"
                  clearable
                ></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="性别" prop="sex">
                <el-select
                  v-model="patientForm.sex"
                  placeholder="请选择"
                  clearable
                >
                  <el-option label="男" :value="1"></el-option>
                  <el-option label="女" :value="2"></el-option>
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="年龄" prop="age">
                <el-input
                  v-model="patientForm.age"
                  placeholder="请输入年龄"
                  maxlength="3"
                  clearable
                ></el-input>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="8">
              <el-form-item label="联系电话" prop="telcode">
                <el-input
                  v-model="patientForm.telcode"
                  placeholder="请输入联系电话"
                  maxlength="20"
                  clearable
                ></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="住院号" prop="hospitalNumber">
                <el-input
                  v-model="patientForm.hospitalNumber"
                  placeholder="请输入住院号"
                  maxlength="50"
                  clearable
                ></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="出院日期" prop="dischargeDate">
                <el-date-picker
                  v-model="patientForm.dischargeDate"
                  type="date"
                  placeholder="选择出院日期"
                  value-format="yyyy-MM-dd"
                  style="width: 100%"
                >
                </el-date-picker>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="诊断名称" prop="diagnosis">
                <el-input
                  v-model="patientForm.diagnosis"
                  placeholder="请输入诊断名称"
                  maxlength="100"
                  clearable
                ></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="责任护士" prop="nurseName">
                <el-input
                  v-model="patientForm.nurseName"
                  placeholder="请输入责任护士"
                  maxlength="50"
                  clearable
                ></el-input>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="24">
              <el-form-item label="居住地址" prop="address">
                <el-input
                  v-model="patientForm.address"
                  placeholder="请输入详细居住地址"
                  maxlength="200"
                  clearable
                ></el-input>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="亲属姓名" prop="relativeName">
                <el-input
                  v-model="patientForm.relativeName"
                  placeholder="请输入亲属姓名"
                  maxlength="30"
                  clearable
                ></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="亲属电话" prop="relativeTel">
                <el-input
                  v-model="patientForm.relativeTel"
                  placeholder="请输入亲属电话"
                  maxlength="20"
                  clearable
                ></el-input>
              </el-form-item>
            </el-col>
          </el-row>
          <!-- å»¶ç»­æŠ¤ç†æ±‡æ€»ä¿¡æ¯ -->
          <el-row :gutter="20" v-if="continuitySummary.continueCount > 0">
            <el-col :span="24">
              <el-form-item label="延续护理汇总">
                <div class="continuity-summary">
                  <div class="summary-item">
                    <span class="label">延续次数:</span>
                    <span class="value"
                      >{{ continuitySummary.continueCount }} æ¬¡</span
                    >
                  </div>
                  <div class="summary-item">
                    <span class="label">最新服务:</span>
                    <span class="value">{{
                      formatDisplayTime(continuitySummary.continueTimeNow)
                    }}</span>
                  </div>
                  <div class="summary-item">
                    <span class="label">下次服务:</span>
                    <span class="value">{{
                      formatDisplayTime(continuitySummary.continueTimeNext)
                    }}</span>
                  </div>
                </div>
              </el-form-item>
            </el-col>
          </el-row>
          <!-- æ“ä½œæŒ‰é’® -->
          <el-form-item>
            <el-button
              type="primary"
              @click="savePatientInfo"
              :loading="savingPatientInfo"
            >
              ä¿å­˜æ‚£è€…信息
            </el-button>
            <el-button @click="resetPatientInfo">重置</el-button>
          </el-form-item>
        </el-form>
      </div>
    </div>
    <!-- ç¬¬äºŒéƒ¨åˆ†ï¼šæœåŠ¡åŸºç¡€ä¿¡æ¯ -->
    <div class="basic-info-section">
      <div class="headline">
        <div>服务基础信息</div>
      </div>
      <div class="basic-info-container">
        <!-- å·¦åŠéƒ¨åˆ†ï¼šå½“前服务随访内容(只读) -->
        <div class="followup-content readonly-content">
          <div class="sub-headline">
            <i class="el-icon-document"></i> å½“前服务随访内容(只读)
            <el-button
              type="text"
              size="small"
              @click="toggleQuestionSelection"
              style="margin-left: 10px"
            >
              {{ showQuestionSelector ? "隐藏问题选取" : "选取延续问题" }}
            </el-button>
          </div>
          <!-- é—®é¢˜é€‰å–面板 -->
          <div v-if="showQuestionSelector" class="question-selector-panel">
            <div class="selector-header">
              <span>请选择延续问题(可多选):</span>
              <el-button
                type="primary"
                size="small"
                @click="confirmQuestionSelection"
              >
                ç¡®è®¤é€‰å–
              </el-button>
            </div>
            <div class="question-list">
              <el-checkbox-group v-model="selectedQuestionIds">
                <div
                  v-for="(question, index) in availableQuestions"
                  :key="question.id"
                  class="question-item"
                >
                  <el-checkbox :label="question.id">
                    <div class="question-content">
                      <span class="question-index"
                        >{{ question.index + 1 }}.</span
                      >
                      <span class="question-text">{{ question.text }}</span>
                    </div>
                  </el-checkbox>
                </div>
              </el-checkbox-group>
            </div>
          </div>
          <div class="content-container">
            <el-tabs v-model="activeName" type="border-card">
              <el-tab-pane name="wj">
                <span slot="label"
                  ><i class="el-icon-notebook-1"></i> é—®å·éšè®¿ç»“æžœ</span
                >
                <div class="CONTENT">
                  <div class="title">{{ taskname ? taskname : "问卷" }}</div>
                  <div class="preview-left" v-if="!Voicetype">
                    <div
                      class="topic-dev"
                      v-for="(item, index) in tableDatatop"
                      :key="item.id"
                    >
                      <!-- å•选 -->
                      <div
                        :class="getTopicClass(item)"
                        :key="index"
                        v-if="item.scriptType == 1 && !item.astrict"
                      >
                        <div class="dev-text">
                          {{ index + 1 }}、[单选]<span>{{
                            item.scriptContent
                          }}</span>
                        </div>
                        <div class="dev-xx">
                          <el-radio-group v-model="item.scriptResult" disabled>
                            <el-radio
                              v-for="(
                                items, indexs
                              ) in item.svyTaskTemplateTargetoptions"
                              :class="getOptionClass(items)"
                              :key="indexs"
                              :label="items.optioncontent"
                              >{{ items.optioncontent }}</el-radio
                            >
                          </el-radio-group>
                        </div>
                        <div
                          v-if="item.showAppendInput || item.answerps"
                          class="append-input-container"
                        >
                          <el-input
                            type="textarea"
                            :rows="2"
                            placeholder="请输入具体信息"
                            v-model="item.answerps"
                            readonly
                          ></el-input>
                        </div>
                        <div v-show="item.prompt">
                          <el-alert :title="item.prompt" type="warning">
                          </el-alert>
                        </div>
                      </div>
                      <!-- å¤šé€‰ -->
                      <div
                        :class="
                          item.isabnormal
                            ? 'scriptTopic-isabnormal'
                            : 'scriptTopic-dev'
                        "
                        :key="index"
                        v-if="item.scriptType == 2 && !item.astrict"
                      >
                        <div class="dev-text">
                          {{ index + 1 }}、[多选]<span>{{
                            item.scriptContent
                          }}</span>
                        </div>
                        <div class="dev-xx">
                          <el-checkbox-group
                            v-model="item.scriptResult"
                            disabled
                          >
                            <el-checkbox
                              :class="items.isabnormal ? 'red-star' : ''"
                              v-for="(
                                items, indexs
                              ) in item.svyTaskTemplateTargetoptions"
                              :key="indexs"
                              :label="items.optioncontent"
                            >
                              {{ items.optioncontent }}
                            </el-checkbox>
                          </el-checkbox-group>
                        </div>
                        <div v-show="item.prompt && item.scriptResult[0]">
                          <el-alert :title="item.prompt" type="warning">
                          </el-alert>
                        </div>
                      </div>
                      <!-- å¡«ç©º -->
                      <div
                        class="scriptTopic-dev"
                        :key="index"
                        v-if="item.scriptType == 4 && !item.astrict"
                      >
                        <div class="dev-text">
                          {{ index + 1 }}、[问答]<span>{{
                            item.scriptContent
                          }}</span>
                          <span v-if="item.valueType == 3">(只能输入数字)</span>
                        </div>
                        <div class="dev-xx" v-if="item.valueType == 3">
                          <el-input
                            type="text"
                            placeholder="请输入答案"
                            v-model="item.scriptResult"
                            readonly
                          >
                          </el-input>
                        </div>
                        <div class="dev-xx" v-else>
                          <el-input
                            type="textarea"
                            :rows="2"
                            placeholder="请输入答案"
                            v-model="item.scriptResult"
                            readonly
                          >
                          </el-input>
                        </div>
                      </div>
                    </div>
                  </div>
                  <div class="preview-left" v-else>
                    <div
                      class="topic-dev"
                      v-for="(item, index) in tableDatatop"
                      :key="item.id"
                    >
                      <div v-if="item.targetvalue">
                        <div class="dev-text">
                          {{ index + 1 }}、[单选]<span>{{
                            item.questiontext
                          }}</span>
                        </div>
                        <div class="dev-xx">
                          <el-radio-group v-model="item.matchedtext" disabled>
                            <el-radio
                              v-for="(items, index) in item.scriptResult"
                              :key="index"
                              :label="items"
                              :class="items.isabnormal ? 'red-star' : ''"
                              >{{ items }}</el-radio
                            >
                          </el-radio-group>
                        </div>
                        <div v-show="item.prompt">
                          <el-alert :title="item.prompt" type="warning">
                          </el-alert>
                        </div>
                      </div>
                      <div class="scriptTopic-dev" :key="index" v-else>
                        <div class="dev-text">
                          {{ index + 1 }}、[问答]<span>{{
                            item.scriptContent
                          }}</span>
                          <span v-if="item.valueType == 3">(只能输入数字)</span>
                        </div>
                        <div class="dev-xx" v-if="item.valueType == 3">
                          <el-input
                            type="text"
                            placeholder="请输入答案"
                            v-model="item.scriptResult"
                            readonly
                          >
                          </el-input>
                        </div>
                        <div class="dev-xx" v-else>
                          <el-input
                            type="textarea"
                            :rows="2"
                            placeholder="请输入答案"
                            v-model="item.scriptResult"
                            readonly
                          >
                          </el-input>
                        </div>
                      </div>
                    </div>
                  </div>
                </div>
              </el-tab-pane>
              <el-tab-pane name="yy">
                <span slot="label"
                  ><i class="el-icon-headset"></i> è¯­éŸ³éšè®¿è¯¦æƒ…</span
                >
                <div class="borderdiv">
                  <div class="title">{{ taskname ? taskname : "问卷" }}</div>
                  <div class="voice-audio">
                    å®Œæ•´è¯­éŸ³ï¼š
                    <mini-audio
                      :audio-source="
                        voice ? voice : '@assets/order/example.mp3'
                      "
                    ></mini-audio>
                  </div>
                  <div class="preview-left">
                    <div v-for="item in voiceDatatop">
                      <div class="leftside">
                        <i class="el-icon-phone-outline"></i
                        ><span>{{ item.questiontext }}</span>
                      </div>
                      <div class="offside">
                        <i class="el-icon-user"></i>
                        <div class="offside-value">
                          <el-input
                            type="textarea"
                            :autosize="{ minRows: 1 }"
                            v-model="item.asrtext"
                            readonly
                          ></el-input>
                          <div>
                            <mini-audio
                              :audio-source="
                                item.questionvoice
                                  ? item.questionvoice
                                  : '@assets/order/example.mp3'
                              "
                            ></mini-audio>
                          </div>
                        </div>
                      </div>
                    </div>
                  </div>
                </div>
              </el-tab-pane>
            </el-tabs>
          </div>
        </div>
        <!-- å³åŠéƒ¨åˆ†ï¼šåŽ†æ¬¡å»¶ç»­æŠ¤ç†æœåŠ¡ -->
        <!-- å³åŠéƒ¨åˆ†ï¼šåŽ†æ¬¡å»¶ç»­æŠ¤ç†æœåŠ¡ -->
        <div class="continuity-history">
          <div class="sub-headline">
            <i class="el-icon-time"></i> åŽ†æ¬¡å»¶ç»­æŠ¤ç†æœåŠ¡
            <el-button
              type="primary"
              size="small"
              icon="el-icon-plus"
              @click="addContinuityTab"
              style="margin-left: 10px"
              >新增延续护理</el-button
            >
          </div>
          <div class="history-content">
            <el-tabs
              v-model="activeContinuityTab"
              type="card"
              closable
              @tab-remove="removeContinuityTab"
              @tab-click="handleTabClick"
            >
              <el-tab-pane
                v-for="(item, index) in continuityTabs"
                :key="item.name"
                :label="item.title"
                :name="item.name"
              >
                <div class="continuity-form">
                  <el-form
                    :ref="'continuityForm' + index"
                    :model="item.form"
                    :rules="continuityRules"
                    label-width="120px"
                  >
                    <!-- å»¶ç»­é—®é¢˜è¡¨å• -->
                    <el-form-item label="延续问题" prop="continuityProblems">
                      <div class="continuity-problems-form">
                        <div class="problems-header">
                          <span>已选取的延续问题:</span>
                          <el-button
                            type="text"
                            size="small"
                            icon="el-icon-plus"
                            @click="addContinuityProblem(index)"
                          >
                            æ–°å¢žé—®é¢˜
                          </el-button>
                        </div>
                        <!-- å·²é€‰å–的问题列表 -->
                        <div
                          v-if="
                            item.form.continuityProblems &&
                            item.form.continuityProblems.length > 0
                          "
                          class="problems-list-container"
                        >
                          <div
                            v-for="(problem, problemIndex) in item.form
                              .continuityProblems"
                            :key="problemIndex"
                            class="problem-item"
                          >
                            <div class="problem-content">
                              <div class="problem-meta">
                                <span class="problem-index"
                                  >问题 {{ problemIndex + 1 }}</span
                                >
                                <el-select
                                  v-model="problem.questionId"
                                  placeholder="选择问题"
                                  size="small"
                                  style="width: 300px; margin: 0 10px"
                                  @change="
                                    handleProblemChange(
                                      index,
                                      problemIndex,
                                      $event
                                    )
                                  "
                                >
                                  <el-option
                                    v-for="q in availableQuestions"
                                    :key="q.id"
                                    :label="q.text"
                                    :value="q.id"
                                  >
                                    <span
                                      >{{ q.index + 1 }}.
                                      {{ truncateText(q.text, 40) }}</span
                                    >
                                  </el-option>
                                </el-select>
                                <!-- ä¿®å¤ï¼šä¼ é€’正确的索引 -->
                                <el-button
                                  type="danger"
                                  icon="el-icon-delete"
                                  size="mini"
                                  circle
                                  @click="
                                    removeContinuityProblem(index, problemIndex)
                                  "
                                >
                                </el-button>
                              </div>
                              <!-- é—®å·å¼é—®é¢˜å±•示区域 -->
                              <div
                                v-if="problem.questionId"
                                class="question-display-area"
                              >
                                <!-- æ ¹æ®é—®é¢˜ç±»åž‹åŠ¨æ€æ¸²æŸ“ -->
                                <div
                                  v-if="
                                    getQuestionOriginalData(problem.questionId)
                                  "
                                  class="question-render"
                                >
                                  <!-- å•选类型 -->
                                  <div
                                    v-if="
                                      getQuestionOriginalData(
                                        problem.questionId
                                      ).scriptType == 1
                                    "
                                    class="question-item-render"
                                  >
                                    <div class="question-text">
                                      <strong>[单选]</strong>
                                      <span>{{
                                        getQuestionOriginalData(
                                          problem.questionId
                                        ).scriptContent
                                      }}</span>
                                    </div>
                                    <div class="question-options">
                                      <el-radio-group
                                        v-model="problem.selectedOption"
                                      >
                                        <el-radio
                                          v-for="option in getQuestionOriginalData(
                                            problem.questionId
                                          ).svyTaskTemplateTargetoptions"
                                          :key="option.optioncontent"
                                          :label="option.optioncontent"
                                          :class="getOptionClass(option)"
                                        >
                                          {{ option.optioncontent }}
                                        </el-radio>
                                      </el-radio-group>
                                    </div>
                                    <!-- é™„加输入框 -->
                                    <div
                                      v-if="problem.showAppendInput"
                                      class="append-input"
                                      style="margin-top: 10px"
                                    >
                                      <el-input
                                        type="textarea"
                                        :rows="2"
                                        placeholder="请输入具体信息"
                                        v-model="problem.appendInput"
                                      ></el-input>
                                    </div>
                                  </div>
                                  <!-- å¤šé€‰ç±»åž‹ -->
                                  <div
                                    v-else-if="
                                      getQuestionOriginalData(
                                        problem.questionId
                                      ).scriptType == 2
                                    "
                                    class="question-item-render"
                                  >
                                    <div class="question-text">
                                      <strong>[多选]</strong>
                                      <span>{{
                                        getQuestionOriginalData(
                                          problem.questionId
                                        ).scriptContent
                                      }}</span>
                                    </div>
                                    <div class="question-options">
                                      <el-checkbox-group
                                        v-model="problem.selectedOptions"
                                      >
                                        <el-checkbox
                                          v-for="option in getQuestionOriginalData(
                                            problem.questionId
                                          ).svyTaskTemplateTargetoptions"
                                          :key="option.optioncontent"
                                          :label="option.optioncontent"
                                          :class="
                                            option.isabnormal ? 'red-star' : ''
                                          "
                                        >
                                          {{ option.optioncontent }}
                                        </el-checkbox>
                                      </el-checkbox-group>
                                    </div>
                                  </div>
                                  <!-- å¡«ç©º/问答类型 -->
                                  <div
                                    v-else-if="
                                      getQuestionOriginalData(
                                        problem.questionId
                                      ).scriptType == 4
                                    "
                                    class="question-item-render"
                                  >
                                    <div class="question-text">
                                      <strong>[问答]</strong>
                                      <span>{{
                                        getQuestionOriginalData(
                                          problem.questionId
                                        ).scriptContent
                                      }}</span>
                                      <span
                                        v-if="
                                          getQuestionOriginalData(
                                            problem.questionId
                                          ).valueType == 3
                                        "
                                        >(只能输入数字)</span
                                      >
                                    </div>
                                    <div class="question-options">
                                      <el-input
                                        v-if="
                                          getQuestionOriginalData(
                                            problem.questionId
                                          ).valueType == 3
                                        "
                                        type="text"
                                        placeholder="请输入答案"
                                        v-model="problem.answer"
                                        style="width: 200px"
                                      ></el-input>
                                      <el-input
                                        v-else
                                        type="textarea"
                                        :rows="2"
                                        placeholder="请输入答案"
                                        v-model="problem.answer"
                                        style="width: 100%"
                                      ></el-input>
                                    </div>
                                  </div>
                                </div>
                                <div v-else class="no-question-data">
                                  é—®é¢˜æ•°æ®åŠ è½½ä¸­...
                                </div>
                              </div>
                              <!-- åŽŸæœ‰çš„é—®é¢˜æè¿°ç­‰å­—æ®µ -->
                              <div class="problem-detail">
                                <div class="detail-row">
                                  <span class="detail-label">当前状态:</span>
                                  <el-select
                                    v-model="problem.status"
                                    placeholder="选择状态"
                                    size="small"
                                    style="width: 120px; margin-right: 20px"
                                  >
                                    <el-option
                                      label="未处理"
                                      value="pending"
                                    ></el-option>
                                    <el-option
                                      label="处理中"
                                      value="processing"
                                    ></el-option>
                                    <el-option
                                      label="已解决"
                                      value="resolved"
                                    ></el-option>
                                    <el-option
                                      label="持续关注"
                                      value="watching"
                                    ></el-option>
                                  </el-select>
                                  <span
                                    class="detail-label"
                                    v-if="problem.status === 'resolved'"
                                    style="margin-left: 20px"
                                  >
                                    è§£å†³æ—¥æœŸï¼š
                                  </span>
                                  <el-date-picker
                                    v-if="problem.status === 'resolved'"
                                    v-model="problem.resolvedDate"
                                    type="date"
                                    placeholder="选择解决日期"
                                    value-format="yyyy-MM-dd"
                                    size="small"
                                    style="width: 150px; margin-left: 10px"
                                  ></el-date-picker>
                                </div>
                                <!-- <div class="detail-row">
                                  <span class="detail-label">问题描述:</span>
                                  <el-input
                                    type="textarea"
                                    :rows="2"
                                    v-model="problem.description"
                                    placeholder="请详细描述该延续问题(可记录评估结果、处理建议等)"
                                    size="small"
                                    style="flex: 1"
                                  ></el-input>
                                </div> -->
                              </div>
                            </div>
                          </div>
                        </div>
                      </div>
                    </el-form-item>
                    <!-- å»¶ç»­æ€§æŠ¤ç†è®°å½• -->
                    <el-form-item label="护理记录" prop="careRecord">
                      <el-input
                        type="textarea"
                        :rows="4"
                        v-model="item.form.careRecord"
                        placeholder="请输入延续性护理记录"
                        clearable
                      ></el-input>
                    </el-form-item>
                    <!-- è´£ä»»æŠ¤å£«ã€å›žè®¿å½¢å¼ã€æœåŠ¡æ—¶é—´ -->
                    <el-row :gutter="20">
                      <el-col :span="12">
                        <el-form-item label="责任护士" prop="dutyNurse">
                          <el-input
                            v-model="item.form.dutyNurse"
                            placeholder="请输入责任护士"
                            clearable
                          ></el-input>
                        </el-form-item>
                      </el-col>
                      <el-col :span="12">
                        <el-form-item label="回访形式" prop="visitType">
                          <el-select
                            v-model="item.form.visitType"
                            placeholder="请选择回访形式"
                            style="width: 100%"
                            clearable
                          >
                            <el-option
                              label="电话回访"
                              value="phone"
                            ></el-option>
                            <el-option
                              label="上门回访"
                              value="home"
                            ></el-option>
                            <el-option
                              label="门诊回访"
                              value="clinic"
                            ></el-option>
                            <el-option
                              label="微信回访"
                              value="wechat"
                            ></el-option>
                            <el-option label="其他" value="other"></el-option>
                          </el-select>
                        </el-form-item>
                      </el-col>
                    </el-row>
                    <el-row :gutter="20">
                      <el-col :span="12">
                        <el-form-item
                          label="下次服务时间"
                          prop="nextServiceTime"
                        >
                          <el-date-picker
                            v-model="item.form.nextServiceTime"
                            type="datetime"
                            placeholder="选择下次延续服务时间"
                            value-format="yyyy-MM-dd HH:mm:ss"
                            style="width: 100%"
                          >
                          </el-date-picker>
                          <div
                            class="time-tip"
                            v-if="item.form.nextServiceTime"
                          >
                            è·ä¸‹æ¬¡æœåŠ¡è¿˜æœ‰
                            {{ calculateDaysLeft(item.form.nextServiceTime) }}
                            å¤©
                          </div>
                        </el-form-item>
                      </el-col>
                      <el-col :span="12">
                        <el-form-item label="服务时间" prop="serviceTime">
                          <el-date-picker
                            v-model="item.form.serviceTime"
                            type="datetime"
                            placeholder="选择服务时间"
                            value-format="yyyy-MM-dd HH:mm:ss"
                            style="width: 100%"
                          >
                          </el-date-picker>
                        </el-form-item>
                      </el-col>
                    </el-row>
                    <!-- ä¸‹æ¬¡å»¶ç»­æœåŠ¡æ—¶é—´ -->
                    <!-- æ“ä½œæŒ‰é’® -->
                    <el-form-item>
                      <el-button
                        type="primary"
                        @click="saveContinuityTab(index)"
                        :loading="item.saving"
                      >
                        ä¿å­˜
                      </el-button>
                      <el-button @click="resetContinuityTab(index)">
                        é‡ç½®
                      </el-button>
                    </el-form-item>
                  </el-form>
                </div>
              </el-tab-pane>
            </el-tabs>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import {
  getsearchrResults,
  getPersonVoices,
  getTaskservelist,
  Editsingletaskson,
} from "@/api/AiCentre/index";
import {
  messagelistpatient,
  alterpatient,
  listcontactinformation,
} from "@/api/patient/homepage";
export default {
  dicts: ["sys_yujing"],
  data() {
    return {
      // è·¯ç”±å‚æ•°
      taskid: "",
      id: "",
      sendname: "",
      patid: "",
      Voicetype: 0,
      serviceType: "",
      // éšè®¿å†…容相关
      activeName: "wj",
      taskname: "",
      voice: "",
      tableDatatop: [],
      voiceDatatop: [],
      form: {},
      userform: {},
      // æ–°å¢žï¼šé—®é¢˜é€‰æ‹©ç›¸å…³
      showQuestionSelector: false,
      selectedQuestionIds: [],
      // æ‚£è€…基础信息表单
      patientForm: {
        name: "",
        sex: "",
        age: "",
        telcode: "",
        hospitalNumber: "",
        dischargeDate: "",
        diagnosis: "",
        nurseName: "",
        address: "",
        relativeName: "",
        relativeTel: "",
      },
      patientRules: {
        name: [{ required: true, message: "请输入患者姓名", trigger: "blur" }],
        age: [
          { required: true, message: "请输入年龄", trigger: "blur" },
          { pattern: /^\d+$/, message: "年龄必须为数字", trigger: "blur" },
        ],
        telcode: [
          { required: true, message: "请输入联系电话", trigger: "blur" },
          {
            pattern: /^1[3-9]\d{9}$/,
            message: "请输入正确的手机号码",
            trigger: "blur",
          },
        ],
      },
      savingPatientInfo: false,
      // å»¶ç»­æŠ¤ç†ç›¸å…³
      activeContinuityTab: "continuity-0",
      continuityTabs: [],
      continuityTabIndex: 0,
      continuityRules: {
        // ç§»é™¤continuityProblems的验证规则
        careRecord: [
          { required: true, message: "请输入护理记录", trigger: "blur" },
        ],
        dutyNurse: [
          { required: true, message: "请输入责任护士", trigger: "blur" },
        ],
        visitType: [
          { required: true, message: "请选择回访形式", trigger: "change" },
        ],
        serviceTime: [
          { required: true, message: "请选择服务时间", trigger: "change" },
        ],
        nextServiceTime: [
          { required: true, message: "请选择下次服务时间", trigger: "change" },
        ],
      },
      // å¯é€‰çš„延续问题(从左侧问卷中提取)
      availableQuestions: [],
      // å»¶ç»­æŠ¤ç†æ±‡æ€»ä¿¡æ¯
      continuitySummary: {
        continueCount: 0,
        continueTimeNow: "",
        continueTimeNext: "",
        continueContent: "",
      },
      // å…¶ä»–
      logsheetlist: [],
    };
  },
  created() {
    this.taskid = this.$route.query.taskid;
    this.id = this.$route.query.id;
    this.sendname = this.$route.query.sendname;
    this.patid = this.$route.query.patid;
    this.Voicetype = this.$route.query.Voicetype || 0;
    this.serviceType = this.$route.query.serviceType;
    this.getTaskservelist(this.id);
  },
  methods: {
    // èŽ·å–ä¸»é¢˜æ ·å¼ç±»
    getTopicClass(item) {
      console.log(item.isabnormal, "getTopicClass");
      if (item.isabnormal == 1) {
        return "scriptTopic-isabnormal";
      } else if (item.isabnormal == 2) {
        return "scriptTopic-warning";
      } else {
        return "scriptTopic-dev";
      }
    },
    // èŽ·å–é€‰é¡¹æ ·å¼ç±»
    getOptionClass(items) {
      if (items.isabnormal == 1) {
        return "red-star";
      } else if (items.isabnormal == 2) {
        return "yellow-star";
      }
      return "";
    },
    // èŽ·å–é—®å·æ•°æ®
    getsearchrResults(id) {
      getsearchrResults({
        taskid: this.taskid,
        patid: this.patid,
        subId: id ? id : this.id,
        isFinish: false,
      }).then((res) => {
        if (res.code === 200) {
          this.tableDatatop = res.data.scriptResult;
          this.tableDatatop.forEach((item) => {
            if (item.scriptType == 2) item.scriptResult = [];
            if (item.scriptResultId && item.scriptType != 2) {
              item.isoption = 3;
              item.scriptResult = item.scriptResult;
            } else if (item.scriptResultId && item.scriptType == 2) {
              item.scriptResult = item.scriptResult.split("&");
              item.isoption = 3;
            }
          });
          this.taskname = res.data.taskName;
          this.overdata();
          // æå–可选的延续问题
          this.extractAvailableQuestions();
        }
      });
    },
    overdata() {
      this.tableDatatop.forEach((item, index) => {
        var obj = item.svyTaskTemplateTargetoptions.find(
          (items) => items.optioncontent == item.scriptResult
        );
        if (obj) {
          console.log(obj, "obj");
          if (obj.isabnormal) {
            this.tableDatatop[index].isabnormal = obj.isabnormal;
          }
          this.$forceUpdate();
        }
      });
    },
    // æå–可选的延续问题
    extractAvailableQuestions() {
      this.availableQuestions = this.tableDatatop
        .filter(
          (item) => item.scriptContent && item.scriptContent.trim() !== ""
        )
        .map((item, index) => ({
          id: `question-${index}`,
          index: index,
          text: item.scriptContent,
          originalIndex: index,
        }));
    }, // åˆ‡æ¢é—®é¢˜é€‰æ‹©é¢æ¿æ˜¾ç¤º
    toggleQuestionSelection() {
      this.showQuestionSelector = !this.showQuestionSelector;
      if (this.showQuestionSelector) {
        // é‡ç½®é€‰æ‹©
        this.selectedQuestionIds = [];
      }
    },
    // æ·»åŠ å»¶ç»­é—®é¢˜
    addContinuityProblem(tabIndex) {
      this.continuityTabs[tabIndex].form.continuityProblems.push({
        questionId: "",
        status: "pending",
        description: "",
        resolvedDate: "",
        createTime: new Date().toISOString(),
        // æ–°å¢žå­—段,用于问卷式交互
        selectedOption: "", // å•选答案
        selectedOptions: [], // å¤šé€‰ç­”案
        answer: "", // å¡«ç©ºç­”案
        showAppendInput: false, // æ˜¯å¦æ˜¾ç¤ºé™„加输入框
        appendInput: "", // é™„加输入内容
      });
    },
    // æ ¹æ®é—®é¢˜ID获取原始问题数据
    getQuestionOriginalData(questionId) {
      if (!questionId) return null;
      const originalIndex = this.availableQuestions.find(
        (q) => q.id === questionId
      )?.originalIndex;
      if (originalIndex !== undefined && this.tableDatatop[originalIndex]) {
        return this.tableDatatop[originalIndex];
      }
      return null;
    },
    // ç§»é™¤å»¶ç»­é—®é¢˜
    removeContinuityProblem(tabIndex, problemIndex) {
      this.continuityTabs[tabIndex].form.continuityProblems.splice(
        problemIndex,
        1
      );
    },
    // å¤„理问题选择变更
    handleProblemChange(tabIndex, problemIndex, questionId) {
      const problem =
        this.continuityTabs[tabIndex].form.continuityProblems[problemIndex];
      if (!questionId) {
        problem.description = "";
        // æ¸…空答案字段
        problem.selectedOption = "";
        problem.selectedOptions = [];
        problem.answer = "";
        problem.showAppendInput = false;
        problem.appendInput = "";
        return;
      }
      // è‡ªåŠ¨å¡«å……é—®é¢˜æè¿°
      const question = this.availableQuestions.find((q) => q.id === questionId);
      if (question) {
        problem.description = `问题:${question.text}`;
      }
      // æ ¹æ®é—®é¢˜ç±»åž‹åˆå§‹åŒ–答案字段
      const originalData = this.getQuestionOriginalData(questionId);
      if (originalData) {
        // åˆå§‹åŒ–单选答案
        if (originalData.scriptType === 1) {
          problem.selectedOption = originalData.scriptResult || "";
          // æ£€æŸ¥æ˜¯å¦éœ€è¦æ˜¾ç¤ºé™„加输入框
          problem.showAppendInput = originalData.showAppendInput || false;
          problem.appendInput = originalData.answerps || "";
        }
        // åˆå§‹åŒ–多选答案
        else if (originalData.scriptType === 2) {
          problem.selectedOptions = Array.isArray(originalData.scriptResult)
            ? [...originalData.scriptResult]
            : originalData.scriptResult
            ? originalData.scriptResult.split("&")
            : [];
        }
        // åˆå§‹åŒ–填空答案
        else if (originalData.scriptType === 4) {
          problem.answer = originalData.scriptResult || "";
        }
      }
    },
    // æ ¹æ®é—®é¢˜ID获取问题文本
    getQuestionText(questionId) {
      if (!questionId) return "未选择问题";
      const question = this.availableQuestions.find((q) => q.id === questionId);
      return question
        ? `${question.index + 1}. ${question.text}`
        : "问题已删除";
    },
    // æˆªæ–­æ–‡æœ¬
    truncateText(text, length) {
      if (!text) return "";
      return text.length > length ? text.substring(0, length) + "..." : text;
    },
    // ç¡®è®¤é—®é¢˜é€‰æ‹©
    confirmQuestionSelection() {
      if (this.selectedQuestionIds.length === 0) {
        this.$modal.msgWarning("请至少选择一个延续问题");
        return;
      }
      // èŽ·å–å½“å‰æ¿€æ´»çš„æ ‡ç­¾é¡µç´¢å¼•
      const activeTabIndex = this.continuityTabs.findIndex(
        (tab) => tab.name === this.activeContinuityTab
      );
      if (activeTabIndex === -1) {
        this.$modal.msgError("未找到激活的延续护理标签页");
        return;
      }
      // æ·»åŠ é€‰ä¸­çš„é—®é¢˜åˆ°å½“å‰æ ‡ç­¾é¡µ
      this.selectedQuestionIds.forEach((questionId) => {
        // æ£€æŸ¥æ˜¯å¦å·²å­˜åœ¨ç›¸åŒé—®é¢˜
        const exists = this.continuityTabs[
          activeTabIndex
        ].form.continuityProblems.some(
          (problem) => problem.questionId === questionId
        );
        if (!exists) {
          this.continuityTabs[activeTabIndex].form.continuityProblems.push({
            questionId: questionId,
            status: "pending",
            description: "",
            resolvedDate: "",
            createTime: new Date().toISOString(),
          });
        }
      });
      // å…³é—­é€‰æ‹©é¢æ¿
      this.showQuestionSelector = false;
      this.selectedQuestionIds = [];
      this.$modal.msgSuccess(
        `已添加 ${this.selectedQuestionIds.length} ä¸ªå»¶ç»­é—®é¢˜`
      );
    },
    // ç§»é™¤å»¶ç»­é—®é¢˜
    removeContinuityProblem(tabIndex, problemIndex) {
      this.continuityTabs[tabIndex].form.continuityProblems.splice(
        problemIndex,
        1
      );
    },
    // èŽ·å–è¯­éŸ³æ•°æ®
    getPersonVoices(id) {
      let obj = {
        taskid: this.taskid,
        patid: this.patid,
        subId: id ? id : this.id,
      };
      getPersonVoices(obj).then((res) => {
        if (res.code == 200) {
          this.voiceDatatop = res.data.serviceSubtaskDetails;
          this.voice = res.data.voice;
          this.activeName = "yy";
          this.taskname = res.data.taskName;
          this.tableDatatop = res.data.filteredDetails;
          this.tableDatatop.forEach((item) => {
            if (item.targetvalue) {
              item.scriptResult = item.targetvalue.split("&");
            } else {
              item.scriptResult = [];
            }
          });
          // æå–可选的延续问题
          this.extractAvailableQuestions();
        }
      });
    },
    // èŽ·å–åŸºç¡€ä¿¡æ¯
    getuserinfo() {
      const queryParams = {
        pid: Number(this.patid),
        allhosp: "0",
      };
      messagelistpatient(queryParams).then((response) => {
        if (response.rows[0]) {
          this.userform = response.rows[0];
          this.initPatientForm();
        }
      });
      listcontactinformation({ patid: this.patid }).then((response) => {
        this.tableData = response.rows;
        if (this.tableData.length) {
          this.patientForm.relativeName = this.tableData[0].relation || "";
          this.patientForm.relativeTel = this.tableData[0].contactway || "";
        }
      });
    },
    // åˆå§‹åŒ–患者表单
    initPatientForm() {
      if (this.userform) {
        this.patientForm.name = this.userform.name || "";
        this.patientForm.sex = this.userform.sex || "";
        this.patientForm.age = this.userform.age || "";
        this.patientForm.telcode = this.userform.telcode || "";
        this.patientForm.address = this.userform.placeOfResidence || "";
        this.patientForm.hospitalNumber = this.userform.medicalRecordNo || "";
      }
      if (this.form) {
        this.patientForm.dischargeDate =
          this.formatTime(this.form.endtime) || "";
        this.patientForm.diagnosis = this.form.leavediagname || "";
        this.patientForm.nurseName = this.form.nurseName || "";
      }
    },
    // èŽ·å–æ‚£è€…è®°å½•
    getTaskservelist(id) {
      getTaskservelist({
        patid: this.patid,
        subId: id,
        pageSize: 100,
      }).then((res) => {
        if (res.code == 200) {
          this.form = res.rows[0].serviceSubtaskList.find(
            (item) => item.id == this.id
          );
          this.logsheetlist = res.rows[0].serviceSubtaskList;
          // åˆå§‹åŒ–患者信息
          this.initPatientForm();
          // åŠ è½½åŽ†å²å»¶ç»­æŠ¤ç†æ•°æ®
          this.loadContinuityHistory();
          this.getuserinfo();
        }
        if (this.Voicetype) {
          this.getPersonVoices();
        } else {
          this.getsearchrResults();
        }
      });
    },
    // åŠ è½½åŽ†å²å»¶ç»­æŠ¤ç†æ•°æ®
    loadContinuityHistory() {
      if (this.form && this.form.continueContent) {
        try {
          const historyData = JSON.parse(this.form.continueContent);
          this.continuityTabs = historyData.map((item, index) => ({
            name: `continuity-${index}`,
            title: `延续护理${index + 1}`,
            form: {
              continuityProblems: item.continuityProblems || [],
              careRecord: item.careRecord || "",
              dutyNurse: item.dutyNurse || "",
              visitType: item.visitType || "",
              serviceTime: item.serviceTime || "",
              nextServiceTime: item.nextServiceTime || "",
            },
            saving: false,
          }));
          if (this.continuityTabs.length > 0) {
            this.activeContinuityTab = this.continuityTabs[0].name;
          }
          // æ›´æ–°æ±‡æ€»ä¿¡æ¯
          this.updateContinuitySummary();
        } catch (error) {
          console.error("解析延续护理历史数据失败:", error);
        }
      }
    },
    // æ›´æ–°å»¶ç»­æŠ¤ç†æ±‡æ€»ä¿¡æ¯
    updateContinuitySummary() {
      if (this.form) {
        this.continuitySummary.continueCount = this.form.continueCount || 0;
        this.continuitySummary.continueTimeNow =
          this.form.continueTimeNow || "";
        this.continuitySummary.continueTimeNext =
          this.form.continueTimeNext || "";
        this.continuitySummary.continueContent =
          this.form.continueContent || "";
      }
    },
    // æ·»åŠ å»¶ç»­æŠ¤ç†æ ‡ç­¾é¡µ
    addContinuityTab() {
      console.log(this.continuityTabs.length);
      if (this.continuityTabs.length) {
        this.continuityTabIndex = this.continuityTabs.length;
      }
      const newIndex = this.continuityTabIndex + 1;
      const newTab = {
        name: `continuity-${newIndex}`,
        title: `延续护理${newIndex}`,
        form: {
          continuityProblems: [],
          careRecord: "",
          dutyNurse: "",
          visitType: "",
          serviceTime: "",
          nextServiceTime: "",
        },
        saving: false,
      };
      this.continuityTabs.push(newTab);
      this.continuityTabIndex = newIndex;
      this.activeContinuityTab = newTab.name;
    },
    // ç§»é™¤å»¶ç»­æŠ¤ç†æ ‡ç­¾é¡µ
    removeContinuityTab(targetName) {
      const tabs = this.continuityTabs;
      let activeName = this.activeContinuityTab;
      if (activeName === targetName) {
        tabs.forEach((tab, index) => {
          if (tab.name === targetName) {
            const nextTab = tabs[index + 1] || tabs[index - 1];
            if (nextTab) {
              activeName = nextTab.name;
            }
          }
        });
      }
      this.activeContinuityTab = activeName;
      this.continuityTabs = tabs.filter((tab) => tab.name !== targetName);
    },
    // å¤„理标签页点击
    handleTabClick(tab) {
      this.activeContinuityTab = tab.name;
    },
    // è®¡ç®—距离下次服务还有多少天
    calculateDaysLeft(nextServiceTime) {
      if (!nextServiceTime) return 0;
      const nextTime = new Date(nextServiceTime);
      const now = new Date();
      const diffTime = nextTime - now;
      const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
      return diffDays > 0 ? diffDays : 0;
    },
    // ä¿å­˜å»¶ç»­æŠ¤ç†æ ‡ç­¾é¡µ
    saveContinuityTab(index) {
      const formRef = this.$refs[`continuityForm${index}`];
      if (!formRef) return;
      // éªŒè¯åŸºæœ¬è¡¨å•
      formRef[0].validate((valid) => {
        if (valid) {
          // éªŒè¯å»¶ç»­é—®é¢˜æ˜¯å¦å®Œæ•´
          const problems = this.continuityTabs[index].form.continuityProblems;
          let hasError = false;
          problems.forEach((problem, problemIndex) => {
            if (!problem.questionId) {
              hasError = true;
              this.$modal.msgError(`第${problemIndex + 1}个延续问题未选择`);
            }
          });
          if (hasError) {
            return false;
          }
          this.continuityTabs[index].saving = true;
          // æ›´æ–°æ±‡æ€»ä¿¡æ¯
          this.updateContinuitySummaryFromTabs();
          // è°ƒç”¨ä¿å­˜API
          this.saveContinuityData();
        } else {
          this.$modal.msgError("请填写完整信息");
          return false;
        }
      });
    },
    // æ›´æ–°å»¶ç»­æŠ¤ç†æ±‡æ€»ä¿¡æ¯
    updateContinuitySummaryFromTabs() {
      if (this.continuityTabs.length === 0) return;
      // æ‰¾åˆ°æœ€æ–°çš„æœåŠ¡æ—¶é—´
      let latestTime = "";
      let nextTime = "";
      let totalProblems = 0;
      this.continuityTabs.forEach((tab) => {
        if (tab.form.serviceTime) {
          if (
            !latestTime ||
            new Date(tab.form.serviceTime) > new Date(latestTime)
          ) {
            latestTime = tab.form.serviceTime;
          }
        }
        if (tab.form.nextServiceTime) {
          if (
            !nextTime ||
            new Date(tab.form.nextServiceTime) < new Date(nextTime)
          ) {
            nextTime = tab.form.nextServiceTime;
          }
        }
        // ç»Ÿè®¡é—®é¢˜æ•°é‡
        totalProblems += tab.form.continuityProblems.length;
      });
      this.continuitySummary.continueCount = this.continuityTabs.length;
      this.continuitySummary.continueTimeNow = latestTime;
      this.continuitySummary.continueTimeNext = nextTime;
      // æž„建完整的延续护理数据
      const continuityData = this.continuityTabs.map((tab) => ({
        ...tab.form,
        tabTitle: tab.title,
        tabName: tab.name,
      }));
      this.continuitySummary.continueContent = JSON.stringify(continuityData);
      this.continuitySummary.totalProblems = totalProblems;
    },
    // ä¿å­˜å»¶ç»­æŠ¤ç†æ•°æ®
    saveContinuityData() {
      const vm = this;
      // éªŒè¯æ¯ä¸ªé—®é¢˜çš„questionId是否已选择
      let hasEmptyQuestion = false;
      this.continuityTabs.forEach((tab, tabIndex) => {
        tab.form.continuityProblems.forEach((problem, problemIndex) => {
          if (!problem.questionId) {
            hasEmptyQuestion = true;
            vm.$modal.msgError(
              `第${tabIndex + 1}个标签页的第${problemIndex + 1}个问题未选择`
            );
          }
        });
      });
      if (hasEmptyQuestion) {
        return;
      }
      const formData = {
        id: vm.id,
        patid: vm.patid,
        taskid: vm.taskid,
        continueFlag: 2,
        continueCount: vm.continuitySummary.continueCount,
        continueTimeNow: vm.continuitySummary.continueTimeNow,
        continueTimeNext: vm.continuitySummary.continueTimeNext,
        continueContent: vm.continuitySummary.continueContent,
        // å¯ä»¥æ·»åŠ é—®å·ç­”æ¡ˆçš„æ•´åˆ
        // questionnaireAnswers: vm.continuityTabs.map((tab) => ({
        //   tabName: tab.name,
        //   problems: tab.form.continuityProblems.map((problem) => ({
        //     questionId: problem.questionId,
        //     answer:
        //       problem.selectedOption ||
        //       problem.selectedOptions ||
        //       problem.answer,
        //     status: problem.status,
        //     description: problem.description,
        //   })),
        // })),
      };
      Editsingletaskson(formData)
        .then((res) => {
          if (res.code === 200) {
            vm.$modal.msgSuccess("延续护理记录保存成功");
            // é‡ç½®ä¿å­˜çŠ¶æ€
            vm.continuityTabs.forEach((tab) => {
              tab.saving = false;
            });
          } else {
            vm.$modal.msgError("保存失败");
          }
        })
        .catch((error) => {
          console.error("保存失败:", error);
          vm.$modal.msgError("保存失败");
          vm.continuityTabs.forEach((tab) => {
            tab.saving = false;
          });
        });
    },
    // é‡ç½®å»¶ç»­æŠ¤ç†æ ‡ç­¾é¡µ
    resetContinuityTab(index) {
      this.continuityTabs[index].form = {
        continuityProblems: [],
        careRecord: "",
        dutyNurse: "",
        visitType: "",
        serviceTime: "",
        nextServiceTime: "",
      };
      this.$refs[`continuityForm${index}`][0].clearValidate();
    },
    // ä¿å­˜æ‚£è€…信息
    savePatientInfo() {
      this.$refs.patientForm.validate((valid) => {
        if (valid) {
          this.savingPatientInfo = true;
          // æ›´æ–°userform数据
          const updatedUserform = {
            ...this.userform,
            name: this.patientForm.name,
            sex: this.patientForm.sex,
            age: this.patientForm.age,
            telcode: this.patientForm.telcode,
            placeOfResidence: this.patientForm.address,
            medicalRecordNo: this.patientForm.hospitalNumber,
          };
          alterpatient(updatedUserform)
            .then((res) => {
              if (res.code == 200) {
                this.$modal.msgSuccess("患者信息保存成功");
                this.userform = updatedUserform;
              } else {
                this.$modal.msgError("患者信息修改失败");
              }
              this.savingPatientInfo = false;
            })
            .catch((error) => {
              console.error("保存失败:", error);
              this.$modal.msgError("保存失败");
              this.savingPatientInfo = false;
            });
        } else {
          this.$modal.msgError("请填写完整信息");
          return false;
        }
      });
    },
    // é‡ç½®æ‚£è€…信息
    resetPatientInfo() {
      this.initPatientForm();
      this.$refs.patientForm.clearValidate();
    },
    // æ—¶é—´æ ¼å¼åŒ–
    formatTime(time) {
      if (!time) return "";
      return time.split(" ")[0];
    },
    // æ ¼å¼åŒ–显示时间
    formatDisplayTime(time) {
      if (!time) return "未设置";
      return time.replace(" ", " ");
    },
    // å¼‚常列渲染
    tableRowClassName({ row }) {
      if (row.id == this.id) {
        return "warning-row";
      }
      return "";
    },
  },
};
</script>
<style lang="scss" scoped>
.ContinuityCarePage {
  margin: 10px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.headline {
  font-size: 20px;
  height: 40px;
  border-left: 5px solid #41a1be;
  padding-left: 10px;
  margin-bottom: 20px;
  display: flex;
  align-items: center;
  color: #333;
  font-weight: 600;
}
.sub-headline {
  font-size: 16px;
  height: 36px;
  padding-left: 8px;
  margin-bottom: 15px;
  display: flex;
  align-items: center;
  color: #409eff;
  font-weight: 500;
  border-bottom: 2px solid #e4e7ed;
  padding-bottom: 8px;
  i {
    margin-right: 8px;
    font-size: 18px;
  }
}
/* ç¬¬ä¸€éƒ¨åˆ†ï¼šæœåŠ¡åŸºç¡€ä¿¡æ¯ */
.basic-info-section {
  margin: 0 10px;
  padding: 20px;
  background: #fff;
  border: 1px solid #dcdfe6;
  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12), 0 0 6px 0 rgba(0, 0, 0, 0.04);
  border-radius: 4px;
  .basic-info-container {
    display: flex;
    gap: 20px;
    min-height: 1000px;
    @media screen and (max-width: 1200px) {
      flex-direction: column;
    }
  }
  .followup-content {
    flex: 1;
    min-width: 0;
    &.readonly-content {
      background: #f8f9fa;
      border-radius: 8px;
      padding: 15px;
      border: 1px solid #e4e7ed;
    }
    .content-container {
      height: calc(1000px - 60px);
      overflow: hidden;
      display: flex;
      flex-direction: column;
      ::v-deep .el-tabs {
        flex: 1;
        display: flex;
        flex-direction: column;
        .el-tabs__content {
          flex: 1;
          overflow: hidden;
          .el-tab-pane {
            height: 100%;
            overflow: hidden;
            display: flex;
            flex-direction: column;
          }
        }
      }
    }
  }
  .continuity-history {
    flex: 1;
    min-width: 0;
    background: #fff;
    border-radius: 8px;
    padding: 15px;
    border: 1px solid #e4e7ed;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
    .history-content {
      height: calc(1000px - 60px);
      overflow: hidden;
      display: flex;
      flex-direction: column;
      ::v-deep .el-tabs {
        flex: 1;
        display: flex;
        flex-direction: column;
        .el-tabs__content {
          flex: 1;
          overflow-y: auto;
          padding: 15px 0;
          .continuity-form {
            padding: 0 10px;
            .el-form {
              .selected-problems {
                margin-top: 10px;
                padding: 10px;
                background: #f5f7fa;
                border-radius: 4px;
                border: 1px solid #e4e7ed;
              }
              .time-tip {
                margin-top: 5px;
                font-size: 12px;
                color: #67c23a;
                font-style: italic;
              }
            }
          }
        }
      }
    }
  }
}
.patient-info-section {
  margin: 0 10px 20px 10px;
  padding: 20px;
  background: #fff;
  border: 1px solid #dcdfe6;
  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12), 0 0 6px 0 rgba(0, 0, 0, 0.04);
  border-radius: 4px;
  .patient-info-form {
    .continuity-summary {
      padding: 15px;
      background: #ddf0f8;
      border-radius: 8px;
      border: 1px solid #b3e0f2;
      .summary-item {
        display: inline-block;
        margin-right: 30px;
        margin-bottom: 8px;
        .label {
          font-weight: 500;
          color: #333;
        }
        .value {
          color: #409eff;
          font-weight: 500;
        }
      }
    }
  }
}
/* å…±äº«æ ·å¼ */
.CONTENT {
  padding: 10px;
  height: 100%;
  display: flex;
  flex-direction: column;
  .title {
    font-size: 18px;
    font-weight: bold;
    margin-bottom: 20px;
    text-align: center;
    color: #333;
  }
}
.preview-left {
  margin: 10px;
  padding: 20px;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  max-height: 800px;
  overflow-y: auto;
  background: #fff;
  flex: 1;
  .topic-dev {
    margin-bottom: 20px;
    font-size: 16px;
    .dev-text {
      margin-bottom: 10px;
      font-weight: 500;
      color: #333;
    }
  }
}
.scriptTopic-dev {
  padding: 15px;
  border-radius: 4px;
  background: #fafafa;
  border: 1px solid #e4e7ed;
  margin-bottom: 15px;
}
.scriptTopic-isabnormal {
  padding: 15px;
  border-radius: 4px;
  background: #fff5f5;
  border: 1px solid #f56c6c;
  color: #f56c6c;
  margin-bottom: 15px;
}
.scriptTopic-warning {
  padding: 15px;
  border-radius: 4px;
  background: #fff9e6;
  border: 1px solid #e6a23c;
  color: #e6a23c;
  margin-bottom: 15px;
}
.red-star {
  ::v-deep.el-radio__label {
    position: relative;
    padding-right: 10px;
  }
  ::v-deep.el-radio__label::after {
    content: "*";
    color: #f56c6c;
    position: absolute;
    right: -5px;
    top: 0;
  }
}
.yellow-star {
  ::v-deep.el-radio__label {
    position: relative;
    padding-right: 10px;
  }
  ::v-deep.el-radio__label::after {
    content: "*";
    color: #e6a23c;
    position: absolute;
    right: -5px;
    top: 0;
    font-weight: bold;
  }
}
.borderdiv {
  height: 100%;
  padding: 10px;
  display: flex;
  flex-direction: column;
  .title {
    font-size: 18px;
    font-weight: bold;
    margin-bottom: 20px;
    text-align: center;
  }
  .voice-audio {
    display: flex;
    align-items: center;
    color: #59a0f0;
    margin-bottom: 20px;
    padding: 10px;
    background: #f5f7fa;
    border-radius: 4px;
  }
  .preview-left {
    flex: 1;
    margin: 0;
    .leftside {
      margin: 15px 0;
      span {
        display: inline-block;
        padding: 8px 12px;
        background: #409eff;
        color: #fff;
        border-radius: 8px;
        max-width: 80%;
        margin-left: 10px;
      }
    }
    .offside {
      display: flex;
      flex-direction: row-reverse;
      margin: 15px 0;
      .offside-value {
        padding: 8px 12px;
        background: #67c23a;
        color: #fff;
        border-radius: 8px;
        max-width: 80%;
        margin-right: 10px;
      }
    }
  }
}
/* å“åº”式设计 */
@media (max-width: 768px) {
  .ContinuityCarePage {
    margin: 5px;
    gap: 10px;
  }
  .basic-info-section,
  .patient-info-section {
    margin: 0 5px;
    padding: 10px;
  }
  .basic-info-container {
    gap: 10px !important;
  }
  .patient-info-form {
    .el-row {
      flex-direction: column;
    }
    .el-col {
      width: 100% !important;
      margin-bottom: 10px;
    }
  }
  .preview-left {
    margin: 5px;
    padding: 10px;
  }
}
/* é—®é¢˜é€‰æ‹©å™¨é¢æ¿ */
.question-selector-panel {
  margin-bottom: 20px;
  padding: 15px;
  background: #f8f9fa;
  border: 1px solid #e4e7ed;
  border-radius: 8px;
  .selector-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 15px;
    padding-bottom: 10px;
    border-bottom: 1px solid #e4e7ed;
    span {
      font-weight: 500;
      color: #333;
    }
  }
  .question-list {
    max-height: 200px;
    overflow-y: auto;
    padding-right: 10px;
    .question-item {
      margin-bottom: 10px;
      padding: 8px 12px;
      background: #fff;
      border-radius: 6px;
      border: 1px solid #e4e7ed;
      transition: all 0.3s;
      &:hover {
        border-color: #409eff;
        background: #f0f7ff;
      }
      .question-content {
        display: flex;
        align-items: flex-start;
        .question-index {
          font-weight: 600;
          color: #409eff;
          min-width: 30px;
        }
        .question-text {
          flex: 1;
          line-height: 1.5;
        }
      }
    }
  }
}
/* å»¶ç»­é—®é¢˜è¡¨å• */
.continuity-problems-form {
  .problems-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 15px;
    padding-bottom: 10px;
    border-bottom: 1px solid #e4e7ed;
    span {
      font-weight: 500;
      color: #333;
    }
  }
  .problems-list {
    .problem-item {
      margin-bottom: 20px;
      padding: 15px;
      background: #f8f9fa;
      border-radius: 8px;
      border: 1px solid #e4e7ed;
      &:last-child {
        margin-bottom: 0;
      }
      .problem-content {
        .problem-meta {
          display: flex;
          align-items: center;
          margin-bottom: 15px;
          padding-bottom: 10px;
          border-bottom: 1px dashed #e4e7ed;
          .problem-index {
            font-weight: 500;
            color: #409eff;
            min-width: 80px;
          }
        }
        .problem-detail {
          .detail-row {
            display: flex;
            align-items: flex-start;
            margin-bottom: 10px;
            &:last-child {
              margin-bottom: 0;
            }
            .detail-label {
              font-weight: 500;
              color: #666;
              min-width: 100px;
              line-height: 32px;
            }
            .detail-value {
              flex: 1;
              line-height: 1.5;
              color: #333;
              padding: 5px 0;
            }
          }
        }
      }
    }
  }
  .empty-problems {
    text-align: center;
    padding: 30px 0;
    background: #fafafa;
    border-radius: 8px;
    border: 1px dashed #e4e7ed;
  }
}
/* å»¶ç»­é—®é¢˜åˆ—表容器 - æ·»åŠ æ»šåŠ¨ */
.problems-list-container {
  max-height: 400px; /* æŽ§åˆ¶æœ€å¤§é«˜åº¦ */
  overflow-y: auto;
  padding-right: 10px;
  margin-bottom: 15px;
  &::-webkit-scrollbar {
    width: 6px;
  }
  &::-webkit-scrollbar-track {
    background: #f1f1f1;
    border-radius: 3px;
  }
  &::-webkit-scrollbar-thumb {
    background: #c1c1c1;
    border-radius: 3px;
    &:hover {
      background: #a8a8a8;
    }
  }
}
/* é—®å·å¼é—®é¢˜å±•示 */
.question-display-area {
  margin: 15px 0;
  padding: 15px;
  background: #fff;
  border-radius: 8px;
  border: 1px solid #e4e7ed;
  .question-item-render {
    .question-text {
      margin-bottom: 15px;
      font-size: 14px;
      line-height: 1.5;
      color: #333;
      strong {
        color: #409eff;
        margin-right: 5px;
      }
      span {
        color: #666;
      }
    }
    .question-options {
      margin-left: 20px;
      .el-radio,
      .el-checkbox {
        display: block;
        margin-bottom: 8px;
        margin-right: 20px;
      }
    }
    .append-input {
      margin-left: 20px;
      margin-top: 10px;
    }
  }
  .no-question-data {
    text-align: center;
    color: #999;
    padding: 20px;
    font-style: italic;
  }
}
/* è°ƒæ•´é—®é¢˜è¯¦æƒ…布局 */
.problem-detail {
  margin-top: 5px;
  padding-top: 5px;
  padding-bottom: 20px;
  margin-bottom: 10px;
  border-bottom: 1px dashed #6e9af4;
  .detail-row {
    display: flex;
    align-items: center;
    margin-bottom: 15px;
    &:last-child {
      margin-bottom: 0;
    }
    .detail-label {
      font-weight: 500;
      color: #666;
      min-width: 80px;
      white-space: nowrap;
    }
  }
}
/* å“åº”式调整 */
@media (max-width: 768px) {
  .problems-list-container {
    max-height: 300px;
  }
  .problem-detail {
    .detail-row {
      flex-direction: column;
      align-items: flex-start;
      .detail-label {
        margin-bottom: 5px;
        min-width: auto;
      }
      .el-select,
      .el-date-picker {
        width: 100% !important;
        margin: 5px 0 !important;
      }
    }
  }
}
/* å“åº”式调整 */
@media (max-width: 768px) {
  .question-selector-panel {
    padding: 10px;
    .question-list {
      .question-item {
        padding: 6px 8px;
      }
    }
  }
  .continuity-problems-form {
    .problems-list {
      .problem-item {
        padding: 10px;
        .problem-content {
          .problem-meta {
            flex-direction: column;
            align-items: flex-start;
            .el-select {
              width: 100% !important;
              margin: 10px 0;
            }
          }
          .problem-detail {
            .detail-row {
              flex-direction: column;
              align-items: flex-start;
              .detail-label {
                margin-bottom: 5px;
                min-width: auto;
              }
            }
          }
        }
      }
    }
  }
}
/* æ»šåŠ¨æ¡ç¾ŽåŒ– */
.preview-left,
.history-content .el-tabs__content,
.content-container .el-tabs__content {
  &::-webkit-scrollbar {
    width: 6px;
  }
  &::-webkit-scrollbar-track {
    background: #f1f1f1;
    border-radius: 3px;
  }
  &::-webkit-scrollbar-thumb {
    background: #c1c1c1;
    border-radius: 3px;
    &:hover {
      background: #a8a8a8;
    }
  }
}
</style>
src/views/followvisit/Continue/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2103 @@
<template>
  <div class="app-container">
    <!-- <div class="leftvlue" style="margin-bottom: 20px">
      <el-row :gutter="10">
        <el-col :span="2.5" v-for="(item, index) in cardlist" :key="index">
          <el-card
            shadow="hover"
            :body-style="item.router ? ' cursor: pointer' : 'cursor: default'"
          >
            <div style="padding: 8px" @click="$router.push(item.router)">
              <span>{{ item.name }}</span>
              <div
                style="
                  text-align: center;
                  font-size: 18px;
                  margin-top: 10px;
                  font-weight: 600;
                "
              >
                {{ item.value ? item.value : 0 }}
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="2.5">
          <div class="ysfleftvlue">
            <el-card shadow="hover">
              <div style="padding: 8px">
                <span>表单已发送</span>
                <div
                  style="
                    text-align: center;
                    font-size: 18px;
                    margin-top: 10px;
                    font-weight: 600;
                  "
                >
                  {{ yfsvalue }}
                </div>
              </div>
            </el-card>
          </div>
        </el-col>
        <el-col :span="2.5">
          <div class="errleftvlue">
            <el-card shadow="hover">
              <div style="padding: 8px">
                <span>异常</span>
                <div
                  style="
                    text-align: center;
                    font-size: 18px;
                    margin-top: 10px;
                    font-weight: 600;
                  "
                >
                  {{ ycvalue }}
                </div>
              </div>
            </el-card>
          </div>
        </el-col>
        <el-col :span="2.5" v-if="orgname == '省立同德翠苑院区'">
          <div class="jgleftvlue">
            <el-card shadow="hover ">
              <div style="padding: 8px">
                <span>警告</span>
                <div
                  style="
                    text-align: center;
                    font-size: 18px;
                    margin-top: 10px;
                    font-weight: 600;
                  "
                >
                  {{ jgvalue }}
                </div>
              </div>
            </el-card>
          </div>
        </el-col>
      </el-row>
    </div> -->
    <el-row :gutter="20">
      <!--用户数据-->
      <el-form
        :model="topqueryParams"
        ref="queryForm"
        size="small"
        :inline="true"
        v-show="showSearch"
        label-width="98px"
      >
        <el-form-item label="任务名称">
          <el-input
            v-model="topqueryParams.taskName"
            placeholder="请选择任务名称"
          ></el-input>
        </el-form-item>
        <el-form-item label="出院时间">
          <el-date-picker
            v-model="dateRange"
            style="width: 240px"
            value-format="yyyy-MM-dd"
            type="daterange"
            range-separator="-"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
          ></el-date-picker>
        </el-form-item>
        <el-form-item label="应随访时间">
          <el-date-picker
            v-model="dateRangefs"
            style="width: 240px"
            value-format="yyyy-MM-dd"
            type="daterange"
            range-separator="-"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
          ></el-date-picker>
        </el-form-item>
        <el-form-item label="患者姓名" prop="sendname">
          <el-input
            v-model="topqueryParams.sendname"
            placeholder="请输入患者姓名"
          ></el-input>
        </el-form-item>
        <el-form-item label="诊断名称" prop="leavediagname">
          <el-input
            v-model="topqueryParams.leavediagname"
            placeholder="请输入诊断名称"
          ></el-input>
        </el-form-item>
        <el-form-item label="随访人员" prop="updateBy">
          <el-input
            v-model="topqueryParams.updateBy"
            placeholder="请输入随访人员"
          ></el-input>
        </el-form-item>
        <!-- <el-form-item label="主治医生" prop="drname">
          <el-input
            v-model="topqueryParams.drname"
            placeholder="请输入主治医生"
          ></el-input>
        </el-form-item> -->
        <!-- <el-form-item label="经管医生" prop="managementDoctor">
          <el-input
            v-model="topqueryParams.managementDoctor"
            placeholder="请输入主治医生"
          ></el-input>
        </el-form-item> -->
        <!-- <el-form-item label="日期限制" prop="status">
          <el-select v-model="endOut" placeholder="请选择">
            <el-option
              v-for="item in endOuts"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            >
            </el-option>
          </el-select>
        </el-form-item> -->
        <el-form-item label="患者范围" prop="status">
          <el-cascader
            v-model="topqueryParams.scopetype"
            placeholder="默认全部"
            :options="sourcetype"
            :props="{ expandTrigger: 'hover' }"
            @change="handleChange"
          ></el-cascader>
        </el-form-item>
        <el-form-item label="任务状态" prop="status">
          <el-select v-model="topqueryParams.sendstate" placeholder="请选择">
            <el-option
              v-for="item in topicoptions"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            >
            </el-option>
          </el-select>
        </el-form-item>
        <!-- <el-form-item label="排序方式" prop="status">
          <el-select v-model="topqueryParams.sort" placeholder="请选择">
            <el-option
              v-for="item in topicoptionssort"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            >
            </el-option>
          </el-select>
        </el-form-item> -->
        <el-form-item>
          <el-button
            type="primary"
            icon="el-icon-search"
            size="medium"
            @click="handleQuery(1)"
            >搜索</el-button
          >
          <el-button icon="el-icon-refresh" size="medium" @click="resetQuery"
            >重置</el-button
          >
        </el-form-item>
      </el-form>
      <el-divider></el-divider>
      <el-row :gutter="10" class="mb8">
        <el-col :span="1.5">
          <div class="documentf">
            <div class="document">
              <el-button
                type="warning"
                plain
                icon="el-icon-warning-outline"
                size="medium"
                @click="toleadExport(1)"
                >执行失败</el-button
              >
            </div>
          </div>
        </el-col>
        <el-col :span="1.5">
          <div class="documentf">
            <div class="document">
              <el-button
                type="danger"
                plain
                icon="el-icon-warning"
                size="medium"
                @click="toleadExport(2)"
                >结果异常</el-button
              >
            </div>
          </div>
        </el-col>
      </el-row>
      <el-table
        v-loading="loading"
        ref="userform"
        :data="userList"
        :row-class-name="tableRowClassName"
        @selection-change="handleSelectionChange"
      >
        <el-table-column type="selection" width="50" align="center" />
        <el-table-column
          label="任务名称"
          fixed
          width="150"
          show-overflow-tooltip
          align="center"
          key="taskName"
          prop="taskName"
        />
        <!-- <el-table-column label="序号" fixed align="center" key="id" prop="id" /> -->
        <el-table-column
          label="姓名"
          width="100"
          align="center"
          key="sendname"
          prop="sendname"
        >
          <template slot-scope="scope">
            <el-button
              size="medium"
              type="text"
              @click="
                gettoken360(scope.row.sfzh, scope.row.drcode, scope.row.drname)
              "
              ><span class="button-textsc">{{
                scope.row.sendname
              }}</span></el-button
            >
          </template>
        </el-table-column>
        <el-table-column
          label="诊断名称"
          align="center"
          key="leavediagname"
          prop="leavediagname"
          width="120"
          :show-overflow-tooltip="true"
        >
        </el-table-column>
        <el-table-column
          label="延续护理次数"
          align="center"
          key="continueCount"
          prop="continueCount"
          width="120"
          :show-overflow-tooltip="true"
        >
        </el-table-column>
        <el-table-column
          label="最新延续护理时间"
          sortable
          align="center"
          prop="finishtime"
          width="160"
        >
          <template slot-scope="scope">
            <span>{{ parseTime(scope.row.continueTimeNext) }}</span>
          </template>
        </el-table-column>
        <el-table-column
          label="下次延续护理时间"
          sortable
          align="center"
          prop="finishtime"
          width="160"
        >
          <template slot-scope="scope">
            <span>{{ parseTime(scope.row.continueTimeNow) }}</span>
          </template>
        </el-table-column>
        <el-table-column
          label="任务状态"
          align="center"
          key="sendstate"
          prop="sendstate"
          width="120"
        >
          <template slot-scope="scope">
            <el-tooltip
              class="item"
              effect="dark"
              :content="scope.row.remark"
              placement="top-start"
            >
              <div v-if="scope.row.sendstate == 1">
                <el-tag type="primary" :disable-transitions="false"
                  >表单已领取</el-tag
                >
              </div>
              <div v-if="scope.row.sendstate == 2">
                <el-tag type="primary" :disable-transitions="false"
                  >待随访</el-tag
                >
              </div>
              <div v-if="scope.row.sendstate == 3">
                <el-tag type="success" :disable-transitions="false"
                  >表单已发送</el-tag
                >
              </div>
              <div v-if="scope.row.sendstate == 4">
                <el-tag type="info" :disable-transitions="false">不执行</el-tag>
              </div>
              <div v-if="scope.row.sendstate == 5">
                <el-tag type="danger" :disable-transitions="false"
                  >发送失败</el-tag
                >
              </div>
            <div v-if="scope.row.sendstate == 6">
                <el-tag type="success" :disable-transitions="false"
                  >已完成</el-tag
                >
              </div>
              <div v-if="scope.row.sendstate == 7">
                <el-tag type="danger" :disable-transitions="false"
                  >超时</el-tag
                >
              </div>
            </el-tooltip>
          </template>
        </el-table-column>
        <!-- <el-table-column
          label="任务异常说明"
          width="120"
          align="center"
          key="remark"
          prop="remark" -->
        />
        <el-table-column
          label="处理意见"
          align="center"
          key="suggest"
          prop="suggest"
          width="120"
        >
          <template slot-scope="scope">
            <dict-tag
              :options="dict.type.sys_suggest"
              :value="scope.row.suggest"
            />
          </template>
        </el-table-column>
        <el-table-column
          label="首次随访完成时间"
          sortable
          align="center"
          prop="finishtime"
          width="160"
        >
          <template slot-scope="scope">
            <span>{{ parseTime(scope.row.finishtime) }}</span>
          </template>
        </el-table-column>
        <el-table-column
          label="出院日期"
          width="200"
          align="center"
          key="endtime"
          prop="endtime"
        >
          <template slot-scope="scope">
            <span>{{ formatTime(scope.row.endtime) }}</span>
          </template></el-table-column
        >
        <el-table-column
          label="应随访日期"
          width="200"
          align="center"
          key="visitTime"
          prop="visitTime"
        >
          <template slot-scope="scope">
            <span>{{ formatTime(scope.row.visitTime) }}</span>
          </template></el-table-column
        >
        <el-table-column
          label="主治医生"
          width="120"
          align="center"
          key="drname"
          prop="drname"
        />
        <el-table-column
          v-if="orgname != '丽水市中医院'"
          label="随访人员"
          align="center"
          key="updateBy"
          prop="updateBy"
          width="120"
        />
        <el-table-column
          v-if="orgname != '丽水市中医院'"
          label="经管医生"
          align="center"
          key="managementDoctor"
          prop="managementDoctor"
          width="120"
        />
        <el-table-column
          label="出院天数"
          width="120"
          align="center"
          key="endDay"
          prop="endDay"
        >
          <template slot-scope="scope">
            <span>{{ scope.row.endDay ? scope.row.endDay + "天" : "" }}</span>
          </template>
        </el-table-column>
        <el-table-column
          label="身份证号码"
          width="200"
          align="center"
          key="sfzh"
          prop="sfzh"
        />
        <el-table-column
          label="联系电话"
          width="200"
          align="center"
          key="phone"
          prop="phone"
        />
        <el-table-column
          label="责任护士"
          width="120"
          align="center"
          key="nurseName"
          prop="nurseName"
        />
        <el-table-column
          label="科室"
          align="center"
          key="deptname"
          prop="deptname"
          width="120"
        >
        </el-table-column>
        <el-table-column
          label="病区"
          align="center"
          key="leavehospitaldistrictname"
          prop="leavehospitaldistrictname"
          width="120"
        >
        </el-table-column>
        <el-table-column
          label="出院随访模板名称"
          align="center"
          key="templatename"
          prop="templatename"
          width="200"
        />
        <el-table-column
          label="任务执行方式"
          align="center"
          key="preachform"
          prop="preachform"
          width="160"
          :show-overflow-tooltip="true"
        >
          <template slot-scope="scope">
            <span v-for="item in scope.row.preachform">{{ item }}、 </span>
          </template>
        </el-table-column>
        <!-- <el-table-column
          label="任务发送流程"
          align="center"
          key="serviceSubtaskRecordList"
          prop="serviceSubtaskRecordList"
          width="160"
          :show-overflow-tooltip="true"
        >
          <template slot-scope="scope">
            <span v-for="item in scope.row.serviceSubtaskRecordList"
              >{{ item.remark }}、
            </span>
          </template>
        </el-table-column> -->
        <el-table-column
          label="任务结果说明"
          width="220"
          align="center"
          key="remark"
          prop="remark"
        >
          <template slot-scope="scope" v-if="scope.row.remark">
            <el-tooltip
              :content="scope.row.remark"
              placement="top"
              effect="dark"
            >
              <el-tag
                type="warning"
                v-if="scope.row.sendstate != 5 && scope.row.sendstate != 4"
                >{{ scope.row.remark }}</el-tag
              >
              <el-tag type="warning" v-else>{{ scope.row.remark }}</el-tag>
            </el-tooltip>
          </template>
        </el-table-column>
        <el-table-column
          label="操作"
          align="center"
          fixed="right"
          width="120"
          class-name="small-padding fixed-width"
        >
          <template slot-scope="scope">
            <el-button size="medium" type="text" @click="Seedetails(scope.row)"
              ><span class="button-zx"
                ><i class="el-icon-s-order"></i>查看详情</span
              ></el-button
            >
          </template>
        </el-table-column>
      </el-table>
      <pagination
        v-show="total > 0"
        :total="total"
        :page.sync="topqueryParams.pageNum"
        :limit.sync="topqueryParams.pageSize"
        @pagination="getList"
      />
    </el-row>
    <!-- æ»¡æ„åº¦å¼¹æ¡† -->
    <el-dialog
      title="随访满意度评分"
      :visible.sync="scoreDialogVisible"
      width="80%"
      :close-on-click-modal="false"
    >
      <el-table :data="selectedRows" border style="width: 100%">
        <el-table-column
          label="姓名"
          width="100"
          align="center"
          prop="sendname"
        />
        <el-table-column
          label="任务名称"
          width="180"
          align="center"
          prop="taskName"
        />
        <!-- æ–°å¢žè¯„分列 -->
        <el-table-column
          label="真实性(20)"
          align="center"
          key="authenticity"
          prop="authenticity"
          width="150"
        >
          <template slot-scope="scope">
            <el-input-number
              v-model="scope.row.authenticity"
              :min="0"
              :max="20"
              :step="1"
              size="small"
            />
          </template>
        </el-table-column>
        <el-table-column
          label="一周内完成(20)"
          align="center"
          key="weekFinish"
          prop="weekFinish"
          width="150"
        >
          <template slot-scope="scope">
            <el-input-number
              v-model="scope.row.weekFinish"
              :min="0"
              :max="20"
              :step="1"
              size="small"
            />
          </template>
        </el-table-column>
        <el-table-column
          label="规范性(10)"
          align="center"
          key="standard"
          prop="standard"
          width="150"
        >
          <template slot-scope="scope">
            <el-input-number
              v-model="scope.row.standard"
              :min="0"
              :max="10"
              :step="1"
              size="small"
            />
          </template>
        </el-table-column>
        <el-table-column
          label="及时性(10)"
          align="center"
          key="timeliness"
          prop="timeliness"
          width="150"
        >
          <template slot-scope="scope">
            <el-input-number
              v-model="scope.row.timeliness"
              :min="0"
              :max="10"
              :step="1"
              size="small"
            />
          </template>
        </el-table-column>
        <el-table-column
          label="宣教情况(10)"
          align="center"
          key="library"
          prop="library"
          width="150"
        >
          <template slot-scope="scope">
            <el-input-number
              v-model="scope.row.library"
              :min="0"
              :max="10"
              :step="1"
              size="small"
            />
          </template>
        </el-table-column>
        <el-table-column
          label="环境满意度(10)"
          align="center"
          key="environment"
          prop="environment"
          width="150"
        >
          <template slot-scope="scope">
            <el-input-number
              v-model="scope.row.environment"
              :min="0"
              :max="10"
              :step="1"
              size="small"
            />
          </template>
        </el-table-column>
        <el-table-column
          label="医生满意度(10)"
          align="center"
          key="doctorSatisfaction"
          prop="doctorSatisfaction"
          width="150"
        >
          <template slot-scope="scope">
            <el-input-number
              v-model="scope.row.doctorSatisfaction"
              :min="0"
              :max="10"
              :step="1"
              size="small"
            />
          </template>
        </el-table-column>
        <el-table-column
          label="护士满意度(10)"
          align="center"
          key="nurseSatisfaction"
          prop="nurseSatisfaction"
          width="150"
        >
          <template slot-scope="scope">
            <el-input-number
              v-model="scope.row.nurseSatisfaction"
              :min="0"
              :max="10"
              :step="1"
              size="small"
            />
          </template>
        </el-table-column>
        <el-table-column
          label="总分"
          align="center"
          key="total"
          prop="total"
          fixed="right"
        >
          <template slot-scope="scope">
            <span>{{ calculateTotal(scope.row) }}</span>
          </template>
        </el-table-column>
      </el-table>
      <div slot="footer" class="dialog-footer">
        <el-button @click="scoreDialogVisible = false">取消</el-button>
        <el-button type="primary" @click="saveScores">保存</el-button>
      </div>
    </el-dialog>
    <!-- æ·»åŠ æˆ–ä¿®æ”¹å½±åƒéšè®¿å¯¹è¯æ¡† -->
    <el-dialog
      :title="amendtag ? '修改患者信息' : '新增患者'"
      :visible.sync="Labelchange"
      width="900px"
    >
      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
        <el-row>
          <el-col :span="8">
            <el-form-item label="姓名" width="100" prop="name">
              <el-input
                v-model="form.name"
                placeholder="请输入姓名"
                maxlength="30"
              />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="性别" width="100" prop="sex">
              <el-select v-model="form.sex" placeholder="请选择性别">
                <el-option
                  v-for="dict in sextype"
                  :key="dict.value"
                  :label="dict.label"
                  :value="dict.value"
                ></el-option>
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="年龄" prop="age">
              <el-input
                v-model="form.age"
                placeholder="请输入年龄"
                maxlength="30"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="8">
            <el-form-item label="过滤医生" width="100" prop="filterDrname">
              <el-input
                v-model="form.filterDrname"
                placeholder="请输入医生姓名"
                maxlength="30"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="24">
            <el-form-item label="过滤原因">
              <el-input
                v-model="form.notrequiredreason"
                type="textarea"
                placeholder="请输入过滤原因"
              ></el-input>
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
        <el-button @click="cancel">取 æ¶ˆ</el-button>
      </div>
    </el-dialog>
    <!-- ä¿®æ”¹å‘送时间对话框 -->
    <el-dialog
      title="发送时间设置"
      :visible.sync="modificationVisible"
      width="45%"
    >
      <div style="margin-bottom: 20px; color: red">
        ç»Ÿä¸€ä¿®æ”¹å½“天未发送的任务时间
      </div>
      <el-form
        :model="ruleForm"
        :rules="rules"
        ref="ruleForm"
        label-width="120px"
        class="demo-ruleForm"
      >
        <el-form-item label="发送日期">
          <el-date-picker
            v-model="ruleForm.value1"
            type="date"
            placeholder="选择日期"
          >
          </el-date-picker>
        </el-form-item>
        <el-form-item label="时间段" prop="type">
          <el-checkbox-group v-model="ruleForm.type">
            <el-checkbox label="上午" name="type"></el-checkbox>
            <el-checkbox label="下午" name="type"></el-checkbox>
            <el-checkbox label="晚上" name="type"></el-checkbox>
          </el-checkbox-group>
        </el-form-item>
        <el-form-item label="上午时间区间" required>
          <el-time-picker
            is-range
            v-model="ruleForm.value2"
            range-separator="至"
            start-placeholder="开始时间"
            end-placeholder="结束时间"
            placeholder="选择时间范围"
          >
          </el-time-picker>
        </el-form-item>
        <el-form-item label="下午时间区间" required>
          <el-time-picker
            is-range
            v-model="ruleForm.value3"
            range-separator="至"
            start-placeholder="开始时间"
            end-placeholder="结束时间"
            placeholder="选择时间范围"
          >
          </el-time-picker>
        </el-form-item>
        <el-form-item label="晚上时间区间" required>
          <el-time-picker
            is-range
            v-model="ruleForm.value4"
            range-separator="至"
            start-placeholder="开始时间"
            end-placeholder="结束时间"
            placeholder="选择时间范围"
          >
          </el-time-picker>
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button @click="modificationVisible = false">取 æ¶ˆ</el-button>
        <el-button type="primary" @click="modificationVisible = false"
          >ç¡® å®š</el-button
        >
      </span>
    </el-dialog>
    <!-- å†æ¬¡éšè®¿ -->
    <el-dialog title="患者再次随访" :visible.sync="dialogFormVisible">
      <el-form ref="zcform" :rules="zcrules" :model="zcform" label-width="80px">
        <el-form-item label="任务名称">
          <el-input
            style="width: 400px"
            disabled
            v-model="zcform.taskName"
          ></el-input>
        </el-form-item>
        <el-form-item label="患者名称">
          <el-input
            style="width: 400px"
            disabled
            v-model="zcform.sendname"
          ></el-input>
        </el-form-item>
        <el-form-item label="年龄">
          <el-input
            style="width: 400px"
            disabled
            v-model="zcform.age"
          ></el-input>
        </el-form-item>
        <el-form-item label="科室">
          <el-input
            style="width: 400px"
            disabled
            v-model="zcform.deptname"
          ></el-input>
        </el-form-item>
        <el-form-item label="病区">
          <el-input
            style="width: 400px"
            disabled
            v-model="zcform.leavehospitaldistrictname"
          ></el-input>
        </el-form-item>
        <el-form-item label="随访方式" prop="resource">
          <el-radio-group v-model="zcform.resource">
            <el-radio label="1">本病区随访</el-radio>
            <el-radio label="2">随访中心随访</el-radio>
          </el-radio-group>
        </el-form-item>
        <!-- <el-form-item label="即刻发送">
          <el-switch v-model="zcform.delivery"></el-switch>
        </el-form-item> -->
        <el-form-item label="出院时间">
          <el-input
            style="width: 400px"
            disabled
            v-model="zcform.endtime"
          ></el-input>
        </el-form-item>
        <el-form-item label="随访完成时间" prop="date1">
          <el-date-picker
            type="date"
            placeholder="选择日期"
            v-model="zcform.date1"
            style="width: 100%"
          ></el-date-picker>
        </el-form-item>
        <el-form-item label="随访记录">
          <el-input type="textarea" v-model="zcform.remark"></el-input>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="dialogFormVisible = false">取 æ¶ˆ</el-button>
        <el-button type="primary" @click="setupsubtask">确认创建服务</el-button>
      </div>
    </el-dialog>
  </div>
</template>
<script>
import {
  delUser,
  addUser,
  updateUser,
  resetUserPwd,
  changeUserStatus,
} from "@/api/system/user";
import {
  getTaskservelist,
  buidegetTasklist,
  addserviceSubtask,
  query360PatInfo,
  addsatisfaction,
} from "@/api/AiCentre/index";
import { alterpatient, particularpatient } from "@/api/patient/homepage";
import Treeselect from "@riophae/vue-treeselect";
import store from "@/store";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
export default {
  name: "Discharge",
  dicts: ["sys_normal_disable", "sys_user_sex", "sys_yujing", "sys_suggest"],
  components: { Treeselect },
  data() {
    return {
      // é®ç½©å±‚
      loading: true,
      // é€‰ä¸­æ•°ç»„
      ids: [],
      // éžå•个禁用
      single: true,
      // éžå¤šä¸ªç¦ç”¨
      multiple: true,
      // æ˜¾ç¤ºæœç´¢æ¡ä»¶
      showSearch: true,
      dialogFormVisible: false,
      // æ€»æ¡æ•°
      total: 0,
      // ç”¨æˆ·è¡¨æ ¼æ•°æ®
      userList: null,
      // å¼¹å‡ºå±‚标题
      title: "新增影像随访",
      // æ˜¯å¦æ˜¾ç¤ºä¿®æ”¹ã€æ·»åŠ å¼¹å‡ºå±‚
      addalteropen: false,
      // ä¿®æ”¹å‘送时间对话框
      modificationVisible: false,
      // éƒ¨é—¨åç§°
      deptName: undefined,
      // é»˜è®¤å¯†ç 
      initPassword: undefined,
      // æ—¥æœŸèŒƒå›´
      dateRange: [],
      dateRangefs: [],
      // å²—位选项
      postOptions: [],
      ruleForm: {
        type: [],
      },
      zcform: {},
      dynamicTags: ["选项一", "选项二", "选项三"], //选项
      inputVisible: false,
      Labelchange: false,
      ycvalue: "",
      jgvalue: "",
      yfsvalue: "",
      inputValue: "",
      preachform: "",
      previewVisible: false, //影像随访预览弹框
      radio: "",
      radios: [],
      previewtype: 2, //预览影像随访类型
      total: 0, // æ€»æ¡æ•°
      // æ»¡æ„åº¦è°ƒæŸ¥æ•°æ®
      scoreDialogVisible: false,
      selectedRows: [],
      value: [],
      list: [],
      sourcetype: [
        {
          value: 1,
          label: "科室",
          children: [],
        },
        {
          value: 2,
          label: "病区",
          children: [],
        },
        {
          value: 3,
          label: "全部",
        },
      ],
      loading: false,
      cardlist: [
        {
          name: "出院服务总量",
          value: 0,
        },
        // {
        //   name: "患者过滤",
        //   value: 0,
        // },
        {
          name: "需随访",
          value: 0,
        },
        {
          name: "发送失败",
          value: 0,
        },
        {
          name: "待随访",
          value: 0,
        },
        // {
        //   name: "已发送",
        //   value: 0,
        // },
        // {
        //   name: "表单已发送",
        //   value: 0,
        // },
      ],
      zcrules: {
        date1: [
          { required: true, message: "请选择随访方式", trigger: "change" },
        ],
        resource: [
          { required: true, message: "请选择随访时间", trigger: "blur" },
        ],
      },
      // è¡¨å•参数
      form: {
        phonenumber: "",
        totagid: "",
        types: "",
        nickName: "",
        qystatus: "",
        btstatus: "",
      },
      // endOut: 1,
      endOut: localStorage.getItem("orgname") == "丽水市中医院" ? 0 : 1, //0 å‡ºé™¢æ—¶é—´(正序)    1 å‡ºé™¢æ—¶é—´(倒序)   2 å‘送时间(正序)    3 å‘送时间(倒序)  7应随访日期(倒序) åº”随访日期(正序)
      endOuts: [
        {
          value: 0,
          label: "截止至当日服务",
        },
        {
          value: 1,
          label: "全部服务",
        },
      ],
      topicoptionssort: [
        {
          value: 0,
          label: "出院时间(正序)",
        },
        {
          value: 1,
          label: "出院时间(倒序)",
        },
        {
          value: 2,
          label: "发送时间(正序)",
        },
        {
          value: 3,
          label: "发送时间(倒序)",
        },
        {
          value: 7,
          label: "应随访日期(正序)",
        },
        {
          value: 8,
          label: "应随访日期(倒序)",
        },
      ],
      // æŸ¥è¯¢å‚æ•°
      topqueryParams: {
        pageNum: 1,
        pageSize: 10,
        sort: localStorage.getItem("orgname") == "丽水市中医院" ? 8 : 2, //0 å‡ºé™¢æ—¶é—´(正序)    1 å‡ºé™¢æ—¶é—´(倒序)   2 å‘送时间(正序)    3 å‘送时间(倒序)  7应随访日期(倒序) åº”随访日期(正序)
        searchscope: 3,
        continueFlag: 2,
        visitCount: 1,
        scopetype: [],
        leaveldeptcodes: [],
        leavehospitaldistrictcodes: [],
      },
      orgname: "",
      propss: { multiple: true },
      options: [],
      topicoptions: [
        {
          value: null,
          label: "全部",
        },
        {
          value: 1,
          label: "表单已领取",
        },
        {
          value: 2,
          label: "待随访",
        },
        {
          value: 3,
          label: "表单已发送",
        },
        {
          value: 4,
          label: "不执行",
        },
         {
          value: 5,
          label: "发送失败",
        },
        {
          value: 6,
          label: "已完成",
        },
         {
          value: 7,
          label: "超时",
        },
      ],
      sextype: [
        {
          value: 1,
          label: "男",
        },
        {
          value: 2,
          label: "女",
        },
      ],
      topicoptionsyj: [
        {
          value: 1,
          label: "异常",
        },
        {
          value: 2,
          label: "警告",
        },
        {
          value: 0,
          label: "正常",
        },
      ],
      url: "http://9.208.2.190:8090/smartor/serviceExternal/query360PatInfo",
      postData: {
        XiaoXiTou: {
          FaSongFCSJC: "ZJHES",
          FaSongJGID: localStorage.getItem("orgid"),
          FaSongJGMC: localStorage.getItem("orgname"),
          FaSongSJ: "2025-01-09 17:29:36",
          FaSongXTJC: "SUIFANGXT",
          FaSongXTMC: "随访系统",
          XiaoXiID: "5FA92AFB-9833-4608-87C7-F56A654AC171",
          XiaoXiLX: "SC_LC_360STCX",
          XiaoXiMC: "360 视图查询",
          ZuHuID: localStorage.getItem("ZuHuID"),
          ZuHuMC: localStorage.getItem("orgname"),
        },
        YeWuXX: {
          BingRenXX: {
            ZhengJianHM: "",
            ZhengJianLXDM: "01",
            ZhengJianLXMC: "居民身份证",
            ZuZhiJGID: localStorage.getItem("orgid"),
            ZuZhiJGMC: localStorage.getItem("orgname"),
          },
          YongHuXX: {
            XiTongID: "SUIFANGXT",
            XiTongMC: "随访系统",
            YongHuID: localStorage.getItem("YongHuID"),
            YongHuXM: localStorage.getItem("YongHuXM"),
            ZuZhiJGID: localStorage.getItem("orgid"),
            ZuZhiJGMC: localStorage.getItem("orgname"),
            idp: "lyra",
          },
        },
      },
      amendtag: false,
      errtype: "",
      leavehospitaldistrictcode: "",
      serviceState: [],
      checkboxlist: [],
      // è¡¨å•校验
      rules: {},
    };
  },
  watch: {},
  created() {
    this.serviceState = store.getters.serviceState;
    this.checkboxlist = store.getters.checkboxlist;
    this.errtype = this.$route.query.errtype;
    this.orgname = localStorage.getItem("orgname");
    this.leavehospitaldistrictcode =
      this.$route.query.leavehospitaldistrictcode;
    this.sourcetype[0].children = store.getters.belongDepts.map((dept) => {
      return {
        label: dept.deptName,
        value: dept.deptCode,
      };
    });
    this.sourcetype[1].children = store.getters.belongWards.map((dept) => {
      return {
        label: dept.districtName,
        value: dept.districtCode,
      };
    });
    if (this.errtype) {
      this.toleadExport(2);
    } else {
      this.getList(1);
    }
    this.getConfigKey("sys.user.initPassword").then((response) => {
      this.initPassword = response.msg;
    });
  },
  activated() {
    this.getList(1);
  },
  methods: {
    /** æŸ¥è¯¢éšè®¿æœåŠ¡åˆ—è¡¨ */
    getList(refresh) {
      // é»˜è®¤å…¨éƒ¨
      if (this.topqueryParams.searchscope == 3) {
        this.topqueryParams.leaveldeptcodes = store.getters.belongDepts.map(
          (obj) => obj.deptCode
        );
        this.topqueryParams.leavehospitaldistrictcodes =
          store.getters.belongWards.map((obj) => obj.districtCode);
      }
      if (this.endOut == 0) {
        this.topqueryParams.endSendDateTime = this.formatDateToYYYYMMDDHHMMSS(
          this.getEndOfDay()
        );
      } else {
        // this.topqueryParams.endSendDateTime = null;
      }
      // æŽ¥å—异常跳转
      if (this.errtype) {
        this.topqueryParams.leavehospitaldistrictcodes.push(
          this.leavehospitaldistrictcode
        );
        console.log(this.topqueryParams.leavehospitaldistrictcodes, "11");
      }
      this.loading = true;
      if (
        this.topqueryParams.leavehospitaldistrictcodes[0] &&
        this.topqueryParams.leaveldeptcodes[0]
      ) {
        this.topqueryParams.deptOrDistrict = 2;
      } else {
        this.topqueryParams.deptOrDistrict = 1;
      }
      getTaskservelist(this.topqueryParams).then((response) => {
        this.userList = response.rows[0].serviceSubtaskList;
        this.total = response.total;
        if (refresh) {
          this.cardlist[0].value =
            Number(response.rows[0].wzx) + Number(response.rows[0].ysf);
          // this.cardlist[1].value = response.rows[0].wzx;
          this.cardlist[1].value = response.rows[0].ysf;
          this.ycvalue = response.rows[0].yc;
          this.jgvalue = response.rows[0].jg;
          this.cardlist[2].value = response.rows[0].fssb;
          this.cardlist[3].value = response.rows[0].dsf;
          // this.cardlist[4].value = response.rows[0].yfs2;
          this.yfsvalue = response.rows[0].yfs;
        }
        this.loading = false;
        this.userList.forEach((item) => {
          let idArray = null;
          if (item.endtime) {
            item.endDay = this.daysBetween(item.endtime);
          }
          if (item.preachform) {
            if (item.endtime) {
              item.preachformson = item.preachform;
              idArray = item.preachform.split(",");
            }
            item.preachform = idArray.map((value) => {
              // æŸ¥æ‰¾id对应的对象
              const item = this.checkboxlist.find(
                (item) => item.value == value
              );
              // å¦‚果找到对应的id,返回label值,否则返回null
              return item ? item.label : null;
            });
          }
        });
        this.total = response.total;
      });
    },
    // æ—¶é—´
    getEndOfDay() {
      const date = new Date(); // åˆ›å»ºä¸€ä¸ªè¡¨ç¤ºå½“前时间的Date对象
      date.setHours(23, 59, 59, 0); // å°†æ—¶é—´è®¾ç½®ä¸º23:59:59.000
      return date;
    },
    formatDateToYYYYMMDDHHMMSS(date) {
      const year = date.getFullYear();
      const month = String(date.getMonth() + 1).padStart(2, "0"); // æœˆä»½è¡¥é›¶
      const day = String(date.getDate()).padStart(2, "0"); // æ—¥æœŸè¡¥é›¶
      const hours = String(date.getHours()).padStart(2, "0");
      const minutes = String(date.getMinutes()).padStart(2, "0");
      const seconds = String(date.getSeconds()).padStart(2, "0");
      return `${year}-${month}-${day}`;
    },
    affiliation() {
      this.topqueryParams.managementDoctorCode = store.getters.hisUserId;
      this.getList(1);
    },
    onthatday() {
      this.topqueryParams.startSendDateTime = this.getCurrentDate();
      this.topqueryParams.endSendDateTime = this.getCurrentDate();
      this.getList(1);
    },
    getCurrentDate() {
      const now = new Date();
      return now.toISOString().slice(0, 10); // æˆªå–前10个字符,即 YYYY-MM-DD
    },
    buidegetTasklist(type) {
      if (this.topqueryParams.searchscope == 3) {
        this.topqueryParams.leaveldeptcodes = store.getters.belongDepts.map(
          (obj) => obj.deptCode
        );
        this.topqueryParams.leavehospitaldistrictcodes =
          store.getters.belongWards.map((obj) => obj.districtCode);
      }
      // æŽ¥å—异常跳转
      if (this.errtype) {
        this.topqueryParams.leavehospitaldistrictcodes.push(
          this.leavehospitaldistrictcode
        );
      }
      let obj = {
        pageNum: 1,
        pageSize: 10,
        leavehospitaldistrictcodes:
          this.topqueryParams.leavehospitaldistrictcodes,
        sendstates: [2, 3],
        leaveldeptcodes: this.topqueryParams.leaveldeptcodes,
      };
      buidegetTasklist(obj).then((response) => {
        this.userList = response.rows[0].serviceSubtaskList;
        this.total = response.total;
        if (refresh) {
          this.cardlist[0].value =
            Number(response.rows[0].wzx) + Number(response.rows[0].ysf);
          this.cardlist[1].value = response.rows[0].wzx;
          this.cardlist[2].value = response.rows[0].ysf;
          this.ycvalue = response.rows[0].yc;
          this.jgvalue = response.rows[0].jg;
          this.cardlist[3].value = response.rows[0].fssb;
          this.cardlist[4].value = response.rows[0].dsf;
          // this.cardlist[5].value = response.rows[0].yfs2;
          this.yfsvalue = response.rows[0].yfs;
        }
        this.loading = false;
        this.userList.forEach((item) => {
          let idArray = null;
          if (item.endtime) {
            item.endDay = this.daysBetween(item.endtime);
          }
          if (item.preachform) {
            if (item.endtime) {
              item.preachformson = item.preachform;
              idArray = item.preachform.split(",");
            }
            item.preachform = idArray.map((value) => {
              // æŸ¥æ‰¾id对应的对象
              const item = this.checkboxlist.find(
                (item) => item.value == value
              );
              // å¦‚果找到对应的id,返回label值,否则返回null
              return item ? item.label : null;
            });
          }
        });
        this.total = response.total;
      });
    },
    // æŸ¥çœ‹é—¨è¯Šéšè®¿è¯¦æƒ…
    Referencequestion(row) {
      this.previewVisible = true;
    },
    // æ·»åŠ å¼¹æ¡†æœç´¢
    remoteMethod(query) {
      if (query !== "") {
        this.loading = true;
        setTimeout(() => {
          this.loading = false;
          this.options = this.list.filter((item) => {
            return item.label.toLowerCase().indexOf(query.toLowerCase()) > -1;
          });
        }, 200);
      } else {
        this.options = [];
      }
    },
    // å½±åƒéšè®¿çŠ¶æ€ä¿®æ”¹
    handleStatusChange(row) {
      let text = row.status === "0" ? "启用" : "停用";
      this.$modal
        .confirm('确认要"' + text + '""' + row.userName + '"用户吗?')
        .then(function () {
          return changeUserStatus(row.userId, row.status);
        })
        .then(() => {
          this.$modal.msgSuccess(text + "成功");
        })
        .catch(function () {
          row.status = row.status === "0" ? "1" : "0";
        });
    },
    // è¡¨å•重置
    reset() {
      this.form = {
        userId: undefined,
        deptId: undefined,
        userName: undefined,
        nickName: undefined,
        password: undefined,
        phonenumber: undefined,
        email: undefined,
        sex: undefined,
        status: "0",
        remark: undefined,
        postIds: [],
        roleIds: [],
      };
      this.resetForm("form");
    },
    /** æœç´¢æŒ‰é’®æ“ä½œ */
    handleQuery(refresh) {
      if (this.topqueryParams.searchscope == 3) {
        this.topqueryParams.leaveldeptcodes = store.getters.belongDepts.map(
          (obj) => obj.deptCode
        );
        this.topqueryParams.leavehospitaldistrictcodes =
          store.getters.belongWards.map((obj) => obj.districtCode);
      }
      this.topqueryParams.pageNum = 1;
      this.topqueryParams.startOutHospTime = this.dateRange[0];
      this.topqueryParams.endOutHospTime = this.dateRange[1];
      this.topqueryParams.startSendDateTime = this.dateRangefs[0];
      this.topqueryParams.endSendDateTime = this.dateRangefs[1];
      this.getList(refresh);
    },
    // æ‚£è€…范围处理
    handleChange(value) {
      let type = value[0];
      let code = value.slice(-1)[0];
      this.topqueryParams.leavehospitaldistrictcodes = [];
      this.topqueryParams.leaveldeptcodes = [];
      if (type == 1) {
        this.topqueryParams.leaveldeptcodes.push(code);
        this.topqueryParams.leavehospitaldistrictcodes = [];
        this.topqueryParams.searchscope = 1;
      } else if (type == 2) {
        this.topqueryParams.leavehospitaldistrictcodes.push(code);
        this.topqueryParams.leaveldeptcodes = [];
        this.topqueryParams.searchscope = 2;
      } else {
        this.topqueryParams.searchscope = 3;
      }
    },
    /** é‡ç½®æŒ‰é’®æ“ä½œ */
    resetQuery() {
      this.dateRange = [];
      this.dateRangefs = [];
      this.topqueryParams = {
        pageNum: 1,
        pageSize: 10,
        sort: 2, //0 å‡ºé™¢æ—¶é—´(正序)    1 å‡ºé™¢æ—¶é—´(倒序)   2 å‘送时间(正序)    3 å‘送时间(倒序)
        searchscope: 3,
        continueFlag: 2,
        visitCount: 1,
        scopetype: [],
        leaveldeptcodes: [],
        leavehospitaldistrictcodes: [],
      };
      this.handleQuery(1);
    },
    handleSelectionChange(rows) {
      this.selectedRows = rows.map((row) => {
        // åˆå§‹åŒ–评分字段
        return {
          ...row,
          authenticity: row.authenticity || 0,
          weekFinish: row.weekFinish || 0,
          standard: row.standard || 0,
          timeliness: row.timeliness || 0,
          library: row.library || 0,
          environment: row.environment || 0,
          doctorSatisfaction: row.doctorSatisfaction || 0,
          nurseSatisfaction: row.nurseSatisfaction || 0,
        };
      });
      if (this.selectedRows.length > 0) {
        this.multiple = false;
      } else {
        this.multiple = true;
      }
    },
    // è®¡ç®—总分
    calculateTotal(row) {
      return (
        (row.authenticity || 0) +
        (row.weekFinish || 0) +
        (row.standard || 0) +
        (row.timeliness || 0) +
        (row.library || 0) +
        (row.environment || 0) +
        (row.doctorSatisfaction || 0) +
        (row.nurseSatisfaction || 0)
      );
    },
    // ä¿å­˜è¯„分
    saveScores() {
      this.selectedRows.forEach((item) => {
        item.createBy = null;
        item.patName = item.sendname;
        item.hospitaldistrictname = item.leavehospitaldistrictname;
      });
      addsatisfaction(this.selectedRows).then((res) => {
        if (res.code == 200) {
          this.$message.success("评分保存成功");
          this.scoreDialogVisible = false;
          this.selectedRows = [];
          this.$refs.userform.clearSelection();
        } else {
          this.$modal.msgWarning("评分保存失败");
          this.scoreDialogVisible = false;
          this.selectedRows = [];
          this.$refs.userform.clearSelection();
        }
      });
      // è¿™é‡Œå¯ä»¥æ·»åŠ ä¿å­˜é€»è¾‘ï¼Œå¦‚è°ƒç”¨API保存评分
    },
    //删除选项
    handleClose(tag) {
      this.dynamicTags.splice(this.dynamicTags.indexOf(tag), 1);
    },
    //触发新增输入
    showInput() {
      this.inputVisible = true;
      this.$nextTick((_) => {
        this.$refs.saveTagInput.$refs.input.focus();
      });
    },
    //获取失去焦点触发
    handleInputConfirm() {
      let inputValue = this.inputValue;
      if (inputValue) {
        this.dynamicTags.push(inputValue);
      }
      this.inputVisible = false;
      this.inputValue = "";
    },
    /** æ–°å¢žæŒ‰é’®æ“ä½œ */
    handleAdd() {
      this.$router.push({
        path: "/followvisit/QuestionnaireTask",
        query: {
          type: 2,
          serviceType: 2,
        },
      });
    },
    //患者360跳转
    gettoken360(sfzh, drcode, drname) {
      // this.$modal.msgWarning("360功能暂未开通");
      this.postData.YeWuXX.BingRenXX.ZhengJianHM = sfzh;
      query360PatInfo(this.postData).then((res) => {
        if (res.data.url) {
          window.open(res.data.url, "_blank");
          // this.linkUrl = res.data.url;
        } else {
          this.$modal.msgWarning("360查询无结果");
        }
      });
    },
    /** é‡ç½®å¯†ç æŒ‰é’®æ“ä½œ */
    handleResetPwd(row) {
      this.$prompt('请输入"' + row.userName + '"的新密码', "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        closeOnClickModal: false,
        inputPattern: /^.{5,20}$/,
        inputErrorMessage: "用户密码长度必须介于 5 å’Œ 20 ä¹‹é—´",
      })
        .then(({ value }) => {
          resetUserPwd(row.userId, value).then((response) => {
            this.$modal.msgSuccess("修改成功,新密码是:" + value);
          });
        })
        .catch(() => {});
    },
    // å–消按钮
    cancel() {
      this.Labelchange = false;
      this.reset();
    },
    /** æäº¤æŒ‰é’® */
    submitForm: function () {
      this.$refs["form"].validate((valid) => {
        if (valid) {
          this.form.isoperation = 2;
          this.form.notrequiredFlag = 1;
          alterpatient(this.form)
            .then((response) => {
              console.log(response);
            })
            .then(() => {
              this.getList(1);
              this.$modal.msgSuccess("患者过滤成功");
            });
          this.reset();
          this.Labelchange = false;
        }
      });
    },
    /** åˆ é™¤æŒ‰é’®æ“ä½œ */
    handleDelete(row) {
      const userIds = row.userId || this.ids;
      this.$modal
        .confirm('是否确认删除用户编号为"' + userIds + '"的数据项?')
        .then(function () {
          return delUser(userIds);
        })
        .then(() => {
          this.getList(1);
          this.$modal.msgSuccess("删除成功");
        })
        .catch(() => {});
    },
    // å…¨éƒ¨åœæ­¢
    AllStop() {
      this.$modal
        .confirm("是否停止全部任务?")
        .then(function () {
          return console.log("停止成功");
        })
        .then(() => {
          this.getList(1);
          this.$modal.msgWarning("停止成功");
        })
        .catch(() => {});
    },
    // å…¨éƒ¨å¼€å§‹
    AllStarted() {
      this.$modal
        .confirm("是否开启全部任务?")
        .then(function () {
          return console.log("开启成功");
        })
        .then(() => {
          this.getList(1);
          this.$modal.msgSuccess("开启成功");
        })
        .catch(() => {});
    },
    // ä»»åŠ¡é‡ç½®
    TaskReset() {
      this.$modal
        .confirm("是否重置选中的任务项?")
        .then(function () {
          return console.log("选中成功");
        })
        .then(() => {
          this.getList(1);
          this.$modal.msgSuccess("重置成功");
        })
        .catch(() => {});
    },
    // è®¾ç½®å‘送时间
    Sendtimesetting() {
      this.modificationVisible = true;
    },
    // è·³è½¬è¯¦æƒ…页
    Seedetails(row) {
      let type = "";
      console.log(row, "rwo");
      if (row.type == 1) {
        type = 1;
      }
      this.$router.push({
        path: "/followvisit/ContinueFordetails/",
        query: {
          taskid: row.taskid,
          patid: row.patid,
          id: row.id,
          Voicetype: type,
          visitCount: this.topqueryParams.visitCount,
        },
      });
    },
    // å†æ¬¡éšè®¿
    followupvisit(row) {
      this.zcform = row;
      this.zcform.endtime = this.formatTime(this.zcform.endtime);
      this.dialogFormVisible = true;
    },
    onSubmit() {},
    // æš‚停服务
    handlestop(row) {
      let objson = row;
      this.$modal
        .confirm(
          '是否确认暂停任务名称为"' +
            row.taskName +
            '患者名称为"' +
            row.sendname +
            '"的数据项?'
        )
        .then(() => {
          getTaskservelist({
            patid: row.patid,
            taskid: row.taskid,
          }).then((res) => {
            if (res.code == 200) {
              objson.sendstate = 4;
              objson.remark = "服务暂停";
              Editsingletaskson(objson).then((res) => {
                if (res.code) {
                  this.$modal.msgSuccess("记录成功");
                  this.getList(1);
                }
              });
            }
          });
        })
        .catch(() => {});
    },
    // æ‚£è€…过滤触发
    handleUpdate(row) {
      particularpatient(row.patid).then((response) => {
        this.form = response.data;
        this.form.filterDrname = store.getters.nickName;
      });
      this.amendtag = true;
      this.Labelchange = true;
    },
    // ä¾¿æ·æŒ‰é’®
    toleadExport(too) {
      if (too == 1) {
        this.topqueryParams.sendstate = 4;
        this.topqueryParams.excep = null;
      } else if (too == 2) {
        this.topqueryParams.excep = 1;
      }
      this.handleQuery();
    },
    /** å¯¼å‡ºæŒ‰é’®æ“ä½œ */
    handleExport() {
      this.topqueryParams.pageNum = null;
      this.topqueryParams.pageSize = null;
      this.download(
        "smartor/serviceSubtask/patItemExport",
        {
          ...this.topqueryParams,
        },
        `user_${new Date().getTime()}.xlsx`
      );
    },
    // å¼‚常列渲染
    tableRowClassName({ row, rowIndex }) {
      if (row.excep == 1) {
        return "warning-row";
      } else if (row.excep == 2) {
        return "remind-row";
      }
      return "";
    },
    // åˆ›å»ºå†æ¬¡éšè®¿æœåŠ¡
    setupsubtask() {
      this.$refs["zcform"].validate((valid) => {
        if (valid) {
          this.zcform.remark =
            this.zcform.remark + "【" + this.getCurrentTime() + "】";
          let form = structuredClone(this.zcform);
          form.visitTime = this.formatTime(form.date1);
          form.finishtime = "";
          if (form.resource) {
            if (form.resource == 2) {
              form.serviceType = 13;
            }
          } else {
            this.$modal.msgError("未选择随访方式");
          }
          form.id = null;
          form.sendstate = 2;
          form.preachform = form.preachformson;
          form.longTask = 0;
          addserviceSubtask(form).then((res) => {
            if (res.code == 200) {
              this.$modal.msgSuccess("创建成功");
            } else {
              this.$modal.msgError("创建失败");
            }
            this.dialogFormVisible = false;
          });
        }
      });
    },
    getCurrentTime() {
      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");
      const hours = String(now.getHours()).padStart(2, "0");
      const minutes = String(now.getMinutes()).padStart(2, "0");
      const seconds = String(now.getSeconds()).padStart(2, "0");
      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
    },
  },
};
</script>
<style lang="scss" scoped>
.el-button--primary.is-plain {
  color: #ffffff;
  background: #409eff;
  border-color: #4fabe9;
}
.document {
  // width: 100px;
  height: 50px;
}
::v-deep.el-table .warning-row {
  background: #eec4c4;
}
::v-deep.el-table .remind-row {
  background: #fcf5aa;
}
.documentf {
  display: flex;
  justify-content: flex-end;
}
.download {
  text-align: center;
  .el-upload__tip {
    font-size: 23px;
  }
  .el-upload__text {
    font-size: 23px;
  }
}
.uploading {
  margin-top: 20px;
  margin: 20px;
  padding: 30px;
  background: #ffffff;
  border: 1px solid #dcdfe6;
  -webkit-box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12),
    0 0 6px 0 rgba(0, 0, 0, 0.04);
}
.el-tag + .el-tag {
  margin-left: 10px;
}
.button-new-tag {
  margin-left: 10px;
  height: 32px;
  line-height: 30px;
  padding-top: 0;
  padding-bottom: 0;
}
.input-new-tag {
  width: 90px;
  margin-left: 10px;
  vertical-align: bottom;
}
.drexamine {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 30px;
  background: #daeaf5;
  img {
    width: 100px;
    height: 100px;
  }
}
.qrcode-dialo {
  // text-align: center;
  //   display: flex;
  margin: 20px;
  padding: 30px;
  background: #edf1f7;
  border: 1px solid #dcdfe6;
  -webkit-box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12),
    0 0 6px 0 rgba(0, 0, 0, 0.04);
  .topic-dev {
    margin-bottom: 25px;
    font-size: 20px !important;
    .dev-text {
      margin-bottom: 10px;
    }
  }
}
::v-deep.leftvlue .el-card__body {
  background: #f2f8ff;
  color: #324a9b;
}
::v-deep.leftvlue .el-card__body:hover {
  background: #3664d9;
  color: #fff;
  cursor: pointer; /* é¼ æ ‡æ‚¬æµ®æ—¶å˜ä¸ºæ‰‹å½¢ */
}
::v-deep.errleftvlue .el-card__body {
  background: #fdd0d7;
}
::v-deep.errleftvlue .el-card__body:hover {
  background: #f88d96;
  cursor: pointer; /* é¼ æ ‡æ‚¬æµ®æ—¶å˜ä¸ºæ‰‹å½¢ */
}
::v-deep.jgleftvlue .el-card__body:hover {
  background: #f7f075;
  cursor: pointer; /* é¼ æ ‡æ‚¬æµ®æ—¶å˜ä¸ºæ‰‹å½¢ */
}
::v-deep.ysfleftvlue .el-card__body {
  background: #d0fdd8;
}
::v-deep.ysfleftvlue .el-card__body:hover {
  background: #0abc54;
  cursor: pointer; /* é¼ æ ‡æ‚¬æµ®æ—¶å˜ä¸ºæ‰‹å½¢ */
}
.button-bb {
  font-weight: 500;
  background-color: #2ba05c;
  padding: 5px;
  border-radius: 1px;
  color: #ffffff;
}
.button-xq {
  font-weight: 500;
  background-color: #409eff;
  padding: 5px;
  border-radius: 1px;
  color: #ffffff;
}
.button-sc {
  font-weight: 500;
  background-color: #b3a21f;
  padding: 5px;
  border-radius: 1px;
  color: #ffffff;
}
.button-zx {
  background: #324a9b;
  padding: 5px;
  border-radius: 1px;
  color: #ffffff;
}
::v-deep.el-radio-group {
  span {
    font-size: 24px;
  }
}
.purple-button {
  background-color: #7e22ce;
  border-color: #7e22ce;
  color: #fff;
}
.purple-button:hover,
.purple-button:focus {
  background-color: #9333ea;
  border-color: #9333ea;
}
.purple-button:active {
  background-color: #6b21a8;
  border-color: #6b21a8;
}
.button-textxga {
  color: #de7897;
}
.purple-button.is-disabled {
  background-color: #d8b4fe;
  border-color: #d8b4fe;
  opacity: 1; /* ä¿æŒç¦ç”¨çŠ¶æ€é€æ˜Žåº¦ */
}
// é€‰é¡¹å­—体放大
// ::v-deep.el-checkbox-group {
//   span {
//     font-size: 24px;
//   }
// }
</style>
src/views/followvisit/HistoricalFollow/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1327 @@
<template>
  <!-- åŽ†å²éšè®¿è®°å½• -->
  <div class="app-container">
    <div class="leftvlue" style="margin-bottom: 20px"></div>
    <el-row :gutter="20">
      <!--用户数据-->
      <el-form
        :model="topqueryParams"
        ref="queryForm"
        size="small"
        :inline="true"
        v-show="showSearch"
        label-width="98px"
      >
        <el-form-item label="任务名称">
          <el-input
            v-model="topqueryParams.taskName"
            placeholder="请选择任务名称"
          ></el-input>
        </el-form-item>
        <el-form-item label="患者姓名" prop="sendname">
          <el-input
            v-model="topqueryParams.sendname"
            placeholder="请输入患者姓名"
          ></el-input>
        </el-form-item>
        <el-form-item label="诊断名称" prop="leavediagname">
          <el-input
            v-model="topqueryParams.leavediagname"
            placeholder="请输入诊断名称"
          ></el-input>
        </el-form-item>
        <el-form-item label="任务状态" prop="status">
          <el-select v-model="topqueryParams.sendstate" placeholder="请选择">
            <el-option
              v-for="item in topicoptions"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            >
            </el-option>
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button
            type="primary"
            icon="el-icon-search"
            size="medium"
            @click="handleQuery(1)"
            >搜索</el-button
          >
          <el-button icon="el-icon-refresh" size="medium" @click="resetQuery"
            >重置</el-button
          >
        </el-form-item>
      </el-form>
      <el-divider></el-divider>
      <el-row :gutter="10" class="mb8">
        <el-col :span="1.5">
          <div class="documentf">
            <div class="document">
              <el-button
                type="warning"
                plain
                icon="el-icon-upload2"
                size="medium"
                @click="handleExport"
                >导出</el-button
              >
            </div>
          </div>
        </el-col>
      </el-row>
      <el-table
        v-loading="loading"
        :data="userList"
        :row-class-name="tableRowClassName"
        @selection-change="handleSelectionChange"
      >
        <el-table-column type="selection" width="50" align="center" />
        <el-table-column
          label="任务名称"
          fixed
          align="center"
          key="taskName"
          prop="taskName"
          width="180"
        />
        <!-- <el-table-column label="序号" fixed align="center" key="id" prop="id" /> -->
        <el-table-column
          label="姓名"
          width="100"
          align="center"
          key="sendname"
          prop="sendname"
        >
          <template slot-scope="scope">
            <el-button
              size="medium"
              type="text"
              @click="
                gettoken360(scope.row.sfzh, scope.row.drcode, scope.row.drname)
              "
              ><span class="button-textsc">{{
                scope.row.sendname
              }}</span></el-button
            >
          </template>
        </el-table-column>
        <el-table-column
          label="任务状态"
          align="center"
          key="sendstate"
          prop="sendstate"
          width="120"
        >
          <template slot-scope="scope">
            <el-tooltip
              class="item"
              effect="dark"
              :content="scope.row.remark"
              placement="top-start"
            >
              <div v-if="scope.row.sendstate == 1">
                <el-tag type="primary" :disable-transitions="false"
                  >表单已领取</el-tag
                >
              </div>
              <div v-if="scope.row.sendstate == 2">
                <el-tag type="primary" :disable-transitions="false"
                  >待随访</el-tag
                >
              </div>
              <div v-if="scope.row.sendstate == 3">
                <el-tag type="success" :disable-transitions="false"
                  >表单已发送</el-tag
                >
              </div>
              <div v-if="scope.row.sendstate == 4">
                <el-tag type="info" :disable-transitions="false">不执行</el-tag>
              </div>
              <div v-if="scope.row.sendstate == 5">
                <el-tag type="danger" :disable-transitions="false"
                  >发送失败</el-tag
                >
              </div>
              <div v-if="scope.row.sendstate == 6">
                <el-tag type="danger" :disable-transitions="false"
                  >已完成</el-tag
                >
              </div>
            </el-tooltip>
          </template>
        </el-table-column>
        <el-table-column
          label="诊断名称"
          align="center"
          key="leavediagname"
          prop="leavediagname"
          width="120"
          :show-overflow-tooltip="true"
        >
        </el-table-column>
        <el-table-column
          label="处理意见"
          align="center"
          key="suggest"
          prop="suggest"
          width="120"
        >
          <template slot-scope="scope">
            <dict-tag
              :options="dict.type.sys_suggest"
              :value="scope.row.suggest"
            />
          </template>
        </el-table-column>
        <el-table-column
          label="随访人员"
          align="center"
          key="updateBy"
          prop="updateBy"
          width="120"
        />
        <el-table-column
          label="随访完成时间"
          sortable
          align="center"
          prop="finishtime"
          width="160"
        >
          <template slot-scope="scope">
            <span>{{ parseTime(scope.row.finishtime) }}</span>
          </template>
        </el-table-column>
        <el-table-column
          label="应随访日期"
          width="200"
          align="center"
          key="visitTime"
          prop="visitTime"
        >
          <template slot-scope="scope">
            <span>{{ formatTime(scope.row.visitTime) }}</span>
          </template></el-table-column
        >
        <el-table-column
          label="出院日期"
          width="200"
          align="center"
          key="endtime"
          prop="endtime"
        >
          <template slot-scope="scope">
            <span>{{ formatTime(scope.row.endtime) }}</span>
          </template></el-table-column
        >
        <el-table-column
          label="出院天数"
          width="120"
          align="center"
          key="endDay"
          prop="endDay"
        >
          <template slot-scope="scope">
            <span>{{ scope.row.endDay ? scope.row.endDay + "天" : "" }}</span>
          </template>
        </el-table-column>
        <el-table-column
          label="身份证号码"
          width="200"
          align="center"
          key="sfzh"
          prop="sfzh"
        />
        <el-table-column
          label="联系电话"
          width="200"
          align="center"
          key="phone"
          prop="phone"
        />
        <el-table-column
          label="责任护士"
          width="120"
          align="center"
          key="nurseName"
          prop="nurseName"
        />
        <el-table-column
          label="主治医生"
          width="120"
          align="center"
          key="drname"
          prop="drname"
        />
        <!-- <el-table-column
          label="病历号"
          align="center"
          sortable
          key="medicalRecordNo"
          prop="medicalRecordNo"
          width="120"
        /> -->
        <!-- <el-table-column label="年龄" align="center" key="age" prop="age" /> -->
        <!-- <el-table-column label="性别"width="100" align="center" key="sex" prop="sex" /> -->
        <!-- <el-table-column label="床号" align="center" key="badNo" prop="badNo" /> -->
        <el-table-column
          label="科室"
          align="center"
          key="deptname"
          prop="deptname"
          width="120"
        >
        </el-table-column>
        <el-table-column
          label="病区"
          align="center"
          key="leavehospitaldistrictname"
          prop="leavehospitaldistrictname"
          width="120"
        >
        </el-table-column>
        <!-- <el-table-column
          label="疾病名称"
          align="center"
          key="icdName"
          prop="icdName"
          width="120"
          :show-overflow-tooltip="true"
        >
        </el-table-column> -->
        <el-table-column
          label="出院随访模板名称"
          align="center"
          key="templatename"
          prop="templatename"
          width="200"
        />
        <el-table-column
          label="任务执行方式"
          align="center"
          key="preachform"
          prop="preachform"
          width="160"
          :show-overflow-tooltip="true"
        >
          <template slot-scope="scope">
            <span v-for="item in scope.row.preachform">{{ item }}、 </span>
          </template>
        </el-table-column>
        <el-table-column
          label="任务结果说明"
          width="200"
          align="center"
          key="remark"
          prop="remark"
        >
          <template slot-scope="scope" v-if="scope.row.remark">
            <el-tag
              type="warning"
              v-if="scope.row.sendstate != 5 && scope.row.sendstate != 4"
              >{{ scope.row.remark }}</el-tag
            >
            <el-tag type="warning" v-else>{{ scope.row.remark }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column
          label="操作"
          align="center"
          fixed="right"
          width="200"
          class-name="small-padding fixed-width"
        >
          <template slot-scope="scope">
            <el-button size="medium" type="text" @click="Seedetails(scope.row)"
              ><span class="button-zx"
                ><i class="el-icon-s-order"></i>查看详情</span
              ></el-button
            >
          </template>
        </el-table-column>
      </el-table>
      <pagination
        v-show="total > 0"
        :total="total"
        :page.sync="topqueryParams.pageNum"
        :limit.sync="topqueryParams.pageSize"
        @pagination="getList"
      />
    </el-row>
    <!-- æ·»åŠ æˆ–ä¿®æ”¹å½±åƒéšè®¿å¯¹è¯æ¡† -->
    <el-dialog
      :title="title"
      :visible.sync="addalteropen"
      width="700px"
      append-to-body
    >
      <el-form ref="form" :model="form" label-width="100px">
        <el-row :gutter="20">
          <el-col :span="12"
            ><el-form-item label="任务名称">
              <el-input v-model="form.name"></el-input> </el-form-item
          ></el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="24"
            ><el-form-item label="所属科室">
              <el-select v-model="form.region" placeholder="请选择科室">
                <el-option label="区域一" value="shanghai"></el-option>
                <el-option label="区域二" value="beijing"></el-option>
              </el-select> </el-form-item></el-col
        ></el-row>
        <el-row :gutter="20">
          <el-col :span="24"
            ><el-form-item label="随访类型">
              <el-select v-model="form.region" placeholder="请选择随访类型">
                <el-option label="区域一" value="shanghai"></el-option>
                <el-option label="区域二" value="beijing"></el-option>
              </el-select> </el-form-item
          ></el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="24">
            <el-form-item label="服务模块">
              <el-select v-model="form.region" placeholder="请选择模块">
                <el-option label="区域一" value="shanghai"></el-option>
                <el-option label="区域二" value="beijing"></el-option>
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="24">
            <el-form-item label="影像随访要求">
              <el-input type="textarea" v-model="form.desc"></el-input>
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="submitForm">提 äº¤</el-button>
        <el-button @click="cancel">返 å›ž</el-button>
      </div>
    </el-dialog>
    <!-- ä¿®æ”¹å‘送时间对话框 -->
    <!-- å†æ¬¡éšè®¿ -->
    <el-dialog title="患者再次随访" :visible.sync="dialogFormVisible">
      <el-form ref="form" :model="zcform" label-width="80px">
        <el-form-item label="患者名称">
          <el-input style="width: 400px" v-model="zcform.name"></el-input>
        </el-form-item>
        <el-form-item label="任务名称">
          <el-input style="width: 400px" v-model="zcform.name"></el-input>
        </el-form-item>
        <el-form-item label="随访方式">
          <el-radio-group v-model="zcform.resource">
            <el-radio label="1">本病区随访</el-radio>
            <el-radio label="2">随访中心随访</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="即刻发送">
          <el-switch v-model="zcform.delivery"></el-switch>
        </el-form-item>
        <el-form-item label="随访时间" v-if="!zcform.delivery">
          <el-col :span="11">
            <el-date-picker
              type="date"
              placeholder="选择日期"
              v-model="zcform.date1"
              style="width: 100%"
            ></el-date-picker>
          </el-col>
          <el-col class="line" :span="2">-</el-col>
          <el-col :span="11">
            <el-time-picker
              placeholder="选择时间"
              v-model="zcform.date2"
              style="width: 100%"
            ></el-time-picker>
          </el-col>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="dialogFormVisible = false">取 æ¶ˆ</el-button>
        <el-button type="primary" @click="dialogFormVisible = false"
          >ç¡® å®š</el-button
        >
      </div>
    </el-dialog>
  </div>
</template>
<script>
import {
  listUser,
  getUser,
  delUser,
  addUser,
  updateUser,
  resetUserPwd,
  changeUserStatus,
} from "@/api/system/user";
import {
  historservelist,
  buidegetTasklist,
  query360PatInfo,
} from "@/api/AiCentre/index";
import Treeselect from "@riophae/vue-treeselect";
import store from "@/store";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
export default {
  name: "Discharge",
  dicts: ["sys_normal_disable", "sys_user_sex", "sys_yujing", "sys_suggest"],
  components: { Treeselect },
  data() {
    return {
      // é®ç½©å±‚
      loading: true,
      // é€‰ä¸­æ•°ç»„
      ids: [],
      // éžå•个禁用
      single: true,
      // éžå¤šä¸ªç¦ç”¨
      multiple: true,
      // æ˜¾ç¤ºæœç´¢æ¡ä»¶
      showSearch: true,
      dialogFormVisible: false,
      // æ€»æ¡æ•°
      total: 0,
      // ç”¨æˆ·è¡¨æ ¼æ•°æ®
      userList: null,
      // å¼¹å‡ºå±‚标题
      title: "新增影像随访",
      // æ˜¯å¦æ˜¾ç¤ºä¿®æ”¹ã€æ·»åŠ å¼¹å‡ºå±‚
      addalteropen: false,
      // ä¿®æ”¹å‘送时间对话框
      modificationVisible: false,
      // éƒ¨é—¨åç§°
      deptName: undefined,
      // é»˜è®¤å¯†ç 
      initPassword: undefined,
      // æ—¥æœŸèŒƒå›´
      dateRange: [],
      // å²—位选项
      postOptions: [],
      ruleForm: {
        type: [],
      },
      postData: {
        XiaoXiTou: {
          FaSongFCSJC: "ZJHES",
          FaSongJGID: localStorage.getItem("orgid"),
          FaSongJGMC: localStorage.getItem("orgname"),
          FaSongSJ: "2025-01-09 17:29:36",
          FaSongXTJC: "SUIFANGXT",
          FaSongXTMC: "随访系统",
          XiaoXiID: "5FA92AFB-9833-4608-87C7-F56A654AC171",
          XiaoXiLX: "SC_LC_360STCX",
          XiaoXiMC: "360 视图查询",
          ZuHuID: localStorage.getItem("ZuHuID"),
          ZuHuMC: localStorage.getItem("orgname"),
        },
        YeWuXX: {
          BingRenXX: {
            ZhengJianHM: "",
            ZhengJianLXDM: "01",
            ZhengJianLXMC: "居民身份证",
            ZuZhiJGID: localStorage.getItem("orgid"),
            ZuZhiJGMC: localStorage.getItem("orgname"),
          },
          YongHuXX: {
            XiTongID: "SUIFANGXT",
            XiTongMC: "随访系统",
            YongHuID: localStorage.getItem("YongHuID"),
            YongHuXM: localStorage.getItem("YongHuXM"),
            ZuZhiJGID: localStorage.getItem("orgid"),
            ZuZhiJGMC: localStorage.getItem("orgname"),
            idp: "lyra",
          },
        },
      },
      zcform: {},
      dynamicTags: ["选项一", "选项二", "选项三"], //选项
      inputVisible: false,
      ycvalue: "",
      yfsvalue: "",
      inputValue: "",
      preachform: "",
      previewVisible: false, //影像随访预览弹框
      radio: "",
      radios: [],
      previewtype: 2, //预览影像随访类型
      total: 0, // æ€»æ¡æ•°
      ImportQuantity: 999, //导影像随访数量
      //预览影像随访信息
      previewvalue: {
        username: "这个医生对你怎么样",
      },
      value: [],
      list: [],
      sourcetype: [
        {
          value: 1,
          label: "科室",
          children: [],
        },
        {
          value: 2,
          label: "病区",
          children: [],
        },
        {
          value: 3,
          label: "全部",
        },
      ],
      loading: false,
      cardlist: [
        {
          name: "出院服务总量",
          value: 0,
        },
        // {
        //   name: "患者过滤",
        //   value: 0,
        // },
        {
          name: "需随访",
          value: 0,
        },
        // {
        //   name: "异常",
        //   value: 0,
        // },
        {
          name: "发送失败",
          value: 0,
        },
        {
          name: "待随访",
          value: 0,
        },
        // {
        //   name: "已发送",
        //   value: 0,
        // },
        // {
        //   name: "表单已发送",
        //   value: 0,
        // },
      ],
      // è¡¨å•参数
      form: {
        phonenumber: "",
        totagid: "",
        types: "",
        nickName: "",
        qystatus: "",
        btstatus: "",
      },
      // æŸ¥è¯¢å‚æ•°
      topqueryParams: {
        pageNum: 1,
        pageSize: 10,
      },
      propss: { multiple: true },
      options: [],
      topicoptions: [
        {
          value: null,
          label: "全部",
        },
        {
          value: 1,
          label: "表单已领取",
        },
        {
          value: 2,
          label: "待随访",
        },
        {
          value: 3,
          label: "表单已发送",
        },
        {
          value: 4,
          label: "不执行",
        },
         {
          value: 5,
          label: "发送失败",
        },
        {
          value: 6,
          label: "已完成",
        },
         {
          value: 7,
          label: "超时",
        },
      ],
      topicoptionsyj: [
        {
          value: 1,
          label: "异常",
        },
        {
          value: 0,
          label: "正常",
        },
      ],
      topicoptionssort: [
        {
          value: 0,
          label: "出院时间(正序)",
        },
        {
          value: 1,
          label: "出院时间(倒序)",
        },
        {
          value: 2,
          label: "发送时间(正序)",
        },
        {
          value: 3,
          label: "发送时间(倒序)",
        },
        {
          value: 7,
          label: "应随访日期(正序)",
        },
        {
          value: 8,
          label: "应随访日期(倒序)",
        },
      ],
      errtype: "",
      leavehospitaldistrictcode: "",
      serviceState: [],
      checkboxlist: [],
      // è¡¨å•校验
      rules: {},
    };
  },
  watch: {},
  created() {
    this.serviceState = store.getters.serviceState;
    this.checkboxlist = store.getters.checkboxlist;
    this.errtype = this.$route.query.errtype;
    this.leavehospitaldistrictcode =
      this.$route.query.leavehospitaldistrictcode;
    this.sourcetype[0].children = store.getters.belongDepts.map((dept) => {
      return {
        label: dept.deptName,
        value: dept.deptCode,
      };
    });
    this.sourcetype[1].children = store.getters.belongWards.map((dept) => {
      return {
        label: dept.districtName,
        value: dept.districtCode,
      };
    });
    if (this.errtype) {
      this.toleadExport(2);
    } else {
      this.getList(1);
    }
    this.getConfigKey("sys.user.initPassword").then((response) => {
      this.initPassword = response.msg;
    });
  },
  activated() {
    this.getList(1);
  },
  methods: {
    /** æŸ¥è¯¢å½±åƒéšè®¿æœåŠ¡åˆ—è¡¨ */
    getList(refresh) {
      // é»˜è®¤å…¨éƒ¨
      this.loading = true;
      historservelist(this.topqueryParams).then((response) => {
        // this.userList = response.rows[0].serviceSubtaskList;
        this.userList = response?.rows ?? [];
        this.total = response.total;
        this.loading = false;
        this.userList.forEach((item) => {
          let idArray = null;
          if (item.endtime) {
            item.endDay = this.daysBetween(item.endtime);
          }
          if (item.preachform) {
            if (item.endtime) {
              item.preachformson = item.preachform;
              idArray = item.preachform.split(",");
            }
            item.preachform = idArray.map((value) => {
              // æŸ¥æ‰¾id对应的对象
              const item = this.checkboxlist.find(
                (item) => item.value == value
              );
              // å¦‚果找到对应的id,返回label值,否则返回null
              return item ? item.label : null;
            });
          }
        });
        this.total = response.total;
      });
    },
    //患者360跳转
    gettoken360(sfzh, drcode, drname) {
      // this.$modal.msgWarning('360功能暂未开通');
      this.postData.YeWuXX.BingRenXX.ZhengJianHM = sfzh;
      query360PatInfo(this.postData).then((res) => {
        if (res.data.url) {
          window.open(res.data.url, "_blank");
          // this.linkUrl = res.data.url;
        } else {
          this.$modal.msgWarning("360查询无结果");
        }
      });
    },
    buidegetTasklist(type) {
      if (this.topqueryParams.searchscope == 3) {
        this.topqueryParams.leaveldeptcodes = store.getters.belongDepts.map(
          (obj) => obj.deptCode
        );
        this.topqueryParams.leavehospitaldistrictcodes =
          store.getters.belongWards.map((obj) => obj.districtCode);
      }
      // æŽ¥å—异常跳转
      if (this.errtype) {
        this.topqueryParams.leavehospitaldistrictcodes.push(
          this.leavehospitaldistrictcode
        );
      }
      let obj = {
        pageNum: 1,
        pageSize: 10,
        leavehospitaldistrictcodes:
          this.topqueryParams.leavehospitaldistrictcodes,
        sendstates: [2, 3],
        leaveldeptcodes: this.topqueryParams.leaveldeptcodes,
      };
      buidegetTasklist(obj).then((response) => {
        this.userList = response.rows[0].serviceSubtaskList;
        this.total = response.total;
        if (refresh) {
          this.cardlist[0].value =
            Number(response.rows[0].wzx) + Number(response.rows[0].ysf);
          this.cardlist[1].value = response.rows[0].wzx;
          this.cardlist[2].value = response.rows[0].ysf;
          this.ycvalue = response.rows[0].yc;
          this.cardlist[3].value = response.rows[0].fssb;
          this.cardlist[4].value = response.rows[0].dsf;
          // this.cardlist[5].value = response.rows[0].yfs2;
          this.yfsvalue = response.rows[0].yfs;
        }
        this.loading = false;
        this.userList.forEach((item) => {
          let idArray = null;
          if (item.endtime) {
            item.endDay = this.daysBetween(item.endtime);
          }
          if (item.preachform) {
            if (item.endtime) {
              item.preachformson = item.preachform;
              idArray = item.preachform.split(",");
            }
            item.preachform = idArray.map((value) => {
              // æŸ¥æ‰¾id对应的对象
              const item = this.checkboxlist.find(
                (item) => item.value == value
              );
              // å¦‚果找到对应的id,返回label值,否则返回null
              return item ? item.label : null;
            });
          }
        });
        this.total = response.total;
      });
    },
    // æŸ¥çœ‹é—¨è¯Šéšè®¿è¯¦æƒ…
    Referencequestion(row) {
      this.previewVisible = true;
    },
    // æ·»åŠ å¼¹æ¡†æœç´¢
    remoteMethod(query) {
      if (query !== "") {
        this.loading = true;
        setTimeout(() => {
          this.loading = false;
          this.options = this.list.filter((item) => {
            return item.label.toLowerCase().indexOf(query.toLowerCase()) > -1;
          });
        }, 200);
      } else {
        this.options = [];
      }
    },
    // å½±åƒéšè®¿çŠ¶æ€ä¿®æ”¹
    handleStatusChange(row) {
      let text = row.status === "0" ? "启用" : "停用";
      this.$modal
        .confirm('确认要"' + text + '""' + row.userName + '"用户吗?')
        .then(function () {
          return changeUserStatus(row.userId, row.status);
        })
        .then(() => {
          this.$modal.msgSuccess(text + "成功");
        })
        .catch(function () {
          row.status = row.status === "0" ? "1" : "0";
        });
    },
    // å–消按钮
    cancel() {
      this.addalteropen = false;
      this.reset();
    },
    // è¡¨å•重置
    reset() {
      this.form = {
        userId: undefined,
        deptId: undefined,
        userName: undefined,
        nickName: undefined,
        password: undefined,
        phonenumber: undefined,
        email: undefined,
        sex: undefined,
        status: "0",
        remark: undefined,
        postIds: [],
        roleIds: [],
      };
      this.resetForm("form");
    },
    /** æœç´¢æŒ‰é’®æ“ä½œ */
    handleQuery(refresh) {
      if (this.topqueryParams.searchscope == 3) {
        this.topqueryParams.leaveldeptcodes = store.getters.belongDepts.map(
          (obj) => obj.deptCode
        );
        this.topqueryParams.leavehospitaldistrictcodes =
          store.getters.belongWards.map((obj) => obj.districtCode);
      }
      this.topqueryParams.pageNum = 1;
      this.topqueryParams.startOutHospTime = this.dateRange[0];
      this.topqueryParams.endOutHospTime = this.dateRange[1];
      this.getList(refresh);
    },
    // æ‚£è€…范围处理
    handleChange(value) {
      let type = value[0];
      let code = value.slice(-1)[0];
      this.topqueryParams.leavehospitaldistrictcodes = [];
      this.topqueryParams.leaveldeptcodes = [];
      if (type == 1) {
        this.topqueryParams.leaveldeptcodes.push(code);
        this.topqueryParams.leavehospitaldistrictcodes = [];
        this.topqueryParams.searchscope = 1;
      } else if (type == 2) {
        this.topqueryParams.leavehospitaldistrictcodes.push(code);
        this.topqueryParams.leaveldeptcodes = [];
        this.topqueryParams.searchscope = 2;
      } else {
        this.topqueryParams.searchscope = 3;
      }
    },
    /** é‡ç½®æŒ‰é’®æ“ä½œ */
    resetQuery() {
      this.dateRange = [];
      this.topqueryParams = {
        pageNum: 1,
        pageSize: 10,
        serviceType: 13,
        sort: 2,
        searchscope: 2,
        sendstate: 2,
        scopetype: [],
        leaveldeptcodes: [],
        leavehospitaldistrictcodes: [],
      };
      this.handleQuery(1);
    },
    // å¤šé€‰æ¡†é€‰ä¸­æ•°æ®
    handleSelectionChange(selection) {
      this.ids = selection.map((item) => item.userId);
      this.single = selection.length != 1;
      this.multiple = !selection.length;
    },
    //删除选项
    handleClose(tag) {
      this.dynamicTags.splice(this.dynamicTags.indexOf(tag), 1);
    },
    //触发新增输入
    showInput() {
      this.inputVisible = true;
      this.$nextTick((_) => {
        this.$refs.saveTagInput.$refs.input.focus();
      });
    },
    onthatday() {
      this.topqueryParams.startSendDateTime = this.getCurrentDate();
      this.topqueryParams.endSendDateTime = this.getCurrentDate();
      this.getList(1);
    },
    getCurrentDate() {
      const now = new Date();
      return now.toISOString().slice(0, 10); // æˆªå–前10个字符,即 YYYY-MM-DD
    },
    //获取失去焦点触发
    handleInputConfirm() {
      let inputValue = this.inputValue;
      if (inputValue) {
        this.dynamicTags.push(inputValue);
      }
      this.inputVisible = false;
      this.inputValue = "";
    },
    /** æ–°å¢žæŒ‰é’®æ“ä½œ */
    handleAdd() {
      this.$router.push({
        path: "/followvisit/QuestionnaireTask",
        query: {
          type: 2,
          serviceType: 13,
        },
      });
    },
    /** é‡ç½®å¯†ç æŒ‰é’®æ“ä½œ */
    handleResetPwd(row) {
      this.$prompt('请输入"' + row.userName + '"的新密码', "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        closeOnClickModal: false,
        inputPattern: /^.{5,20}$/,
        inputErrorMessage: "用户密码长度必须介于 5 å’Œ 20 ä¹‹é—´",
      })
        .then(({ value }) => {
          resetUserPwd(row.userId, value).then((response) => {
            this.$modal.msgSuccess("修改成功,新密码是:" + value);
          });
        })
        .catch(() => {});
    },
    /** æäº¤æŒ‰é’® */
    submitForm: function () {
      this.$refs["form"].validate((valid) => {
        if (valid) {
          if (this.form.userId != undefined) {
            updateUser(this.form).then((response) => {
              this.$modal.msgSuccess("修改成功");
              this.open = false;
              this.getList(1);
            });
          } else {
            addUser(this.form).then((response) => {
              this.$modal.msgSuccess("新增成功");
              this.open = false;
              this.getList(1);
            });
          }
        }
      });
    },
    /** åˆ é™¤æŒ‰é’®æ“ä½œ */
    handleDelete(row) {
      const userIds = row.userId || this.ids;
      this.$modal
        .confirm('是否确认删除用户编号为"' + userIds + '"的数据项?')
        .then(function () {
          return delUser(userIds);
        })
        .then(() => {
          this.getList(1);
          this.$modal.msgSuccess("删除成功");
        })
        .catch(() => {});
    },
    // å…¨éƒ¨åœæ­¢
    AllStop() {
      this.$modal
        .confirm("是否停止全部任务?")
        .then(function () {
          return console.log("停止成功");
        })
        .then(() => {
          this.getList(1);
          this.$modal.msgWarning("停止成功");
        })
        .catch(() => {});
    },
    // å…¨éƒ¨å¼€å§‹
    AllStarted() {
      this.$modal
        .confirm("是否开启全部任务?")
        .then(function () {
          return console.log("开启成功");
        })
        .then(() => {
          this.getList(1);
          this.$modal.msgSuccess("开启成功");
        })
        .catch(() => {});
    },
    // ä»»åŠ¡é‡ç½®
    TaskReset() {
      this.$modal
        .confirm("是否重置选中的任务项?")
        .then(function () {
          return console.log("选中成功");
        })
        .then(() => {
          this.getList(1);
          this.$modal.msgSuccess("重置成功");
        })
        .catch(() => {});
    },
    // è®¾ç½®å‘送时间
    Sendtimesetting() {
      this.modificationVisible = true;
    },
    // è·³è½¬è¯¦æƒ…页
    Seedetails(row) {
      let type = "";
      console.log(row, "rwo");
      if (row.type == 1) {
        type = 1;
      }
      this.$router.push({
        path: "/followvisit/record/detailpage/",
        query: {
          taskid: row.taskid,
          patid: row.patid,
          id: row.id,
          Voicetype: type,
          again: 1,
        },
      });
    },
    // å†æ¬¡éšè®¿
    followupvisit() {
      this.dialogFormVisible = true;
    },
    onSubmit() {},
    // ä¾¿æ·æŒ‰é’®
    toleadExport(too) {
      if (too == 1) {
        this.topqueryParams.sendstate = 4;
        this.topqueryParams.excep = null;
      } else if (too == 2) {
        this.topqueryParams.excep = 1;
      }
      this.handleQuery();
    },
    /** å¯¼å‡ºæŒ‰é’®æ“ä½œ */
    handleExport() {
      console.log(this.topqueryParams);
      this.download(
        // "smartor/serviceSubtask/export",
        "smartor/serviceSubtask/getSubtaskByDiagnameExport",
        {
          ...this.topqueryParams,
        },
        `user_${new Date().getTime()}.xlsx`
      );
    },
    // å¼‚常列渲染
    tableRowClassName({ row, rowIndex }) {
      if (row.excep == 1) {
        return "warning-row";
      }
      return "";
    },
  },
};
</script>
<style lang="scss" scoped>
.el-button--primary.is-plain {
  color: #ffffff;
  background: #409eff;
  border-color: #4fabe9;
}
.document {
  // width: 100px;
  height: 50px;
}
::v-deep.el-table .warning-row {
  background: #eec4c4;
}
.documentf {
  display: flex;
  justify-content: flex-end;
}
.download {
  text-align: center;
  .el-upload__tip {
    font-size: 23px;
  }
  .el-upload__text {
    font-size: 23px;
  }
}
.uploading {
  margin-top: 20px;
  margin: 20px;
  padding: 30px;
  background: #ffffff;
  border: 1px solid #dcdfe6;
  -webkit-box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12),
    0 0 6px 0 rgba(0, 0, 0, 0.04);
}
.el-tag + .el-tag {
  margin-left: 10px;
}
.button-new-tag {
  margin-left: 10px;
  height: 32px;
  line-height: 30px;
  padding-top: 0;
  padding-bottom: 0;
}
.input-new-tag {
  width: 90px;
  margin-left: 10px;
  vertical-align: bottom;
}
.drexamine {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 30px;
  background: #daeaf5;
  img {
    width: 100px;
    height: 100px;
  }
}
.qrcode-dialo {
  // text-align: center;
  //   display: flex;
  margin: 20px;
  padding: 30px;
  background: #edf1f7;
  border: 1px solid #dcdfe6;
  -webkit-box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12),
    0 0 6px 0 rgba(0, 0, 0, 0.04);
  .topic-dev {
    margin-bottom: 25px;
    font-size: 20px !important;
    .dev-text {
      margin-bottom: 10px;
    }
  }
}
::v-deep.leftvlue .el-card__body {
  background: #f2f8ff;
  color: #324a9b;
}
::v-deep.leftvlue .el-card__body:hover {
  background: #3664d9;
  color: #fff;
  cursor: pointer; /* é¼ æ ‡æ‚¬æµ®æ—¶å˜ä¸ºæ‰‹å½¢ */
}
::v-deep.errleftvlue .el-card__body {
  background: #fdd0d7;
}
::v-deep.errleftvlue .el-card__body:hover {
  background: #f88d96;
  cursor: pointer; /* é¼ æ ‡æ‚¬æµ®æ—¶å˜ä¸ºæ‰‹å½¢ */
}
::v-deep.ysfleftvlue .el-card__body {
  background: #d0fdd8;
}
::v-deep.ysfleftvlue .el-card__body:hover {
  background: #8df8a4;
  cursor: pointer; /* é¼ æ ‡æ‚¬æµ®æ—¶å˜ä¸ºæ‰‹å½¢ */
}
.button-bb {
  font-weight: 500;
  background-color: #2ba05c;
  padding: 5px;
  border-radius: 1px;
  color: #ffffff;
}
.button-xq {
  font-weight: 500;
  background-color: #409eff;
  padding: 5px;
  border-radius: 1px;
  color: #ffffff;
}
.button-sc {
  font-weight: 500;
  background-color: #b3a21f;
  padding: 5px;
  border-radius: 1px;
  color: #ffffff;
}
.button-zx {
  background: #4fabe9;
  padding: 5px;
  border-radius: 1px;
  color: #ffffff;
}
::v-deep.el-radio-group {
  span {
    font-size: 24px;
  }
}
// é€‰é¡¹å­—体放大
// ::v-deep.el-checkbox-group {
//   span {
//     font-size: 24px;
//   }
// }
</style>
src/views/followvisit/OutpatientAgain/index.vue
@@ -79,7 +79,7 @@
          ></el-input>
        </el-form-item>
        <el-form-item label="门诊时间">
        <el-form-item label="服务时间">
          <el-date-picker
            v-model="dateRange"
            style="width: 240px"
@@ -180,7 +180,7 @@
        <el-col :span="1.5">
          <el-button
            type="primary"
                        icon="el-icon-plus"
            icon="el-icon-plus"
            size="medium"
            @click="handleAdd"
            >新增</el-button
@@ -325,9 +325,14 @@
                  >发送失败</el-tag
                >
              </div>
              <div v-if="scope.row.sendstate == 6">
            <div v-if="scope.row.sendstate == 6">
                <el-tag type="success" :disable-transitions="false"
                  >已完成</el-tag
                >
              </div>
              <div v-if="scope.row.sendstate == 7">
                <el-tag type="danger" :disable-transitions="false"
                  >超时</el-tag
                >
              </div>
            </el-tooltip>
@@ -382,11 +387,11 @@
          label="应随访日期"
          width="200"
          align="center"
          key="longSendTime"
          prop="longSendTime"
          key="visitTime"
          prop="visitTime"
        >
          <template slot-scope="scope">
            <span>{{ formatTime(scope.row.longSendTime) }}</span>
            <span>{{ formatTime(scope.row.visitTime) }}</span>
          </template></el-table-column
        >
        <el-table-column
@@ -398,7 +403,7 @@
        />
        <!-- <el-table-column
          label="门诊天数"
          label="服务天数"
          width="120"
          align="center"
          key="endDay"
@@ -485,7 +490,7 @@
        />
        <el-table-column
          label="门诊随访模板名称"
          label="服务随访模板名称"
          align="center"
          key="templatename"
          prop="templatename"
@@ -644,7 +649,7 @@
            </el-form-item>
          </el-col>
        </el-row>
<el-row >
        <el-row>
          <el-col :span="8">
            <el-form-item label="过滤医生" width="100" prop="filterDrname">
              <el-input
@@ -795,7 +800,7 @@
        <!-- <el-form-item label="即刻发送">
          <el-switch v-model="zcform.delivery"></el-switch>
        </el-form-item> -->
        <el-form-item label="门诊时间">
        <el-form-item label="服务时间">
          <el-input
            style="width: 400px"
            disabled
@@ -919,7 +924,7 @@
      loading: false,
      cardlist: [
        {
          name: "门诊服务总量",
          name: "服务跟踪总量",
          value: 0,
        },
        // {
@@ -968,11 +973,11 @@
      topicoptionssort: [
        {
          value: 0,
          label: "门诊时间(正序)",
          label: "服务时间(正序)",
        },
        {
          value: 1,
          label: "门诊时间(倒序)",
          label: "服务时间(倒序)",
        },
        {
          value: 2,
@@ -996,13 +1001,13 @@
        pageNum: 1,
        pageSize: 10,
        sendstate: 2,
        sort: localStorage.getItem("orgname") == "丽水市中医院" ? 8 : 2, //0 é—¨è¯Šæ—¶é—´(正序)    1 é—¨è¯Šæ—¶é—´(倒序)   2 å‘送时间(正序)    3 å‘送时间(倒序)  7应随访日期(倒序) åº”随访日期(正序)
        sort: localStorage.getItem("orgname") == "丽水市中医院" ? 8 : 2, //0 æœåŠ¡æ—¶é—´(正序)    1 æœåŠ¡æ—¶é—´(倒序)   2 å‘送时间(正序)    3 å‘送时间(倒序)  7应随访日期(倒序) åº”随访日期(正序)
        serviceType: 3,
        searchscope: 3,
        visitCount: 2,
        scopetype: [],
        visitDeptCodes: [],
        leaveldeptcodes:[],
        // leaveldeptcodes:[],
        leavehospitaldistrictcodes: [],
      },
      propss: { multiple: true },
@@ -1029,13 +1034,17 @@
          value: 4,
          label: "不执行",
        },
        {
         {
          value: 5,
          label: "发送失败",
        },
        {
          value: 6,
          label: "已完成",
        },
         {
          value: 7,
          label: "超时",
        },
      ],
      sextype: [
@@ -1141,9 +1150,9 @@
        this.topqueryParams.visitDeptCodes = store.getters.belongDepts.map(
          (obj) => obj.deptCode
        );
        this.topqueryParams.leaveldeptcodes = store.getters.belongDepts.map(
          (obj) => obj.deptCode
        );
        // this.topqueryParams.leaveldeptcodes = store.getters.belongDepts.map(
        //   (obj) => obj.deptCode
        // );
        this.topqueryParams.leavehospitaldistrictcodes =
          store.getters.belongWards.map((obj) => obj.districtCode);
      }
@@ -1157,9 +1166,9 @@
      this.loading = true;
      if (
        this.topqueryParams.leavehospitaldistrictcodes[0] &&
        this.topqueryParams.visitDeptCodes[0]&&this.topqueryParams.leaveldeptcodes[0]
        this.topqueryParams.visitDeptCodes[0]
      ) {
        this.topqueryParams.deptOrDistrict = 2;
        this.topqueryParams.deptOrDistrict = 4;
      } else {
        this.topqueryParams.deptOrDistrict = 1;
      }
@@ -1204,7 +1213,7 @@
      });
    },
    affiliation() {
      this.topqueryParams.managementDoctorCode= store.getters.hisUserId;
      this.topqueryParams.managementDoctorCode = store.getters.hisUserId;
      this.getList(1);
    },
@@ -1222,9 +1231,9 @@
        this.topqueryParams.visitDeptCodes = store.getters.belongDepts.map(
          (obj) => obj.deptCode
        );
          this.topqueryParams.leaveldeptcodes = store.getters.belongDepts.map(
          (obj) => obj.deptCode
        );
        //   this.topqueryParams.leaveldeptcodes = store.getters.belongDepts.map(
        //   (obj) => obj.deptCode
        // );
        this.topqueryParams.leavehospitaldistrictcodes =
          store.getters.belongWards.map((obj) => obj.districtCode);
      }
@@ -1241,7 +1250,7 @@
          this.topqueryParams.leavehospitaldistrictcodes,
        sendstates: [2, 3],
        visitDeptCodes: this.topqueryParams.visitDeptCodes,
        leaveldeptcodes: this.topqueryParams.leaveldeptcodes,
        // leaveldeptcodes: this.topqueryParams.leaveldeptcodes,
      };
      buidegetTasklist(obj).then((response) => {
        this.userList = response.rows[0].serviceSubtaskList;
@@ -1283,7 +1292,7 @@
        this.total = response.total;
      });
    },
    // æŸ¥çœ‹é—¨è¯Šéšè®¿è¯¦æƒ…
    // æŸ¥çœ‹æœåŠ¡éšè®¿è¯¦æƒ…
    Referencequestion(row) {
      this.previewVisible = true;
    },
@@ -1341,9 +1350,9 @@
        this.topqueryParams.visitDeptCodes = store.getters.belongDepts.map(
          (obj) => obj.deptCode
        );
        this.topqueryParams.leaveldeptcodes = store.getters.belongDepts.map(
          (obj) => obj.deptCode
        );
        // this.topqueryParams.leaveldeptcodes = store.getters.belongDepts.map(
        //   (obj) => obj.deptCode
        // );
        this.topqueryParams.leavehospitaldistrictcodes =
          store.getters.belongWards.map((obj) => obj.districtCode);
      }
@@ -1361,16 +1370,16 @@
      let code = value.slice(-1)[0];
      this.topqueryParams.leavehospitaldistrictcodes = [];
      this.topqueryParams.visitDeptCodes = [];
      this.topqueryParams.leaveldeptcodes = [];
      // this.topqueryParams.leaveldeptcodes = [];
      if (type == 1) {
        this.topqueryParams.visitDeptCodes.push(code);
        this.topqueryParams.leaveldeptcodes.push(code);
        // this.topqueryParams.leaveldeptcodes.push(code);
        this.topqueryParams.leavehospitaldistrictcodes = [];
        this.topqueryParams.searchscope = 1;
      } else if (type == 2) {
        this.topqueryParams.leavehospitaldistrictcodes.push(code);
        this.topqueryParams.visitDeptCodes = [];
        this.topqueryParams.leaveldeptcodes = [];
        // this.topqueryParams.leaveldeptcodes = [];
        this.topqueryParams.searchscope = 2;
      } else {
        this.topqueryParams.searchscope = 3;
@@ -1384,13 +1393,13 @@
        pageNum: 1,
        pageSize: 10,
        sendstate: 2,
        sort: 2, //0 é—¨è¯Šæ—¶é—´(正序)    1 é—¨è¯Šæ—¶é—´(倒序)   2 å‘送时间(正序)    3 å‘送时间(倒序)
        sort: 2, //0 æœåŠ¡æ—¶é—´(正序)    1 æœåŠ¡æ—¶é—´(倒序)   2 å‘送时间(正序)    3 å‘送时间(倒序)
        serviceType: 3,
        searchscope: 3,
        visitCount: 2,
        scopetype: [],
        visitDeptCodes: [],
        leaveldeptcodes:[],
        // leaveldeptcodes:[],
        leavehospitaldistrictcodes: [],
      };
      this.handleQuery(1);
@@ -1478,7 +1487,7 @@
            .then((response) => {
              console.log(response);
            })
              .then(() => {
            .then(() => {
              this.getList(1);
              this.$modal.msgSuccess("患者过滤成功");
            });
@@ -1547,11 +1556,11 @@
    },
    // è·³è½¬è¯¦æƒ…页
    Seedetails(row) {
    let type = "";
      let type = "";
      console.log(row, "rwo");
        if (row.type == 1) {
          type = 1;
        }
      if (row.type == 1) {
        type = 1;
      }
      this.$router.push({
        path: "/followvisit/record/detailpage/",
        query: {
@@ -1645,7 +1654,7 @@
          this.zcform.remark =
            this.zcform.remark + "【" + this.getCurrentTime() + "】";
          let form = structuredClone(this.zcform);
          form.longSendTime = this.formatTime(form.date1);
          form.visitTime = this.formatTime(form.date1);
          form.finishtime = "";
          if (form.resource) {
            if (form.resource == 2) {
@@ -1777,11 +1786,11 @@
  }
}
::v-deep.leftvlue .el-card__body {
  background: #F2F8FF;
  color: #324A9B;
  background: #f2f8ff;
  color: #324a9b;
}
::v-deep.leftvlue .el-card__body:hover {
  background: #3664D9;
  background: #3664d9;
  color: #fff;
  cursor: pointer; /* é¼ æ ‡æ‚¬æµ®æ—¶å˜ä¸ºæ‰‹å½¢ */
}
在上述文件截断后对比
src/views/followvisit/SpecificDisease/index.vue src/views/followvisit/Tracking/index.vue src/views/followvisit/again/index.vue src/views/followvisit/complaint/index.vue src/views/followvisit/discharge/index.vue src/views/followvisit/discharge/js/prototype.js src/views/followvisit/discharge/outpatientService.vue src/views/followvisit/mzsatisfaction/index.vue src/views/followvisit/outpatient/index.vue src/views/followvisit/record/TracingInfo/index.vue src/views/followvisit/record/detailpage/index copy.vue src/views/followvisit/record/detailpage/index.vue src/views/followvisit/record/index.vue src/views/followvisit/record/physical/index.vue src/views/followvisit/satisfaction/index.vue src/views/followvisit/technology/index.vue src/views/followvisit/zbAgain/index.vue src/views/followvisit/zysatisfaction/index.vue src/views/index.vue src/views/knowledge/questionbank/particulars/index.vue src/views/login-sy.vue src/views/login.vue src/views/outsideChainwtnew.vue src/views/patient/patient/AwaitingAdmission.vue src/views/patient/patient/behospitalized.vue src/views/patient/patient/hospital.vue src/views/patient/patient/indexls.vue src/views/patient/physical/index.vue src/views/patient/propaganda/Missioncreation.vue src/views/patient/propaganda/QuestionnaireTask.vue src/views/patient/propaganda/index.vue src/views/patient/propaganda/particty.vue src/views/patient/questionnaire/index.vue src/views/patient/shadow/index.vue src/views/patient/subsequent/index.vue src/views/repositoryai/templateku/configurat/index.vue src/views/sfstatistics/ProblemStatistics/index.vue src/views/sfstatistics/percentage/index.vue src/views/sfstatistics/percentage/satisfaction.vue src/views/system/user/index.vue vue.config.js xinhua.zip (已删除)