xw пре 4 дана
родитељ
комит
53d4d56c9c
3 измењених фајлова са 1000 додато и 169 уклоњено
  1. 948 161
      src/views/course/coursePlaySourceConfig/index.vue
  2. 24 3
      src/views/qw/sop/addSop.vue
  3. 28 5
      src/views/qw/sop/sop.vue

+ 948 - 161
src/views/course/coursePlaySourceConfig/index.vue

@@ -1,132 +1,294 @@
 <template>
-  <div class="app-container">
-    <el-form ref="miniAppConfigForm" :model="miniAppConfigForm" :inline="true" label-width="100px" class="mini-app-config-form">
-      <el-form-item label="主要小程序">
-        <el-select
-          v-model="miniAppConfigForm.mainMiniAppId"
-          placeholder="请选择主要小程序"
-          clearable
-          size="small"
-          style="width: 220px"
-        >
-          <el-option
-            v-for="item in companyMiniAppList"
-            :key="item.id"
-            :label="item.name"
-            :value="item.appid || item.appId"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="备用小程序">
-        <el-select
-          v-model="miniAppConfigForm.backupMiniAppId"
-          placeholder="请选择备用小程序"
-          clearable
-          size="small"
-          style="width: 220px"
-        >
-          <el-option
-            v-for="item in companyMiniAppList"
-            :key="'backup-' + item.id"
-            :label="item.name"
-            :value="item.appid || item.appId"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item>
-        <el-button type="primary" size="mini" :loading="miniAppSaving" @click="onSubmitMiniApp">保存</el-button>
-      </el-form-item>
-    </el-form>
-
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
-      <el-form-item label="名称" prop="name">
-        <el-input
-          v-model="queryParams.name"
-          placeholder="请输入名称"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="appid" prop="appid">
-        <el-input
-          v-model="queryParams.appid"
-          placeholder="请输入appid"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item>
-        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
-        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
-      </el-form-item>
-    </el-form>
-
-    <el-row :gutter="10" class="mb8">
-      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
-    </el-row>
+  <div class="miniapp-config-page">
+    <!-- 页面标题 -->
+    <div class="page-header">
+      <div class="page-header-left">
+        <div class="page-header-icon">
+          <i class="el-icon-s-grid" />
+        </div>
+        <div class="page-header-text">
+          <h2 class="page-title">小程序配置管理</h2>
+          <p class="page-subtitle">统一管理企微主体、主备及所有小程序的运营状态</p>
+        </div>
+      </div>
+      <div class="page-stats">
+        <div class="stat-chip">
+          <span class="stat-num">{{ qwCompanyList.length }}</span>
+          <span class="stat-label">企微主体</span>
+        </div>
+        <div class="stat-chip">
+          <span class="stat-num">{{ list.length }}</span>
+          <span class="stat-label">小程序</span>
+        </div>
+      </div>
+    </div>
 
