Browse Source

诊断录入页面搜索诊断优化

yanqiliang 2 months ago
parent
commit
9fec0c5b0d
3 changed files with 669 additions and 171 deletions
  1. 22 0
      src/App.vue
  2. 608 170
      src/pages/components/searchSelect/searchSelect.vue
  3. 39 1
      vite.config.js

+ 22 - 0
src/App.vue

@@ -155,6 +155,27 @@ export default {
             });
         }
     },
+    methods: {
+        prefetchH5PageModulesInDev() {
+            if (!import.meta.env || !import.meta.env.DEV) return;
+            const pageLoaders = Object.values(import.meta.glob('./pages/**/*.vue'));
+            const concurrency = 4;
+            let cursor = 0;
+            const run = async () => {
+                while (cursor < pageLoaders.length) {
+                    const loader = pageLoaders[cursor++];
+                    try {
+                        await loader();
+                    } catch (error) {
+                        console.warn('[dev:h5] prefetch page module failed:', error);
+                    }
+                }
+            };
+            for (let i = 0; i < concurrency; i += 1) {
+                run();
+            }
+        }
+    },
     onLaunch: function (options = {}) {
         console.log('getDeviceInfo==', uni.getWindowInfo());
         this.globalData.getAppInfo();
@@ -167,6 +188,7 @@ export default {
         }
 
         // #ifdef H5
+        this.prefetchH5PageModulesInDev();
 		options = options || { query: {} };
 		if(options && options.query.state) {
 			this.globalData.pubAccCode = options.query.state

+ 608 - 170
src/pages/components/searchSelect/searchSelect.vue

@@ -1,68 +1,101 @@
 <template>
-    <view>
+    <view class="search-page">
         <!-- pages/components/searchSelect/searchSelect.wxml -->
-        <nav-bar :navbarData="navbarData"></nav-bar>
-        <view class="searchTop" :style="'padding-top:' + (height + 100) + 'rpx'">
-            <van-search :value="desc" @search="onChangeSearch" @change="onChangeSearch" @input="onInputSearch" placeholder="请输入搜索关键词" focus />
+        <view class="search-header" :style="'height:' + navHeight + 'rpx;padding-top:' + statusHeight + 'rpx'">
+            <view class="search-header__back" @tap="goBack">
+                <text class="search-header__back-icon">‹</text>
+            </view>
+            <view class="search-header__title">搜索</view>
         </view>
-        <!-- <view class='searchList'>
-	<view class="mb20">最近使用记录</view>
-	<text class="searchListBox" wx:for="{{searchList}}" wx:key="key">{{item.label}}</text>
-</view> -->
-        <scroll-view :scroll-y="true" :style="'height:calc(100% - 160rpx - ' + (height * 2 - 2) + 'rpx)'" @scrolltolower="scrollLower">
-            <view class="searchContent">
-                <van-radio-group :value="radio" @change="onChangeRadio">
-                    <van-cell-group>
-                        <van-cell
-                            center
-                            clickable
-                            use-label-slot
-                            :data-name="index"
-                            @click="onClickRadio($event, { name: index })"
-                            v-for="(item, index) in selectData"
-                            :key="index"
-                        >
-                            <view slot="title">
-                                <view class="van-cell-text">
-                                    <text class="ml10" v-for="(cell, index1) in htmltitle[code]" :key="index1">{{ item[cell] }}</text>
-                                    <van-tag mark type="warning" class="ml10" v-for="(cell, index1) in htmltag[code]" :key="index1">{{ item[cell] || '无库存' }}</van-tag>
+
+        <view class="search-main" :style="'padding-top:' + navHeight + 'rpx'">
+            <view class="search-bar">
+                <view class="search-bar__box">
+                    <text class="search-bar__icon">⌕</text>
+                    <input
+                        class="search-bar__input"
+                        :value="desc"
+                        confirm-type="search"
+                        :placeholder="searchPlaceholder"
+                        placeholder-class="search-bar__placeholder"
+                        focus
+                        @input="onInputSearch"
+                        @confirm="onChangeSearch"
+                    />
+                    <view class="search-bar__clear" v-if="desc" @tap="clearSearch">×</view>
+                </view>
+            </view>
+
+            <view class="search-summary" v-if="desc">
+                <text>关键词:{{ desc }}</text>
+                <text v-if="!show && selectData.length">共 {{ selectData.length }} 条</text>
+            </view>
+
+            <scroll-view class="search-scroll" :style="'height:calc(100vh - ' + (navHeight + (desc ? 176 : 128)) + 'rpx)'" :scroll-y="true" @scrolltolower="scrollLower">
+                <view class="search-content">
+                    <view class="state-box" v-if="show && currentPage === 1">
+                        <view class="loading-ring"></view>
+                        <text>检索中...</text>
+                    </view>
+
+                    <view class="state-box" v-else-if="errorMessage">
+                        <view class="state-box__title">检索失败</view>
+                        <view class="state-box__desc">{{ errorMessage }}</view>
+                        <button class="state-box__button" @tap="retrySearch">重试</button>
+                    </view>
+
+                    <view class="state-box" v-else-if="!desc">
+                        <view class="state-box__title">输入关键词开始检索</view>
+                        <view class="state-box__desc">可按诊断名称、拼音或编码搜索</view>
+                    </view>
+
+                    <view class="state-box" v-else-if="!selectData.length">
+                        <view class="state-box__title">暂无匹配诊断</view>
+                        <view class="state-box__desc">请更换关键词后再试</view>
+                    </view>
+
+                    <view v-else class="result-list">
+                        <view class="result-item" v-for="(item, index) in selectData" :key="item.id || item.diagCode || item.medID || index" :data-name="index" @tap="onClickRadio">
+                            <view class="result-item__body">
+                                <view class="result-item__title">
+                                    <block v-for="(cell, index1) in htmltitle[code]" :key="index1">
+                                        <text v-if="item[cell]" class="result-item__title-text">{{ item[cell] }}</text>
+                                    </block>
+                                </view>
+                                <view class="result-item__tags" v-if="htmltag[code] && htmltag[code].length">
+                                    <text class="result-item__tag" v-for="(cell, index1) in htmltag[code]" :key="index1">{{ item[cell] || '无库存' }}</text>
                                 </view>
+                                <view class="result-item__label" v-if="getLabelText(item)">{{ getLabelText(item) }}</view>
                             </view>
+                            <view class="result-item__check" :class="{ 'result-item__check--active': radio === index }"></view>
+                        </view>
+                    </view>
 
-                            <view slot="label">
-                                <view class="ml10" v-for="(cell, index1) in htmllabel[code]" :key="index1">{{ item[cell] }}</view>
-                            </view>
+                    <view class="loading-more" v-if="show && currentPage > 1">
+                        <view class="loading-ring loading-ring--small"></view>
+                        <text>加载中...</text>
+                    </view>
 
-                            <van-radio slot="right-icon" checked-color="#07c160" :name="index" />
-                        </van-cell>
-                    </van-cell-group>
-                </van-radio-group>
-            </view>
-            <van-toast id="van-toast" />
-        </scroll-view>
-        <van-overlay :show="show">
-            <view class="wrapper">
-                <van-loading type="spinner" size="38px" color="#ffffff">检索中...</van-loading>
-            </view>
-        </van-overlay>
+                    <view class="list-footer" v-if="finished && selectData.length">已加载全部</view>
+                </view>
+            </scroll-view>
+        </view>
     </view>
 </template>
 
 <script>
 import Cache from '../../../utils/cache';
-import navBar from '@/pages/components/navbar/index';
 // pages/components/searchSelect/searchSelect.js
 // 公共selectData查询  obj 和 code 做动态控制 其他参数统一
 // 因为考虑到以后数据过多使用滚动底部懒加载的方式会重复多次请求 故请求建议全部放到组件里面 统一控制分页的请求
 // 考虑到进入这个页面的可能是从嵌套了许多层之后的的组件进入 不是page页面 navigateBack 只能返回page 不适合使用普通的传值方式 所以把选中返回值传入小程序缓存Storage 任何地方使用的在所以把选中返回值传入Storage取值
 import { $http } from '../../../config/https';
-import Toast from '../../../utils/toast';
-const app = getApp();
+// H5 兼容:getApp() 在某些场景下可能返回 undefined 或不带 globalData 的 Vue 实例
+const app = (typeof getApp === 'function' && getApp()) || { globalData: {} };
 export default {
-    components: {
-        navBar
-    },
+    components: {},
     data() {
+        const statusHeight = ((app.globalData && app.globalData.height) || 0) * 2;
         return {
             focus: true,
             // 获取焦点
@@ -78,6 +111,7 @@ export default {
             // 页数
             desc: '',
             // 搜索内容
+            searchPlaceholder: '请输入搜索关键词',
             radio: 0,
             // 选项
             options: [],
@@ -100,15 +134,14 @@ export default {
                 '03050031': ['diagCode'] //诊断名称
             },
 
-            navbarData: {
-                isGoBack: true,
-                background: '',
-                title: '搜索',
-                //导航栏 中间的标题
-                bottom: false
-            },
-            height: app.globalData.height,
-            show: false
+            statusHeight,
+            navHeight: statusHeight + 88,
+            height: (app.globalData && app.globalData.height) || 0,
+            show: false,
+            finished: false,
+            errorMessage: '',
+            searchTimer: null,
+            requestSeq: 0
         };
     },
     /**
@@ -119,11 +152,11 @@ export default {
         this.setData(
             {
                 code,
-                options
-            },
-            () => {
-                this.judge(code, options);  // 判断 调取哪个接口
+                options,
+                searchPlaceholder: code === '03050031' ? '请输入诊断名称或编码' : '请输入医嘱名称或编码'
             }
+            // 注意:不再在 onLoad 中自动调 judge(),避免 H5 端空 desc 时误触发加载层
+            // 数据由用户输入关键词后通过 onChangeSearch / onInputSearch 触发
         );
         uni.getSystemInfo({
             success: (res) => {
@@ -148,7 +181,11 @@ export default {
     /**
      * 生命周期函数--监听页面卸载
      */
-    onUnload: function () {},
+    onUnload: function () {
+        if (this.searchTimer) {
+            clearTimeout(this.searchTimer);
+        }
+    },
     /**
      * 页面相关事件处理函数--监听用户下拉动作
      */
@@ -162,15 +199,24 @@ export default {
      */
     onShareAppMessage: function () {},
     methods: {
+        normalizeSearchValue(value) {
+            if (value == null) return '';
+            if (typeof value === 'object') {
+                value = value.value != null ? value.value : value.detail != null ? value.detail : '';
+            }
+            return String(value).trim();
+        },
+
         //判断 调取哪个接口
         judge: function (code, options) {
             let type = ''; // 草药标志
             let herbalFormID = ''; // 剂型
             let { obj, desc } = this;
-            let patientData = Cache.get('patientData');
-            let userData = Cache.get('userData');
-            let { admID, patID } = patientData;
+            let patientData = Cache.get('patientData') || {};
+            let userData = Cache.get('userData') || {};
+            let { admID } = patientData;
             let { locID, userID, userCode, userName, locDesc, groupID, groupDesc, hospID, hospCode, hospDesc } = userData;
+            let keyword = this.normalizeSearchValue(desc);
             if (options) {
                 type = options.type;
                 herbalFormID = options.herbalFormID;
@@ -179,7 +225,7 @@ export default {
                 case '03050019':
                     //医嘱名称跳转
                     obj = {
-                        searchCode: desc || '',
+                        searchCode: keyword,
                         //医嘱项搜索码
                         admID: admID || '',
                         //就诊记录ID
@@ -208,7 +254,7 @@ export default {
                 case '03050031':
                     //诊断名称跳转
                     obj = {
-                        desc: desc || '',
+                        desc: keyword,
                         // 检索框内容
                         hospID: hospID || '',
                         //医院ID  session 中获取
@@ -227,7 +273,9 @@ export default {
                 {
                     code,
                     obj,
-                    options
+                    options,
+                    errorMessage: '',
+                    finished: false
                 },
                 () => {
                     this.selectDataFun(); // 获取数据
@@ -260,75 +308,116 @@ export default {
             });
         },
 
-        // 查询诊断列表接口
+        getRequestKeyword(obj) {
+            if (!obj) return '';
+            if (this.code === '03050019') {
+                return this.normalizeSearchValue(obj.searchCode);
+            }
+            return this.normalizeSearchValue(obj.desc);
+        },
+
+        syncRequestKeyword(obj, keyword) {
+            if (this.code === '03050019') {
+                obj.searchCode = keyword;
+            } else {
+                obj.desc = keyword;
+            }
+        },
+
+        // 查询诊断/医嘱列表接口
         selectDataFun: function (concat) {
             let { code, obj, pageSize, currentPage } = this;
-            if (code === '') {
+            if (!code) {
+                this.setData({ show: false });
+                return false;
+            }
+
+            let userData = Cache.get('userData');
+            if (!userData || !userData.userID) {
+                this.setData({
+                    selectData: [],
+                    show: false,
+                    finished: false,
+                    errorMessage: '请先登录后再检索'
+                });
                 return false;
             }
-            if (obj.desc == '') {
+
+            let keyword = this.getRequestKeyword(obj);
+            if (!keyword) {
                 this.setData({
-                    selectData: []
+                    selectData: [],
+                    show: false,
+                    finished: false,
+                    errorMessage: ''
                 });
-                return;
+                return false;
             }
+            this.syncRequestKeyword(obj, keyword);
+
             let that = this;
-            this.setData(
+            let seq = this.requestSeq + 1;
+            this.setData({
+                requestSeq: seq,
+                show: true,
+                errorMessage: ''
+            });
+
+            let showTimer = setTimeout(() => {
+                if (that.requestSeq === seq) {
+                    that.setData({
+                        show: false,
+                        errorMessage: '接口响应超时,请稍后重试'
+                    });
+                }
+            }, 15000);
+
+            $http.post(
+                'urlDeault',
+                this,
                 {
-                    show: true
-                },
-                () => {
-                    $http.post(
-                        'urlDeault',
-                        this,
-                        {
-                            code: code,
-                            data: {
-                                params: [obj],
-                                pagination: [
-                                    //页码信息
-                                    {
-                                        pageSize: pageSize,
-                                        currentPage: currentPage
-                                    }
-                                ]
-                            },
-                            success: function (res) {
-                                let errorCode = res.errorCode;
-                                if (errorCode === '0') {
-                                    let data = res.result && res.result.data;
-                                    if (concat) {
-                                        // 滚动加载 故合并数据
-                                        let { selectData } = that;
-                                        var cont = selectData.concat(data);
-                                        that.setData({
-                                            selectData: cont,
-                                            //selectData 数据
-                                            show: false
-                                        });
-                                    } else {
-                                        // 非滚动加载 故重置数据
-                                        that.setData({
-                                            selectData: data,
-                                            //selectData 数据
-                                            show: false
-                                        });
-                                    }
-                                } else {
-                                    uni.showToast({
-                                        title: res.errorMessage,
-                                        icon: 'none'
-                                    });
-                                    that.setData({
-                                        show: false
-                                    });
-                                }
+                    code: code,
+                    data: {
+                        params: [obj],
+                        pagination: [
+                            {
+                                pageSize: pageSize,
+                                currentPage: currentPage
                             }
-                        },
-                        {
-                            error: 'Y'
+                        ]
+                    },
+                    success: function (res) {
+                        clearTimeout(showTimer);
+                        if (that.requestSeq !== seq) return;
+
+                        let errorCode = res.errorCode;
+                        if (errorCode === '0') {
+                            let data = (res.result && res.result.data) || [];
+                            let nextList = concat ? that.selectData.concat(data) : data;
+                            that.setData({
+                                selectData: nextList,
+                                show: false,
+                                finished: data.length < pageSize,
+                                errorMessage: ''
+                            });
+                        } else {
+                            that.setData({
+                                show: false,
+                                errorMessage: res.errorMessage || '检索失败,请稍后重试'
+                            });
                         }
-                    );
+                    },
+                    fail: function (res) {
+                        clearTimeout(showTimer);
+                        if (that.requestSeq !== seq) return;
+                        that.setData({
+                            show: false,
+                            errorMessage: (res && res.errorMessage) || '检索失败,请稍后重试'
+                        });
+                    }
+                },
+                {
+                    error: 'Y'
                 }
             );
         },
@@ -336,11 +425,17 @@ export default {
         // search修改
         onChangeSearch(e) {
             let value = this.getSearchValue(e);
-            if (value != '') {
+            if (this.searchTimer) {
+                clearTimeout(this.searchTimer);
+            }
+            if (value) {
                 this.setData(
                     {
                         desc: value,
-                        currentPage: 1
+                        currentPage: 1,
+                        radio: '',
+                        finished: false,
+                        errorMessage: ''
                     },
                     () => {
                         let { code, options } = this;
@@ -349,7 +444,11 @@ export default {
                 );
             } else {
                 this.setData({
-                    selectData: []
+                    desc: '',
+                    selectData: [],
+                    show: false,
+                    finished: false,
+                    errorMessage: ''
                 });
             }
         },
@@ -358,33 +457,96 @@ export default {
         onInputSearch(e) {
             let value = this.getSearchValue(e);
             if (value === this.desc) return;
-            if (value != '') {
-                this.setData(
-                    {
-                        desc: value,
-                        currentPage: 1
-                    },
-                    () => {
-                        let { code, options } = this;
-                        this.judge(code, options);
-                    }
-                );
+            if (this.searchTimer) {
+                clearTimeout(this.searchTimer);
+            }
+            if (value) {
+                this.setData({
+                    desc: value,
+                    currentPage: 1,
+                    radio: '',
+                    finished: false,
+                    errorMessage: ''
+                });
+                this.searchTimer = setTimeout(() => {
+                    let { code, options } = this;
+                    this.judge(code, options);
+                }, 350);
             } else {
                 this.setData({
-                    selectData: []
+                    desc: '',
+                    selectData: [],
+                    show: false,
+                    finished: false,
+                    errorMessage: ''
                 });
             }
         },
 
         // 兼容不同环境下van-search的事件格式
+        // H5 端 e = { detail: { value: 'xxx' } },微信小程序端 e = { detail: 'xxx' }
         getSearchValue(e) {
-            if (e && e.detail != null && e.detail !== '') return e.detail;
-            if (e && e.target && e.target.value != null) return e.target.value;
-            if (typeof e === 'string') return e;
-            if (e && e.detail && e.detail.value != null) return e.detail.value;
+            if (!e) return '';
+            // 1. H5 端:e.detail 是对象 { value: 'xxx' }
+            if (e.detail && typeof e.detail === 'object' && e.detail.value != null) {
+                return this.normalizeSearchValue(e.detail.value);
+            }
+            // 2. 微信小程序端:e.detail 直接是字符串
+            if (typeof e.detail === 'string') return this.normalizeSearchValue(e.detail);
+            // 3. 原生 input 事件:e.target.value
+            if (e.target && e.target.value != null) return this.normalizeSearchValue(e.target.value);
+            // 4. 兜底:直接传字符串
+            if (typeof e === 'string') return this.normalizeSearchValue(e);
             return '';
         },
 
+        clearSearch() {
+            if (this.searchTimer) {
+                clearTimeout(this.searchTimer);
+            }
+            this.setData({
+                desc: '',
+                selectData: [],
+                show: false,
+                finished: false,
+                errorMessage: '',
+                currentPage: 1,
+                radio: ''
+            });
+        },
+
+        retrySearch() {
+            if (!this.desc) return;
+            this.setData(
+                {
+                    currentPage: 1,
+                    finished: false,
+                    errorMessage: ''
+                },
+                () => {
+                    this.judge(this.code, this.options);
+                }
+            );
+        },
+
+        goBack() {
+            uni.navigateBack({
+                fail: function () {
+                    uni.switchTab({
+                        url: '/pages/tabBar/home/home'
+                    });
+                }
+            });
+        },
+
+        getLabelText(item) {
+            let labels = this.htmllabel[this.code] || [];
+            return labels
+                .map((cell) => item[cell])
+                .filter(Boolean)
+                .join('  ');
+        },
+
         // 点击切换
         onChangeRadio(event) {
             this.setData({
@@ -394,10 +556,9 @@ export default {
 
         // 点击切换
         onClickRadio(event, _dataset) {
-            /* ---处理dataset begin--- */
-            this.handleDataset(event, _dataset);
-            /* ---处理dataset end--- */
-            const { name } = event.currentTarget.dataset;
+            const dataset = (event && event.currentTarget && event.currentTarget.dataset) || _dataset || {};
+            const name = Number(dataset.name);
+            if (Number.isNaN(name)) return;
             this.setData(
                 {
                     radio: name
@@ -411,27 +572,33 @@ export default {
         // 点击 返回上一页 传递 所选择的参数对象 使用调取上个页面的callData方法传入选择的数据
         call(name) {
             let { selectData } = this;
+            let selected = selectData[name];
+            if (!selected) return;
             // 数据存入Storage
-            //wx.setStorageSync('searchSelect', selectData[name])
+            Cache.set('searchSelect', selected);
             var pages = getCurrentPages();
             var prevPage = pages[pages.length - 2]; //上一个页面
-            //直接调用上一个页面的setData()方法,把数据存到上一个页面中去
+            const callPrevPage = function () {
+                const target = prevPage && (prevPage.$vm || prevPage);
+                if (target && typeof target.searchSelect === 'function') {
+                    target.searchSelect(selected);
+                }
+            };
             uni.navigateBack({
                 //返回
                 delta: 1,
                 success: function () {
-                    //调用前一个页面的方法updateTime()。
-                    prevPage.searchSelect(selectData[name]);
+                    callPrevPage();
+                },
+                fail: function () {
+                    callPrevPage();
                 }
             });
         },
 
         // 滚动至底端事件
         scrollLower: function () {
-            Toast.loading({
-                duration: 1000,
-                message: '加载中...'
-            });
+            if (this.show || this.finished || !this.desc || this.errorMessage) return;
             let { currentPage } = this;
             this.setData(
                 {
@@ -450,33 +617,304 @@ export default {
 
 page {
     height: 100%;
+    background: #f4f7fb;
+}
+
+.search-page {
+    min-height: 100vh;
+    background: #f4f7fb;
+    color: #1f2937;
+    box-sizing: border-box;
+}
+
+.search-header {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    z-index: 20;
+    background: #ffffff;
+    box-sizing: border-box;
+    box-shadow: 0 1rpx 0 rgba(15, 23, 42, 0.08);
+}
+
+.search-header__title {
+    height: 88rpx;
+    line-height: 88rpx;
+    text-align: center;
+    font-size: 36rpx;
+    font-weight: 700;
+    color: #111827;
+}
+
+.search-header__back {
+    position: absolute;
+    left: 0;
+    bottom: 0;
+    width: 96rpx;
+    height: 88rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.search-header__back-icon {
+    color: #8a98aa;
+    font-size: 64rpx;
+    line-height: 1;
+    transform: translateY(-2rpx);
 }
 
-.searchTop {
-    background: #fff;
-    padding: 10rpx;
+.search-main {
+    min-height: 100vh;
+    box-sizing: border-box;
+}
+
+.search-bar {
+    position: sticky;
+    top: 0;
+    z-index: 10;
+    background: #f4f7fb;
+    padding: 18rpx 24rpx 14rpx;
+    box-sizing: border-box;
+}
+
+.search-bar__box {
     width: 100%;
+    height: 76rpx;
+    border-radius: 12rpx;
+    background: #ffffff;
+    border: 1rpx solid #dce5f0;
+    display: flex;
+    align-items: center;
+    padding: 0 22rpx;
+    box-sizing: border-box;
+    box-shadow: 0 6rpx 16rpx rgba(15, 23, 42, 0.04);
+}
+
+.search-bar__icon {
+    width: 40rpx;
+    color: #8a98aa;
+    font-size: 32rpx;
+    line-height: 1;
+}
+
+.search-bar__input {
+    flex: 1;
+    min-width: 0;
+    height: 72rpx;
+    line-height: 72rpx;
+    color: #111827;
+    font-size: 29rpx;
+    padding: 0 12rpx;
+    box-sizing: border-box;
+}
+
+.search-bar__placeholder {
+    color: #9aa6b6;
+}
+
+.search-bar__clear {
+    width: 44rpx;
+    height: 44rpx;
+    line-height: 40rpx;
+    border-radius: 50%;
+    background: #eef2f7;
+    color: #7b8798;
+    text-align: center;
+    font-size: 36rpx;
+    box-sizing: border-box;
+}
+
+.search-summary {
+    display: flex;
+    justify-content: space-between;
+    padding: 0 28rpx 12rpx;
+    color: #6b7686;
+    font-size: 25rpx;
+    line-height: 36rpx;
+    box-sizing: border-box;
+}
+
+.search-scroll {
+    height: calc(100vh - 190rpx);
+}
+
+.search-content {
+    padding: 0 18rpx 28rpx;
+    box-sizing: border-box;
+}
+
+.state-box {
+    min-height: 300rpx;
+    margin: 12rpx 6rpx;
+    border-radius: 12rpx;
+    background: #ffffff;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    color: #7b8798;
+    font-size: 28rpx;
+    box-sizing: border-box;
+    padding: 44rpx 32rpx;
+    text-align: center;
+}
+
+.state-box__title {
+    color: #1f2937;
+    font-size: 30rpx;
+    font-weight: 600;
+    line-height: 44rpx;
+}
+
+.state-box__desc {
+    margin-top: 10rpx;
+    color: #7b8798;
+    font-size: 26rpx;
+    line-height: 38rpx;
+}
+
+.state-box__button {
+    margin-top: 28rpx;
+    min-width: 160rpx;
+    height: 64rpx;
+    line-height: 64rpx;
+    border-radius: 10rpx !important;
+    color: #ffffff;
+    background: #1890ff;
+    font-size: 28rpx;
+    padding: 0 28rpx;
+}
+
+.state-box__button:after {
+    border: 0;
+}
+
+.result-list {
+    display: flex;
+    flex-direction: column;
+    gap: 14rpx;
+}
+
+.result-item {
+    min-height: 112rpx;
+    border-radius: 12rpx;
+    background: #ffffff;
+    border: 1rpx solid #e6edf5;
+    padding: 20rpx 22rpx;
+    box-sizing: border-box;
+    display: flex;
+    align-items: center;
+    gap: 18rpx;
+    box-shadow: 0 6rpx 16rpx rgba(15, 23, 42, 0.035);
+}
+
+.result-item__body {
+    flex: 1;
+    min-width: 0;
+}
+
+.result-item__title {
+    display: flex;
+    flex-wrap: wrap;
+    align-items: center;
+    gap: 10rpx;
+    color: #111827;
     font-size: 30rpx;
+    font-weight: 600;
+    line-height: 42rpx;
 }
 
-.searchList {
-    background: #eee;
-    padding: 20rpx 10rpx;
+.result-item__title-text {
+    word-break: break-all;
 }
 
-.searchListBox {
-    padding: 15rpx 30rpx;
-    margin-left: 10rpx;
+.result-item__tags {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8rpx;
+    margin-top: 10rpx;
+}
+
+.result-item__tag {
+    height: 34rpx;
+    line-height: 34rpx;
+    border-radius: 6rpx;
+    background: #fff7e6;
+    color: #b26a00;
+    font-size: 22rpx;
+    padding: 0 10rpx;
+}
+
+.result-item__label {
+    margin-top: 8rpx;
+    color: #687386;
     font-size: 25rpx;
-    background: #fff;
-    border: 2rpx solid #fff;
-    border-radius: 4rpx !important;
-    margin-top: 30rpx;
+    line-height: 36rpx;
+    word-break: break-all;
+}
+
+.result-item__check {
+    width: 34rpx;
+    height: 34rpx;
+    border-radius: 50%;
+    border: 2rpx solid #c8d2df;
+    box-sizing: border-box;
+    position: relative;
+}
+
+.result-item__check--active {
+    border-color: #1890ff;
+    background: #1890ff;
+}
+
+.result-item__check--active:after {
+    content: "";
+    position: absolute;
+    left: 9rpx;
+    top: 4rpx;
+    width: 9rpx;
+    height: 17rpx;
+    border: solid #ffffff;
+    border-width: 0 4rpx 4rpx 0;
+    transform: rotate(45deg);
 }
-.wrapper {
+
+.loading-more,
+.list-footer {
+    height: 86rpx;
     display: flex;
     align-items: center;
     justify-content: center;
-    height: 100%;
+    gap: 12rpx;
+    color: #8a98aa;
+    font-size: 25rpx;
+}
+
+.loading-ring {
+    width: 38rpx;
+    height: 38rpx;
+    border-radius: 50%;
+    border: 4rpx solid #d7e6f8;
+    border-top-color: #1890ff;
+    animation: search-spin 0.8s linear infinite;
+    margin-bottom: 18rpx;
+}
+
+.loading-ring--small {
+    width: 28rpx;
+    height: 28rpx;
+    border-width: 3rpx;
+    margin-bottom: 0;
+}
+
+@keyframes search-spin {
+    from {
+        transform: rotate(0deg);
+    }
+    to {
+        transform: rotate(360deg);
+    }
 }
 </style>

+ 39 - 1
vite.config.js

@@ -31,9 +31,47 @@ export default defineConfig({
 			'@': 'src',
 			'jweixin-module': path.resolve(__dirname, 'src/js_sdk/jweixin-module/index.js')
         }
+    },
+    optimizeDeps: {
+        entries: [
+            'src/main.js',
+            'src/App.vue',
+            'src/pages/**/*.vue'
+        ],
+        include: [
+            'tim-wx-sdk',
+            'cos-wx-sdk-v5',
+            'jweixin-module'
+        ]
+    },
+    build: {
+        chunkSizeWarningLimit: 1000,
+        rollupOptions: {
+            output: {
+                manualChunks(id) {
+                    if (!id.includes('node_modules')) return;
+                    if (/[\\/]node_modules[\\/](tim-|trtc-|cos-|webrtc-adapter)/.test(id)) {
+                        return 'vendor-tencent';
+                    }
+                    if (/[\\/]node_modules[\\/](@vant|weui-miniprogram|vant-aliapp)/.test(id)) {
+                        return 'vendor-ui';
+                    }
+                    if (/[\\/]node_modules[\\/](@dcloudio|vue|vuex|vue-router)/.test(id)) {
+                        return 'vendor-core';
+                    }
+                }
+            }
+        }
     },
 	server: {
 	    host: '0.0.0.0',
+        warmup: {
+            clientFiles: [
+                'src/main.js',
+                'src/App.vue',
+                'src/**/*.vue'
+            ]
+        },
 	    allowedHosts: [
 	      'frp.12123wzcx.com' // 允许所有 frp.12123wzcx.com 的子域名
 	    ],
@@ -75,4 +113,4 @@ export default defineConfig({
 
 	    }
 	},
-})
+})