-    <el-table v-loading="loading" :data="list" border>
-      <el-table-column label="ID" align="center" prop="id" />
-      <el-table-column label="名称" align="center" prop="name" />
-      <el-table-column label="图标" align="center" prop="img">
-        <template slot-scope="scope">
-          <el-image
-            style="width: 80px; height: 80px"
-            :src="scope.row.img"
-            :preview-src-list="[scope.row.img]">
-          </el-image>
-        </template>
-      </el-table-column>
-      <el-table-column label="原始ID" align="center" prop="originalId" />
-      <el-table-column label="appId" align="center" prop="appid" />
-      <el-table-column label="secret" align="center" prop="secret" />
-      <el-table-column label="token" align="center" prop="token" />
-      <el-table-column label="aesKey" align="center" prop="aesKey" />
-      <el-table-column label="msgDataFormat" align="center" prop="msgDataFormat" />
-      <el-table-column label="类型" align="center" prop="type">
-        <template slot-scope="scope">
-          <dict-tag :options="typesOptions" :value="scope.row.type"/>
-        </template>
-      </el-table-column>
-      <el-table-column label="互医/商城小程序" align="center" prop="isMall" width="80px">
-        <template slot-scope="scope">
-          <el-tag v-for="(item, index) in isMallOptions" v-if="scope.row.isMall==item.dictValue" :key="index">{{item.dictLabel}}</el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column label="状态" align="center" prop="status" width="100px">
-        <template slot-scope="scope">
-          <el-tag
-            :type="scope.row.status === 0 ? 'success' : scope.row.status === 1 ? 'warning' : 'danger'"
-          >
-            {{ getStatusLabel(scope.row.status) }}
-          </el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column label="创建时间" align="center" prop="createTime" />
-      <el-table-column label="修改时间" align="center" prop="updateTime" />
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="80">
-        <template slot-scope="scope">
-          <el-button
-            v-if="canModify"
-            size="mini"
-            type="text"
-            icon="el-icon-edit"
-            @click="handleUpdate(scope.row)"
-          >修改</el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-
-    <pagination
-      v-show="total>0"
-      :total="total"
-      :page.sync="queryParams.pageNum"
-      :limit.sync="queryParams.pageSize"
-      @pagination="getList"
-    />
+    <!-- 三栏布局 -->
+    <el-row :gutter="16" class="config-columns">
+      <!-- 企微主体 -->
+      <el-col :xs="24" :sm="24" :md="8" :lg="8" class="config-col">
+        <div class="config-card">
+          <div class="card-header">
+            <div class="card-header-title">
+              <i class="el-icon-chat-dot-round card-header-icon" />
+              <span>企微主体</span>
+            </div>
+          </div>
+          <div v-loading="qwLoading" class="card-body card-body-flex">
+            <el-input
+              v-model="qwSearchName"
+              placeholder="搜索主体名称 / CorpID / 关联公司..."
+              prefix-icon="el-icon-search"
+              clearable
+              size="small"
+              class="panel-search"
+              @input="handleQwSearch"
+              @clear="handleQwSearch"
+            />
+            <div class="panel-toolbar">
+              <span class="panel-count">共 {{ filteredQwCompanyList.length }} 个主体</span>
+              <div class="panel-actions">
+                <span v-if="qwSelectedIds.length" class="selected-count">已选 {{ qwSelectedIds.length }} 项</span>
+                <el-button
+                  type="primary"
+                  plain
+                  size="mini"
+                  icon="el-icon-edit-outline"
+                  :disabled="!qwSelectedIds.length"
+                  @click="handleBatchUpdateMiniAppId"
+                >批量修改主体小程序</el-button>
+              </div>
+            </div>
+            <div v-if="filteredQwCompanyList.length" class="table-wrap">
+              <el-table
+                ref="qwCompanyTable"
+                :data="paginatedQwCompanyList"
+                size="small"
+                class="data-table"
+                :row-key="row => row.id"
+                @selection-change="handleQwSelectionChange"
+              >
+                <el-table-column type="selection" width="40" align="center" reserve-selection />
+                <el-table-column
+                  label="企业名称"
+                  prop="corpName"
+                  min-width="88"
+                  show-overflow-tooltip
+                />
+                <el-table-column
+                  label="绑定小程序"
+                  prop="miniName"
+                  min-width="80"
+                  show-overflow-tooltip
+                >
+                  <template slot-scope="scope">
+                    {{ scope.row.miniName || '-' }}
+                  </template>
+                </el-table-column>
+                <el-table-column label="状态" width="52" align="center">
+                  <template slot-scope="scope">
+                    <span
+                      class="status-light-wrap"
+                      :title="getQwStatusLabel(scope.row.status)"
+                    >
+                      <span
+                        class="status-light"
+                        :class="'status-light--' + getQwStatusLight(scope.row.status)"
+                      />
+                    </span>
+                  </template>
+                </el-table-column>
+                <el-table-column label="操作" width="48" align="center" fixed="right">
+                  <template slot-scope="scope">
+                    <el-button
+                      v-if="canModifyQw"
+                      type="text"
+                      icon="el-icon-edit"
+                      class="row-action-btn"
+                      @click.stop="handleUpdateQwCompany(scope.row)"
+                    />
+                  </template>
+                </el-table-column>
+              </el-table>
+            </div>
+            <div v-if="filteredQwCompanyList.length > qwPageSize" class="panel-pagination">
+              <el-pagination
+                small
+                background
+                layout="prev, pager, next"
+                :current-page.sync="qwPageNum"
+                :page-size="qwPageSize"
+                :total="filteredQwCompanyList.length"
+              />
+            </div>
+            <el-empty
+              v-else-if="!qwLoading && !filteredQwCompanyList.length"
+              :description="qwCompanyList.length ? '未找到匹配的企微主体' : '暂无企微主体数据'"
+              :image-size="64"
+              class="panel-empty"
+            />
+          </div>
+        </div>
+      </el-col>
+
+      <!-- 主备小程序 -->
+      <el-col :xs="24" :sm="24" :md="8" :lg="8" class="config-col">
+        <div class="config-card">
+          <div class="card-header">
+            <div class="card-header-title">
+              <i class="el-icon-share card-header-icon" />
+              <span>主备小程序</span>
+            </div>
+          </div>
+          <div v-loading="miniAppLoading" class="card-body card-body-flex">
+            <div class="mb-cards">
+              <div class="mb-card mb-card--primary">
+                <div class="mb-card-head">
+                  <i class="el-icon-star-on" />
+                  <span>主要小程序</span>
+                  <el-tag v-if="mainMiniApp" type="primary" size="mini" effect="plain">运行中</el-tag>
+                </div>
+                <el-select
+                  v-if="canModifyMiniApp"
+                  v-model="miniAppConfigForm.mainMiniAppId"
+                  placeholder="请选择主要小程序"
+                  clearable
+                  filterable
+                  size="small"
+                  class="mb-card-select"
+                  :disabled="miniAppSaving"
+                  @change="saveMiniAppConfig"
+                >
+                  <el-option
+                    v-for="item in companyMiniAppList"
+                    :key="'main-' + item.id"
+                    :label="item.name"
+                    :value="item.appid || item.appId"
+                  />
+                </el-select>
+                <template v-else>
+                  <div class="mb-card-name">{{ mainMiniApp ? mainMiniApp.name : '未配置' }}</div>
+                </template>
+                <div v-if="mainMiniApp" class="mb-card-appid">
+                  {{ mainMiniApp.appid || mainMiniApp.appId }}
+                </div>
+              </div>
+              <div class="mb-card mb-card--backup">
+                <div class="mb-card-head">
+                  <i class="el-icon-umbrella" />
+                  <span>备用小程序</span>
+                  <el-tag v-if="backupMiniApp" type="primary" size="mini" effect="plain">运行中</el-tag>
+                </div>
+                <el-select
+                  v-if="canModifyMiniApp"
+                  v-model="miniAppConfigForm.backupMiniAppId"
+                  placeholder="请选择备用小程序"
+                  clearable
+                  filterable
+                  size="small"
+                  class="mb-card-select"
+                  :disabled="miniAppSaving"
+                  @change="saveMiniAppConfig"
+                >
+                  <el-option
+                    v-for="item in companyMiniAppList"
+                    :key="'backup-' + item.id"
+                    :label="item.name"
+                    :value="item.appid || item.appId"
+                  />
+                </el-select>
+                <template v-else>
+                  <div class="mb-card-name">{{ backupMiniApp ? backupMiniApp.name : '未配置' }}</div>
+                </template>
+                <div v-if="backupMiniApp" class="mb-card-appid">
+                  {{ backupMiniApp.appid || backupMiniApp.appId }}
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </el-col>
+
+      <!-- 小程序列表 -->
+      <el-col :xs="24" :sm="24" :md="8" :lg="8" class="config-col">
+        <div class="config-card">
+          <div class="card-header">
+            <div class="card-header-title">
+              <i class="el-icon-s-order card-header-icon" />
+              <span>小程序列表</span>
+            </div>
+          </div>
+          <div class="card-body card-body-flex">
+            <el-input
+              v-model="listSearchName"
+              placeholder="搜索小程序名称..."
+              prefix-icon="el-icon-search"
+              clearable
+              size="small"
+              class="panel-search"
+              @input="handleListSearch"
+              @clear="handleListSearch"
+            />
+            <div class="panel-toolbar">
+              <span class="panel-count">共 {{ filteredList.length }} 个小程序</span>
+            </div>
+            <div v-if="filteredList.length" class="table-wrap">
+              <el-table
+                v-loading="loading"
+                :data="paginatedList"
+                border
+                size="small"
+                class="data-table"
+              >
+                <el-table-column label="名称" prop="name" min-width="72" show-overflow-tooltip />
+                <el-table-column label="AppID" prop="appid" min-width="100" show-overflow-tooltip />
+                <el-table-column label="状态" align="center" width="72">
+                  <template slot-scope="scope">
+                    <el-tag
+                      :type="getStatusTagType(scope.row.status)"
+                      size="mini"
+                      effect="plain"
+                    >
+                      {{ getStatusLabel(scope.row.status) }}
+                    </el-tag>
+                  </template>
+                </el-table-column>
+                <el-table-column label="修改状态" align="center" width="72">
+                  <template slot-scope="scope">
+                    <el-button
+                      v-if="canModify"
+                      type="text"
+                      icon="el-icon-edit"
+                      class="row-action-btn"
+                      @click="handleUpdate(scope.row)"
+                    />
+                    <span v-else class="no-permission-text">-</span>
+                  </template>
+                </el-table-column>
+              </el-table>
+            </div>
+            <div v-if="filteredList.length > listPageSize" class="panel-pagination">
+              <el-pagination
+                small
+                background
+                layout="prev, pager, next"
+                :current-page.sync="listPageNum"
+                :page-size="listPageSize"
+                :total="filteredList.length"
+              />
+            </div>
+            <el-empty
+              v-else-if="!loading && !filteredList.length"
+              description="暂无小程序数据"
+              :image-size="64"
+              class="panel-empty"
+            />
+          </div>
+        </div>
+      </el-col>
+    </el-row>
 
+    <!-- 修改小程序状态 -->
     <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
       <el-form ref="form" :model="form" :rules="rules" label-width="80px">
         <el-form-item label="状态" prop="status">
@@ -152,12 +314,97 @@
       </div>
     </el-dialog>
 
+    <!-- 修改企微小程序 -->
+    <el-dialog title="修改企微小程序" :visible.sync="qwOpen" width="600px" append-to-body>
+      <el-form ref="qwForm" :model="qwForm" label-width="120px">
+        <el-form-item label="企业CorpID">
+          <el-input v-model="qwForm.corpId" disabled />
+        </el-form-item>
+        <el-form-item label="企业名称">
+          <el-input v-model="qwForm.corpName" disabled />
+        </el-form-item>
+        <el-form-item label="小程序id">
+          <el-select
+            v-model="qwForm.miniAppId"
+            filterable
+            clearable
+            placeholder="请选择小程序"
+            style="width: 100%"
+          >
+            <el-option
+              v-for="item in miniAppSelectList"
+              :key="item.appId"
+              :label="item.appName"
+              :value="item.appId"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="关联公司">
+          <el-select
+            v-model="qwForm.companyIds"
+            multiple
+            filterable
+            disabled
+            placeholder="关联公司"
+            style="width: 100%"
+          >
+            <el-option
+              v-for="item in companys"
+              :key="item.companyId"
+              :label="item.companyName"
+              :value="item.companyId"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="状态">
+          <dict-tag :options="qwStatusOptions" :value="qwForm.status" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" :loading="qwSaving" @click="submitQwForm">确 定</el-button>
+        <el-button @click="qwOpen = false">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 批量修改主体小程序 -->
+    <el-dialog title="批量修改主体小程序" :visible.sync="batchMiniAppDialogVisible" width="420px" append-to-body>
+      <el-form ref="batchMiniAppForm" :model="batchMiniAppForm" :rules="batchMiniAppRules" label-width="100px">
+        <el-form-item label="选择小程序" prop="miniAppId">
+          <el-select
+            v-model="batchMiniAppForm.miniAppId"
+            placeholder="请选择小程序"
+            clearable
+            filterable
+            style="width: 100%"
+          >
+            <el-option
+              v-for="item in companyMiniAppList"
+              :key="'batch-' + item.id"
+              :label="item.name"
+              :value="item.appid || item.appId"
+            />
+          </el-select>
+        </el-form-item>
+        <el-alert
+          :title="`此操作将更新已选中的 ${qwSelectedIds.length} 个企微主体的小程序配置`"
+          type="warning"
+          :closable="false"
+          show-icon
+        />
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" :loading="batchUpdateLoading" @click="submitBatchMiniAppForm">确 定</el-button>
+        <el-button @click="cancelBatchMiniApp">取 消</el-button>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
 <script>
-import { list, get, update } from '@/api/course/coursePlaySourceConfig'
+import { list, get, update, listAll } from '@/api/course/coursePlaySourceConfig'
 import { getCompanyMiniAppList, updateMiniAppMasterServer } from '@/api/company/companyConfig'
+import { listQwCompany, getQwCompany, updateQwCompany, batchUpdateMiniAppId } from '@/api/qw/qwCompany'
+import { getCompanyList } from '@/api/company/company'
 import { resetForm } from '@/utils/common'
 import { checkPermi } from '@/utils/permission'
 
@@ -171,26 +418,46 @@ export default {
       },
       companyMiniAppList: [],
       miniAppSaving: false,
+      miniAppLoading: false,
+      qwSearchName: '',
+      qwPageNum: 1,
+      qwPageSize: 12,
+      listSearchName: '',
+      listPageNum: 1,
+      listPageSize: 12,
       queryParams: {
         pageNum: 1,
-        pageSize: 10,
+        pageSize: 9999,
         name: null,
         appid: null
       },
-      showSearch: true,
       loading: false,
       list: [],
-      total: 0,
-      typesOptions: [],
+      filteredList: [],
+      qwLoading: false,
+      qwCompanyList: [],
+      qwSelectedIds: [],
+      batchMiniAppDialogVisible: false,
+      batchMiniAppForm: {
+        miniAppId: ''
+      },
+      batchUpdateLoading: false,
+      batchMiniAppRules: {
+        miniAppId: [
+          { required: true, message: '请选择小程序', trigger: 'change' }
+        ]
+      },
+      qwOpen: false,
+      qwForm: {},
+      qwSaving: false,
+      qwStatusOptions: [],
+      companys: [],
+      miniAppSelectList: [],
       statusOptions: [
         { label: '正常', value: 0 },
         { label: '半封禁', value: 1 },
         { label: '封禁', value: 2 }
       ],
-      isMallOptions: [
-        { dictLabel: '是', dictValue: 1 },
-        { dictLabel: '否', dictValue: 0 }
-      ],
       title: null,
       open: false,
       form: {},
@@ -205,25 +472,161 @@ export default {
     companyId() {
       return this.$store.state.user.user && this.$store.state.user.user.companyId
     },
-    /** 查询详情需 query,保存需 edit */
     canModify() {
       return checkPermi(['course:playSourceConfig:query']) &&
         checkPermi(['course:playSourceConfig:edit'])
+    },
+    canModifyQw() {
+      return checkPermi(['qw:qwCompany:edit'])
+    },
+    canModifyMiniApp() {
+      return this.canModify
+    },
+    mainMiniApp() {
+      if (!this.miniAppConfigForm.mainMiniAppId) return null
+      return this.findMiniAppByAppId(this.miniAppConfigForm.mainMiniAppId)
+    },
+    backupMiniApp() {
+      if (!this.miniAppConfigForm.backupMiniAppId) return null
+      return this.findMiniAppByAppId(this.miniAppConfigForm.backupMiniAppId)
+    },
+    filteredQwCompanyList() {
+      const keyword = (this.qwSearchName || '').trim().toLowerCase()
+      if (!keyword) {
+        return this.qwCompanyList
+      }
+      return this.qwCompanyList.filter(item => {
+        const corpName = (item.corpName || '').toLowerCase()
+        const corpId = (item.corpId || '').toLowerCase()
+        const miniName = (item.miniName || '').toLowerCase()
+        const companyNames = this.getRelatedCompanyNames(item).toLowerCase()
+        return corpName.includes(keyword) ||
+          corpId.includes(keyword) ||
+          miniName.includes(keyword) ||
+          companyNames.includes(keyword)
+      })
+    },
+    paginatedQwCompanyList() {
+      const start = (this.qwPageNum - 1) * this.qwPageSize
+      return this.filteredQwCompanyList.slice(start, start + this.qwPageSize)
+    },
+    paginatedList() {
+      const start = (this.listPageNum - 1) * this.listPageSize
+      return this.filteredList.slice(start, start + this.listPageSize)
+    }
+  },
+  watch: {
+    filteredQwCompanyList() {
+      this.qwPageNum = 1
+    },
+    filteredList() {
+      this.listPageNum = 1
     }
   },
   created() {
-    this.getDicts('play_source_type').then(response => {
-      this.typesOptions = response.data.map(item => ({
-        ...item,
-        listClass: 'primary'
-      }))
+    this.getDicts('sys_company_status').then(response => {
+      this.qwStatusOptions = response.data
     })
+    getCompanyList().then(response => {
+      this.companys = response.data || []
+    })
+    this.loadQwCompany()
     this.loadMiniAppMasterServer()
     this.getList()
   },
   methods: {
     resetForm,
+    findMiniAppByAppId(appId) {
+      return this.companyMiniAppList.find(
+        item => (item.appid || item.appId) === appId
+      )
+    },
+    loadQwCompany() {
+      this.qwLoading = true
+      listQwCompany({
+        pageNum: 1,
+        pageSize: 9999
+      }).then(response => {
+        this.qwCompanyList = response.rows || []
+      }).catch(() => {
+        this.qwCompanyList = []
+      }).finally(() => {
+        this.qwLoading = false
+      })
+    },
+    handleQwSearch() {
+      this.qwPageNum = 1
+    },
+    handleQwSelectionChange(selection) {
+      this.qwSelectedIds = selection.map(item => item.id)
+    },
+    handleBatchUpdateMiniAppId() {
+      if (!this.qwSelectedIds.length) {
+        this.msgWarning('请先勾选需要更换的主体')
+        return
+      }
+      if (!this.companyMiniAppList.length) {
+        this.msgWarning('暂无可用小程序,请稍后再试')
+        return
+      }
+      this.batchMiniAppForm = { miniAppId: '' }
+      this.batchMiniAppDialogVisible = true
+      this.$nextTick(() => {
+        if (this.$refs.batchMiniAppForm) {
+          this.$refs.batchMiniAppForm.clearValidate()
+        }
+      })
+    },
+    cancelBatchMiniApp() {
+      this.batchMiniAppDialogVisible = false
+      this.batchMiniAppForm = { miniAppId: '' }
+      if (this.$refs.batchMiniAppForm) {
+        this.$refs.batchMiniAppForm.resetFields()
+      }
+    },
+    submitBatchMiniAppForm() {
+      this.$refs.batchMiniAppForm.validate(valid => {
+        if (!valid) return
+        this.$confirm(
+          `确定要将已选中的 ${this.qwSelectedIds.length} 个企微主体的小程序更换为所选小程序吗?`,
+          '提示',
+          {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            type: 'warning'
+          }
+        ).then(() => {
+          this.batchUpdateLoading = true
+          batchUpdateMiniAppId({
+            miniAppId: this.batchMiniAppForm.miniAppId,
+            ids: this.qwSelectedIds
+          }).then(response => {
+            this.msgSuccess(response.msg || '批量更新完成')
+            this.batchMiniAppDialogVisible = false
+            this.qwSelectedIds = []
+            if (this.$refs.qwCompanyTable) {
+              this.$refs.qwCompanyTable.clearSelection()
+            }
+            this.loadQwCompany()
+          }).catch(() => {
+            this.msgError('批量更新失败')
+          }).finally(() => {
+            this.batchUpdateLoading = false
+          })
+        }).catch(() => {})
+      })
+    },
+    getRelatedCompanyNames(item) {
+      if (!item || !item.companyIds) return '-'
+      const ids = String(item.companyIds).split(',').filter(Boolean)
+      const names = ids.map(id => {
+        const company = this.companys.find(c => String(c.companyId) === String(id))
+        return company ? company.companyName : id
+      })
+      return names.length ? names.join(' · ') : '-'
+    },
     loadMiniAppMasterServer() {
+      this.miniAppLoading = true
       getCompanyMiniAppList().then(res => {
         this.companyMiniAppList = res.data || []
         const current = res.current
@@ -238,9 +641,11 @@ export default {
         this.companyMiniAppList = []
         this.miniAppConfigForm.mainMiniAppId = null
         this.miniAppConfigForm.backupMiniAppId = null
+      }).finally(() => {
+        this.miniAppLoading = false
       })
     },
-    onSubmitMiniApp() {
+    saveMiniAppConfig() {
       const miniAppMaster = this.miniAppConfigForm.mainMiniAppId
         ? [this.miniAppConfigForm.mainMiniAppId]
         : []
@@ -254,13 +659,56 @@ export default {
           this.loadMiniAppMasterServer()
         } else {
           this.msgError(response.msg || '保存失败')
+          this.loadMiniAppMasterServer()
         }
       }).catch(() => {
         this.msgError('保存失败')
+        this.loadMiniAppMasterServer()
       }).finally(() => {
         this.miniAppSaving = false
       })
     },
+    getAppList() {
+      this.miniAppSelectList = []
+      listAll().then(response => {
+        const code = response.code
+        const dataList = response.rows || response.data || []
+        if (code === 200 && dataList.length) {
+          this.miniAppSelectList = dataList
+            .filter(v => v.type == 1)
+            .map(v => ({ appId: v.appid, appName: v.name }))
+        }
+      })
+    },
+    handleUpdateQwCompany(item) {
+      if (!item) return
+      this.getAppList()
+      getQwCompany(item.id).then(response => {
+        this.qwForm = response.data
+        if (this.qwForm.companyIds) {
+          this.qwForm.companyIds = String(this.qwForm.companyIds)
+            .split(',')
+            .map(Number)
+        }
+        this.qwOpen = true
+      })
+    },
+    submitQwForm() {
+      this.qwSaving = true
+      const payload = { ...this.qwForm }
+      if (Array.isArray(payload.companyIds)) {
+        payload.companyIds = payload.companyIds.join(',')
+      }
+      updateQwCompany(payload).then(() => {
+        this.msgSuccess('修改成功')
+        this.qwOpen = false
+        this.loadQwCompany()
+      }).catch(() => {
+        this.msgError('修改失败')
+      }).finally(() => {
+        this.qwSaving = false
+      })
+    },
     getCompanyQueryParams() {
       const companyIdNum = Number(this.companyId)
       if (!Number.isFinite(companyIdNum)) {
@@ -275,24 +723,38 @@ export default {
       const params = this.getCompanyQueryParams()
       if (!params) {
         this.list = []
-        this.total = 0
+        this.filteredList = []
         this.loading = false
         return
       }
       this.loading = true
       list(params).then(response => {
-        this.list = response.rows
-        this.total = response.total
+        this.list = response.rows || []
+        this.applyListFilter()
+        this.loading = false
+      }).catch(() => {
+        this.list = []
+        this.filteredList = []
         this.loading = false
       })
     },
-    handleQuery() {
-      this.queryParams.pageNum = 1
-      this.getList()
+    applyListFilter() {
+      const keyword = (this.listSearchName || '').trim().toLowerCase()
+      if (!keyword) {
+        this.filteredList = this.list
+        return
+      }
+      this.filteredList = this.list.filter(item =>
+        (item.name || '').toLowerCase().includes(keyword)
+      )
+    },
+    handleListSearch() {
+      this.listPageNum = 1
+      this.applyListFilter()
     },
-    resetQuery() {
-      this.resetForm('queryForm')
-      this.getList()
+    getMiniAppCorpName(appid) {
+      const qw = this.qwCompanyList.find(item => item.miniAppId === appid)
+      return qw ? qw.corpName : '-'
     },
     handleUpdate(row) {
       get(row.id).then(response => {
@@ -301,14 +763,12 @@ export default {
           type: response.data.type != null ? response.data.type.toString() : response.data.type
         }
         this.open = true
-        this.title = '修改小程序配置'
+        this.title = '修改小程序状态'
       })
     },
     submitForm() {
       this.$refs.form.validate(valid => {
-        if (!valid) {
-          return
-        }
+        if (!valid) return
         const companyIdNum = Number(this.companyId)
         if (!Number.isFinite(companyIdNum)) {
           this.msgError('获取销售公司失败')
@@ -341,16 +801,343 @@ export default {
         '2': '封禁'
       }
       return statusMap[status] || '未知'
+    },
+    getStatusTagType(status) {
+      if (status === 0 || status === '0') return 'success'
+      if (status === 1 || status === '1') return 'warning'
+      if (status === 2 || status === '2') return 'danger'
+      return 'info'
+    },
+    getQwStatusLabel(status) {
+      const item = this.qwStatusOptions.find(
+        opt => String(opt.dictValue) === String(status)
+      )
+      return item ? item.dictLabel : '未知'
+    },
+    getQwStatusLight(status) {
+      const label = this.getQwStatusLabel(status)
+      if (String(status) === '0' || label === '正常') {
+        return 'green'
+      }
+      if (String(status) === '1' || (label && label.includes('禁'))) {
+        return 'red'
+      }
+      return 'gray'
     }
   }
 }
 </script>
 
-<style scoped>
-.mini-app-config-form {
-  margin-bottom: 16px;
-  padding: 16px 16px 0;
-  background: #f5f7fa;
-  border-radius: 4px;
+<style scoped lang="scss">
+.miniapp-config-page {
+  padding: 20px;
+  background: #f0f2f5;
+  min-height: calc(100vh - 84px);
+}
+
+.page-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 20px;
+  gap: 16px;
+  flex-wrap: wrap;
+}
+
+.page-header-left {
+  display: flex;
+  align-items: flex-start;
+  gap: 16px;
+}
+
+.page-header-icon {
+  width: 48px;
+  height: 48px;
+  border-radius: 12px;
+  background: linear-gradient(135deg, #409eff, #66b1ff);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+
+  i {
+    font-size: 24px;
+    color: #fff;
+  }
+}
+
+.page-title {
+  margin: 0 0 4px;
+  font-size: 20px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.page-subtitle {
+  margin: 0;
+  font-size: 13px;
+  color: #909399;
+  line-height: 1.5;
+}
+
+.page-stats {
+  display: flex;
+  gap: 12px;
+}
+
+.stat-chip {
+  background: #fff;
+  border-radius: 10px;
+  padding: 10px 18px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  min-width: 72px;
+}
+
+.stat-num {
+  font-size: 22px;
+  font-weight: 700;
+  color: #409eff;
+  line-height: 1.2;
+}
+
+.stat-label {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 2px;
+}
+
+.config-columns {
+  align-items: stretch;
+  height: calc(100vh - 168px);
+  min-height: 560px;
+}
+
+.config-col {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.config-card {
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  height: 100%;
+}
+
+.card-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 14px 16px;
+  border-bottom: 1px solid #f0f0f0;
+  flex-shrink: 0;
+}
+
+.card-header-title {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-size: 14px;
+  font-weight: 600;
+  color: #303133;
+
+  .card-header-icon {
+    font-size: 16px;
+    color: #409eff;
+  }
+}
+
+.card-body {
+  padding: 14px 16px;
+  flex: 1;
+  min-height: 0;
+}
+
+.card-body-flex {
+  display: flex;
+  flex-direction: column;
+}
+
+.panel-search {
+  flex-shrink: 0;
+  margin-bottom: 8px;
+}
+
+.panel-toolbar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 8px;
+  flex-shrink: 0;
+  gap: 8px;
+}
+
+.panel-actions {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  flex-shrink: 0;
+}
+
+.selected-count {
+  font-size: 12px;
+  color: #409eff;
+  white-space: nowrap;
+}
+
+.panel-count {
+  font-size: 12px;
+  color: #909399;
+}
+
+.table-wrap {
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+  border: 1px solid #ebeef5;
+  border-radius: 8px;
+}
+
+.data-table {
+  ::v-deep .el-table th {
+    background: #fafafa;
+    color: #606266;
+    font-weight: 500;
+    padding: 6px 0;
+  }
+
+  ::v-deep .el-table td {
+    padding: 6px 0;
+  }
+
+}
+
+.row-action-btn {
+  padding: 4px;
+  font-size: 14px;
+}
+
+.status-light-wrap {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  cursor: default;
+}
+
+.status-light {
+  width: 10px;
+  height: 10px;
+  border-radius: 50%;
+
+  &--green {
+    background: #52c41a;
+    box-shadow: 0 0 0 2px rgba(82, 196, 26, 0.2), 0 0 8px rgba(82, 196, 26, 0.55);
+  }
+
+  &--red {
+    background: #f5222d;
+    box-shadow: 0 0 0 2px rgba(245, 34, 45, 0.2), 0 0 8px rgba(245, 34, 45, 0.55);
+  }
+
+  &--gray {
+    background: #bfbfbf;
+    box-shadow: 0 0 0 2px rgba(191, 191, 191, 0.2);
+  }
+}
+
+.panel-pagination {
+  flex-shrink: 0;
+  padding-top: 10px;
+  text-align: center;
+
+  ::v-deep .el-pagination {
+    padding: 0;
+  }
+}
+
+.panel-empty {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.mb-cards {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+  flex-shrink: 0;
+}
+
+.mb-card {
+  border-radius: 10px;
+  padding: 12px 14px;
+  border: 1px solid #eef0f3;
+
+  &--primary {
+    background: linear-gradient(135deg, #f0f7ff 0%, #fafcff 100%);
+    border-color: #d9ecff;
+    border-left: 3px solid #409eff;
+  }
+
+  &--backup {
+    background: #fafafa;
+    border-left: 3px solid #909399;
+  }
+}
+
+.mb-card-head {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 12px;
+  font-weight: 600;
+  color: #606266;
+  margin-bottom: 8px;
+
+  i {
+    font-size: 14px;
+    color: #e6a23c;
+  }
+
+  .mb-card--backup & i {
+    color: #909399;
+  }
+
+  .el-tag {
+    margin-left: auto;
+  }
+}
+
+.mb-card-appid {
+  font-size: 11px;
+  color: #909399;
+  font-family: monospace;
+  word-break: break-all;
+  margin-top: 8px;
+}
+
+.mb-card-select {
+  width: 100%;
+}
+
+.mb-card-name {
+  font-size: 15px;
+  font-weight: 600;
+  color: #303133;
+  margin-bottom: 2px;
+}
+
+.no-permission-text {
+  color: #c0c4cc;
+  font-size: 12px;
 }
 </style>

+ 24 - 3
src/views/qw/sop/addSop.vue

@@ -124,7 +124,7 @@
           </el-select>
           <Tip title="选择的企业微信员工下面的群聊" />
         </el-form-item>
-        <el-form-item label="客户评级" prop="isRating" style="margin-top: 2%" v-if="form.filterMode == 1">
+        <el-form-item label="客户评级" prop="isRating" style="margin-top: 2%" v-if="form.filterMode == 1 && form.level !== 1">
           <el-switch
             v-model="form.isRating"
             active-color="#13ce66"
@@ -478,6 +478,7 @@
 
 <script>
 import { addSop, courseList, updateSop } from '@/api/qw/sop'
+import { getCompanyInfo } from '@/api/company/company'
 import { getQwAllUserList, listUser } from '@/api/company/companyUser'
 import qwUserList from '@/views/qw/user/qwUserList.vue'
 import companyUserList from '@/views/company/companyUser/companyUserList.vue'
@@ -601,6 +602,7 @@ export default {
         expiryTime: 4,
         isAutoSop: 1,
         autoSopTime: {autoSopType: 2, autoStartTime: '00:00', autoEndTime: '24:00', autoSopSend: 2},
+        level: 0,
       },
       userSelectList: [],
       qwUserIds: [],
@@ -652,6 +654,7 @@ export default {
       }
     );
     this.refreshData(this.form.corpId);
+    this.fetchCompanyLevel();
 
     courseList().then(response => {
       this.courseList = response.list;
@@ -669,6 +672,20 @@ export default {
   },
   methods: {
 
+    fetchCompanyLevel() {
+      getCompanyInfo().then(response => {
+        this.form.level = response.data?.level ?? 0;
+      });
+    },
+
+    buildSubmitData() {
+      const submitData = { ...this.form };
+      if (submitData.level === 1) {
+        delete submitData.isRating;
+      }
+      return submitData;
+    },
+
     //刷新部分数据
     refreshData(row) {
 
@@ -954,6 +971,7 @@ export default {
         createBy: null,
         createTime: null,
         isRating: null,
+        level: 0,
         autoSopTime: {autoSopType: 2, autoStartTime: '00:00', autoEndTime: '24:00', autoSopSend: 2},
         openCommentStatus: 0,
       };
@@ -1067,8 +1085,10 @@ export default {
 
           this.form.autoSopTime = JSON.stringify(this.form.autoSopTime)
 
+          const submitData = this.buildSubmitData();
           if (this.form.id != null) {
-            updateSop(this.form).then(response => {
+            updateSop(submitData).then(response => {
+              this.form.level = response.level ?? response.data?.level ?? this.form.level;
               this.msgSuccess("修改成功");
               this.$store.dispatch("tagsView/delView", this.$route);
               // this.$router.replace('/qw/conversion/sop')
@@ -1083,7 +1103,8 @@ export default {
               this.reset();
             });
           } else {
-            addSop(this.form).then(response => {
+            addSop(submitData).then(response => {
+              this.form.level = response.data?.level ?? response.level ?? this.form.level;
               this.msgSuccess("新增成功");
               this.$store.dispatch("tagsView/delView", this.$route);
               // this.$router.replace('/qw/conversion/sop')

+ 28 - 5
src/views/qw/sop/sop.vue

@@ -555,7 +555,7 @@
             (小时)
           </el-col>
 
-          <el-col style="margin-top: 3%">
+          <el-col style="margin-top: 3%" v-if="outTimeOpen.level !== 1">
             <span>是否开启客户评级:</span>
             <el-switch
               v-model="outTimeOpen.isRating"
@@ -1109,6 +1109,7 @@ import {
 import {sendMsgSop} from "@/api/qw/sopUserLogsInfo";
 import {listSopTemp} from "@/api/qw/sopTemp";
 import {getQwAllUserList, listUser} from '@/api/company/companyUser'
+import { getCompanyInfo } from '@/api/company/company'
 import qwUserList from '@/views/qw/user/qwUserList.vue'
 import ImageUpload from "@/views/qw/sop/ImageUpload";
 import CustomerGroupDetails from '@/views/qw/groupMsg/customerGroupDetails.vue'
@@ -1235,8 +1236,10 @@ export default {
         tempId: null,
         expiryTime: null,
         isRating: null,
+        level: 0,
         isSampSend:null,
       },
+      companyLevel: 0,
       updateSopNameDialog: {
         title: '修改规则名称',
         open: false,
@@ -1346,6 +1349,7 @@ export default {
       if (this.myQwCompanyList != null) {
         this.queryParams.corpId = this.myQwCompanyList[0].dictValue;
         this.refreshData(this.queryParams.corpId);
+        this.fetchCompanyLevel();
         this.getList();
       }
     });
@@ -1654,6 +1658,7 @@ export default {
     updateCorpId() {
       this.reset();
       this.refreshData(this.queryParams.corpId);
+      this.fetchCompanyLevel();
       this.queryParams.qwUserId = null;
       this.getList();
     },
@@ -1714,20 +1719,32 @@ export default {
       this.outTimeOpen.maxConversionDay=val.maxConversionDay;
       this.outTimeOpen.courseDay=val.courseDay;
       this.outTimeOpen.isSampSend=val.isSampSend;
+      this.outTimeOpen.level = val.level != null ? val.level : this.companyLevel;
       this.outTimeOpen.open = true;
+      if (val.level == null) {
+        getSop(val.id).then(response => {
+          if (response.data?.level != null) {
+            this.outTimeOpen.level = response.data.level;
+          }
+        });
+      }
     },
 
     handleUpdateExpiryTime() {
-      updateSop({
+      const data = {
         id: this.outTimeOpen.id,
         tempId: this.outTimeOpen.tempId,
         expiryTime: this.outTimeOpen.expiryTime,
-        isRating: this.outTimeOpen.isRating,
         minConversionDay: this.outTimeOpen.minConversionDay,
         maxConversionDay: this.outTimeOpen.maxConversionDay,
         courseDay: this.outTimeOpen.courseDay,
-        isSampSend:this.outTimeOpen.isSampSend
-      }).then(response => {
+        isSampSend: this.outTimeOpen.isSampSend
+      };
+      if (this.outTimeOpen.level !== 1) {
+        data.isRating = this.outTimeOpen.isRating;
+      }
+      updateSop(data).then(response => {
+        this.outTimeOpen.level = response.level ?? response.data?.level ?? this.outTimeOpen.level;
         this.msgSuccess("修改成功");
         this.resetUpdateExpiryTime()
         this.getList();
@@ -1743,9 +1760,15 @@ export default {
         minConversionDay: null,
         maxConversionDay: null,
         isRating: null,
+        level: 0,
         isSampSend:null,
       }
     },
+    fetchCompanyLevel() {
+      getCompanyInfo().then(response => {
+        this.companyLevel = response.data?.level ?? 0;
+      });
+    },
     addContent(index) {
       // this.setting[index].content.push({type:1})
       if (this.form.sendType == 2) {