Просмотр исходного кода

Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into dev

YunaiV 9 месяцев назад
Родитель
Сommit
15f5963e68

BIN
.image/common/ai-feature.png


+ 2 - 2
src/api/ai/workflow/index.ts

@@ -20,6 +20,6 @@ export const deleteWorkflow = async (id) => {
   return await request.delete({ url: '/ai/workflow/delete?id=' + id })
 }
 
-export const updateWorkflowModel = async (data) => {
-  return await request.put({ url: '/ai/workflow/updateWorkflowModel', data })
+export const testWorkflow = async (data) => {
+  return await request.post({ url: '/ai/workflow/test', data })
 }

+ 5 - 0
src/api/system/tenant/index.ts

@@ -41,6 +41,11 @@ export const getTenant = (id: number) => {
   return request.get({ url: '/system/tenant/get?id=' + id })
 }
 
+// 获取租户精简信息列表
+export const getTenantList = () => {
+  return request.get({ url: '/system/tenant/simple-list' })
+}
+
 // 新增租户
 export const createTenant = (data: TenantVO) => {
   return request.post({ url: '/system/tenant/create', data })

+ 1 - 1
src/components/Dialog/src/Dialog.vue

@@ -91,7 +91,7 @@ const dialogStyle = computed(() => {
             icon="ep:close"
             hover-color="var(--el-color-primary)"
             color="var(--el-color-info)"
-            @click="close"
+            @click.stop="close"
           />
         </div>
       </div>

+ 2 - 1
src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue

@@ -237,7 +237,7 @@ const props = defineProps({
 const prefix = inject('prefix')
 const width = inject('width')
 
-const formKey = ref('')
+const formKey = ref(undefined)
 const businessKey = ref('')
 const optionModelTitle = ref('')
 const fieldList = ref<any[]>([])
@@ -462,6 +462,7 @@ const updateElementExtensions = () => {
 const formList = ref([]) // 流程表单的下拉框的数据
 onMounted(async () => {
   formList.value = await FormApi.getFormSimpleList()
+  formKey.value = parseInt(formKey.value)
 })
 
 watch(

+ 14 - 2
src/config/axios/service.ts

@@ -3,7 +3,14 @@ import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestCo
 import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
 import qs from 'qs'
 import { config } from '@/config/axios/config'
-import { getAccessToken, getRefreshToken, getTenantId, removeToken, setToken } from '@/utils/auth'
+import {
+  getAccessToken,
+  getRefreshToken,
+  getTenantId,
+  getVisitTenantId,
+  removeToken,
+  setToken
+} from '@/utils/auth'
 import errorCode from './errorCode'
 
 import { resetRouter } from '@/router'
@@ -24,7 +31,7 @@ export const isRelogin = { show: false }
 let requestList: any[] = []
 // 是否正在刷新中
 let isRefreshToken = false
-// 请求白名单,无须token的接口
+// 请求白名单,无须 token 的接口
 const whiteList: string[] = ['/login', '/refresh-token']
 
 // 创建axios实例
@@ -55,6 +62,11 @@ service.interceptors.request.use(
     if (tenantEnable && tenantEnable === 'true') {
       const tenantId = getTenantId()
       if (tenantId) config.headers['tenant-id'] = tenantId
+      // 只有登录时,才设置 visit-tenant-id 访问租户
+      const visitTenantId = getVisitTenantId()
+      if (config.headers.Authorization && visitTenantId) {
+        config.headers['visit-tenant-id'] = visitTenantId
+      }
     }
     const method = config.method?.toUpperCase()
     // 防止 GET 请求缓存

+ 2 - 0
src/hooks/web/useCache.ts

@@ -10,6 +10,7 @@ export const CACHE_KEY = {
   // 用户相关
   ROLE_ROUTERS: 'roleRouters',
   USER: 'user',
+  VisitTenantId: 'visitTenantId',
   // 系统设置
   IS_DARK: 'isDark',
   LANG: 'lang',
@@ -35,5 +36,6 @@ export const deleteUserCache = () => {
   const { wsCache } = useCache()
   wsCache.delete(CACHE_KEY.USER)
   wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
+  wsCache.delete(CACHE_KEY.VisitTenantId)
   // 注意,不要清理 LoginForm 登录表单
 }

+ 46 - 0
src/layout/components/TenantVisit/index.vue

@@ -0,0 +1,46 @@
+<template>
+  <div>
+    <el-select
+      filterable
+      placeholder="请选择租户"
+      class="!w-180px"
+      v-model="value"
+      @change="handleChange"
+      clearable
+    >
+      <el-option v-for="item in tenants" :key="item.id" :label="item.name" :value="item.id" />
+    </el-select>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted } from 'vue'
+import * as TenantApi from '@/api/system/tenant'
+import { getVisitTenantId, setVisitTenantId } from '@/utils/auth'
+import { useMessage } from '@/hooks/web/useMessage'
+import { useTagsView } from '@/hooks/web/useTagsView'
+
+const message = useMessage() // 消息弹窗
+const tagsView = useTagsView() // 标签页操作
+
+const value = ref(getVisitTenantId()) // 当前选中的租户 ID
+const tenants = ref<any[]>([]) // 租户列表
+
+const handleChange = (id: number) => {
+  // 设置访问租户 ID
+  setVisitTenantId(id)
+  // 关闭其他标签页,只保留当前页
+  tagsView.closeOther()
+  // 刷新当前页面
+  tagsView.refreshPage()
+  // 提示切换成功
+  const tenant = tenants.value.find((item) => item.id === id)
+  if (tenant) {
+    message.success(`切换当前租户为: ${tenant.name}`)
+  }
+}
+
+onMounted(async () => {
+  tenants.value = await TenantApi.getTenantList()
+})
+</script>

+ 8 - 0
src/layout/components/ToolHeader.vue

@@ -8,8 +8,10 @@ import { Breadcrumb } from '@/layout/components/Breadcrumb'
 import { SizeDropdown } from '@/layout/components/SizeDropdown'
 import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
 import RouterSearch from '@/components/RouterSearch/index.vue'
+import TenantVisit from '@/layout/components/TenantVisit/index.vue'
 import { useAppStore } from '@/store/modules/app'
 import { useDesign } from '@/hooks/web/useDesign'
+import { checkPermi } from '@/utils/permission'
 
 const { getPrefixCls, variables } = useDesign()
 
@@ -41,6 +43,11 @@ const locale = computed(() => appStore.getLocale)
 // 消息图标
 const message = computed(() => appStore.getMessage)
 
+// 租户切换权限
+const hasTenantVisitPermission = computed(
+  () => import.meta.env.VITE_APP_TENANT_ENABLE === 'true' && checkPermi(['system:tenant:visit'])
+)
+
 export default defineComponent({
   name: 'ToolHeader',
   setup() {
@@ -62,6 +69,7 @@ export default defineComponent({
           </div>
         ) : undefined}
         <div class="h-full flex items-center">
+          {hasTenantVisitPermission.value ? <TenantVisit /> : undefined}
           {screenfull.value ? (
             <Screenfull class="custom-hover" color="var(--top-header-text-color)"></Screenfull>
           ) : undefined}

+ 10 - 2
src/utils/auth.ts

@@ -67,6 +67,14 @@ export const getTenantId = () => {
   return wsCache.get(CACHE_KEY.TenantId)
 }
 
-export const setTenantId = (username: string) => {
-  wsCache.set(CACHE_KEY.TenantId, username)
+export const setTenantId = (tenantId: number) => {
+  wsCache.set(CACHE_KEY.TenantId, tenantId)
+}
+
+export const getVisitTenantId = () => {
+  return wsCache.get(CACHE_KEY.VisitTenantId)
+}
+
+export const setVisitTenantId = (visitTenantId: number) => {
+  wsCache.set(CACHE_KEY.VisitTenantId, visitTenantId)
 }

+ 2 - 0
src/views/ai/chat/index/index.vue

@@ -462,6 +462,8 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
       (error) => {
         message.alert(`对话异常! ${error}`)
         stopStream()
+        // 需要抛出异常,禁止重试
+        throw error
       },
       () => {
         stopStream()

+ 2 - 0
src/views/ai/mindmap/index/index.vue

@@ -80,6 +80,8 @@ const submit = (data: AiMindMapGenerateReqVO) => {
     onError(err) {
       console.error('生成思维导图失败', err)
       stopStream()
+      // 需要抛出异常,禁止重试
+      throw error
     },
     ctrl: ctrl.value
   })

+ 5 - 2
src/views/ai/model/model/ModelForm.vue

@@ -4,7 +4,7 @@
       ref="formRef"
       :model="formData"
       :rules="formRules"
-      label-width="120px"
+      label-width="130px"
       v-loading="formLoading"
     >
       <el-form-item label="所属平台" prop="platform">
@@ -146,7 +146,10 @@ const formRules = reactive({
   platform: [{ required: true, message: '所属平台不能为空', trigger: 'blur' }],
   type: [{ required: true, message: '模型类型不能为空', trigger: 'blur' }],
   sort: [{ required: true, message: '排序不能为空', trigger: 'blur' }],
-  status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
+  status: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
+  temperature: [{ required: true, message: '温度参数不能为空', trigger: 'blur' }],
+  maxTokens: [{ required: true, message: '回复数 Token 数不能为空', trigger: 'blur' }],
+  maxContexts: [{ required: true, message: '上下文数量不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
 const apiKeyList = ref([] as ApiKeyVO[]) // API 密钥列表

+ 202 - 1
src/views/ai/workflow/form/WorkflowDesign.vue

@@ -13,11 +13,58 @@
         测试
       </el-button>
     </div>
+
+    <!-- 测试窗口 -->
+    <el-drawer v-model="showTestDrawer" title="工作流测试" :modal="false">
+      <fieldset>
+        <legend class="ml-15px"><h3>运行参数配置</h3></legend>
+        <div class="p-20px">
+          <div
+            class="flex justify-around mb-10px"
+            v-for="(param, index) in params4Test"
+            :key="index"
+          >
+            <el-select class="w-200px!" v-model="param.key" placeholder="参数名">
+              <el-option
+                v-for="(value, key) in paramsOfStartNode"
+                :key="key"
+                :label="value?.description || key"
+                :value="key"
+                :disabled="!!value?.disabled"
+              />
+            </el-select>
+            <el-input class="w-200px!" v-model="param.value" placeholder="参数值" />
+            <el-button type="danger" plain :icon="Delete" circle @click="removeParam(index)" />
+          </div>
+          <!-- TODO @lesan:是不是不用添加和删除参数,直接把必填和选填列出来,然后加上参数校验? -->
+          <el-button type="primary" plain @click="addParam">添加参数</el-button>
+        </div>
+      </fieldset>
+      <fieldset class="mt-20px bg-#f8f9fa">
+        <legend class="ml-15px"><h3>运行结果</h3></legend>
+        <div class="p-20px">
+          <div v-if="loading"> <el-text type="primary">执行中...</el-text></div>
+          <div v-else-if="error">
+            <el-text type="danger">{{ error }}</el-text>
+          </div>
+          <pre v-else-if="testResult" class="result-content"
+            >{{ JSON.stringify(testResult, null, 2) }}
+          </pre>
+          <div v-else> <el-text type="info">点击运行查看结果</el-text> </div>
+        </div>
+      </fieldset>
+      <el-button class="mt-20px w-100%" size="large" type="success" @click="goRun">
+        运行流程
+      </el-button>
+    </el-drawer>
   </div>
 </template>
 
 <script setup lang="ts">
 import Tinyflow from '@/components/Tinyflow/Tinyflow.vue'
+import * as WorkflowApi from '@/api/ai/workflow'
+// TODO @lesan:要不使用 ICon 哪个组件哈
+import { Delete } from '@element-plus/icons-vue'
 
 defineProps<{
   provider: any
@@ -25,9 +72,149 @@ defineProps<{
 
 const tinyflowRef = ref()
 const workflowData = inject('workflowData') as Ref
+const showTestDrawer = ref(false)
+const params4Test = ref([])
+const paramsOfStartNode = ref({})
+const testResult = ref(null)
+const loading = ref(false)
+const error = ref(null)
 
+/** 展示工作流测试抽屉 */
 const testWorkflowModel = () => {
-  // TODO @lesan 测试
+  showTestDrawer.value = !showTestDrawer.value
+}
+
+/** 运行流程 */
+const goRun = async () => {
+  try {
+    const val = tinyflowRef.value.getData()
+    loading.value = true
+    error.value = null
+    testResult.value = null
+    /// 查找start节点
+    const startNode = getStartNode()
+
+    // 获取参数定义
+    const parameters = startNode.data?.parameters || []
+    const paramDefinitions = {}
+    parameters.forEach((param) => {
+      paramDefinitions[param.name] = param.dataType
+    })
+
+    // 参数类型转换
+    const convertedParams = {}
+    for (const { key, value } of params4Test.value) {
+      const paramKey = key.trim()
+      if (!paramKey) continue
+
+      let dataType = paramDefinitions[paramKey]
+      if (!dataType) {
+        dataType = 'String'
+      }
+
+      try {
+        convertedParams[paramKey] = convertParamValue(value, dataType)
+      } catch (e) {
+        throw new Error(`参数 ${paramKey} 转换失败: ${e.message}`)
+      }
+    }
+
+    const data = {
+      graph: JSON.stringify(val),
+      params: convertedParams
+    }
+
+    const response = await WorkflowApi.testWorkflow(data)
+    testResult.value = response
+  } catch (err) {
+    error.value = err.response?.data?.message || '运行失败,请检查参数和网络连接'
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 监听测试抽屉的开启,获取开始节点参数列表 */
+watch(showTestDrawer, (value) => {
+  if (!value) return
+
+  /// 查找start节点
+  const startNode = getStartNode()
+
+  // 获取参数定义
+  const parameters = startNode.data?.parameters || []
+  const paramDefinitions = {}
+
+  // 加入参数选项方便用户添加非必须参数
+  parameters.forEach((param) => {
+    paramDefinitions[param.name] = param
+  })
+
+  function mergeIfRequiredButNotSet(target) {
+    let needPushList = []
+    for (let key in paramDefinitions) {
+      let param = paramDefinitions[key]
+
+      if (param.required) {
+        let item = target.find((item) => item.key === key)
+
+        if (!item) {
+          needPushList.push({ key: param.name, value: param.defaultValue || '' })
+        }
+      }
+    }
+    target.push(...needPushList)
+  }
+  // 自动装载需必填的参数
+  mergeIfRequiredButNotSet(params4Test.value)
+
+  paramsOfStartNode.value = paramDefinitions
+})
+
+/** 获取开始节点 */
+const getStartNode = () => {
+  const val = tinyflowRef.value.getData()
+  const startNode = val.nodes.find((node) => node.type === 'startNode')
+  if (!startNode) {
+    throw new Error('流程缺少开始节点')
+  }
+  return startNode
+}
+
+/** 添加参数项 */
+const addParam = () => {
+  params4Test.value.push({ key: '', value: '' })
+}
+
+/** 删除参数项 */
+const removeParam = (index) => {
+  params4Test.value.splice(index, 1)
+}
+
+/** 类型转换函数 */
+const convertParamValue = (value, dataType) => {
+  if (value === '') return null // 空值处理
+
+  switch (dataType) {
+    case 'String':
+      return String(value)
+    case 'Number':
+      const num = Number(value)
+      if (isNaN(num)) throw new Error('非数字格式')
+      return num
+    case 'Boolean':
+      if (value.toLowerCase() === 'true') return true
+      if (value.toLowerCase() === 'false') return false
+      throw new Error('必须为 true/false')
+    case 'Object':
+    case 'Array':
+      try {
+        return JSON.parse(value)
+      } catch (e) {
+        throw new Error(`JSON格式错误: ${e.message}`)
+      }
+    default:
+      throw new Error(`不支持的类型: ${dataType}`)
+  }
 }
 
 /** 表单校验 */
@@ -47,3 +234,17 @@ defineExpose({
   validate
 })
 </script>
+
+<style lang="css" scoped>
+.result-content {
+  background: white;
+  padding: 12px;
+  border-radius: 4px;
+  max-height: 300px;
+  overflow: auto;
+  font-family: Monaco, Consolas, monospace;
+  font-size: 14px;
+  line-height: 1.5;
+  white-space: pre-wrap;
+}
+</style>

+ 16 - 11
src/views/ai/workflow/form/index.vue

@@ -59,7 +59,7 @@
         <WorkflowDesign
           v-if="currentStep === 1"
           v-model="formData"
-          :provider="provider"
+          :provider="llmProvider"
           ref="workflowDesignRef"
         />
       </div>
@@ -73,7 +73,8 @@ import { CommonStatusEnum } from '@/utils/constants'
 import * as WorkflowApi from '@/api/ai/workflow'
 import BasicInfo from './BasicInfo.vue'
 import WorkflowDesign from './WorkflowDesign.vue'
-import { ApiKeyApi } from '@/api/ai/model/apiKey'
+import { ModelApi } from '@/api/ai/model/model'
+import { AiModelTypeEnum } from '@/views/ai/utils/constants'
 
 const router = useRouter()
 const { delView } = useTagsViewStore()
@@ -104,31 +105,35 @@ const formData: any = ref({
   graph: '',
   status: CommonStatusEnum.ENABLE
 })
-// TODO @lesan:待接入
-const provider = ref<any>()
+const llmProvider = ref<any>([])
 const workflowData = ref<any>({})
 provide('workflowData', workflowData)
 
 /** 初始化数据 */
 const actionType = route.params.type as string
 const initData = async () => {
+  // 编辑情况下,需要加载工作流配置
   if (actionType === 'update') {
     const workflowId = route.params.id as string
     formData.value = await WorkflowApi.getWorkflow(workflowId)
     workflowData.value = JSON.parse(formData.value.graph)
   }
 
-  const apiKeys = await ApiKeyApi.getApiKeySimpleList()
-  provider.value = {
+  // 加载模型列表
+  const models = await ModelApi.getModelSimpleList(AiModelTypeEnum.CHAT)
+  llmProvider.value = {
     llm: () =>
-      apiKeys.map(({ id, name }) => ({
+      models.map(({ id, name }) => ({
         value: id,
         label: name
       })),
     knowledge: () => [],
     internal: () => []
   }
+  // TODO @lesan:知识库(可以看下 knowledge)
+  // TODO @lesan:搜索引擎(这个之前有个 pr 搞了,,,可能来接下)
 
+  // 设置当前步骤
   currentStep.value = 0
 }
 
@@ -164,17 +169,17 @@ const handleSave = async () => {
 
     // 更新表单数据
     const data = {
-      ...formData.value
+      ...formData.value,
+      graph: JSON.stringify(workflowData.value)
     }
-
-    data.graph = JSON.stringify(workflowData.value)
-
     if (actionType === 'update') {
       await WorkflowApi.updateWorkflow(data)
     } else {
       await WorkflowApi.createWorkflow(data)
     }
 
+    // 保存成功,提示并跳转到列表页
+    message.success('保存成功')
     delView(unref(router.currentRoute))
     await router.push({ name: 'AiWorkflow' })
   } catch (error: any) {

+ 4 - 2
src/views/ai/write/index/index.vue

@@ -57,9 +57,11 @@ const submit = (data: WriteVO) => {
     },
     ctrl: abortController.value,
     onClose: stopStream,
-    onError: (...err) => {
-      console.error('写作异常', ...err)
+    onError: (error) => {
+      console.error('写作异常', error)
       stopStream()
+      // 需要抛出异常,禁止重试
+      throw error
     }
   })
 }

+ 2 - 2
src/views/mp/material/components/upload.ts

@@ -1,8 +1,8 @@
 import type { UploadProps, UploadRawFile } from 'element-plus'
-import { getAccessToken } from '@/utils/auth'
+import { getRefreshToken } from '@/utils/auth'
 import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
 
-const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 请求头
+const HEADERS = { Authorization: 'Bearer ' + getRefreshToken() } // 请求头(解决 el-upload 上传过程中,无法刷新令牌的问题)
 const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-permanent' // 上传地址
 
 interface UploadData {

+ 64 - 4
src/views/pay/app/components/channel/WeixinChannelForm.vue

@@ -71,11 +71,12 @@
           >
             <el-input
               v-model="formData.config.keyContent"
-              :autosize="{ minRows: 8, maxRows: 8 }"
+              :autosize="{ minRows: 2, maxRows: 4 }"
               :style="{ width: '100%' }"
               placeholder="请上传 apiclient_cert.p12 证书"
               readonly
               type="textarea"
+              :rows="2"
             />
           </el-form-item>
           <el-form-item label="" label-width="180px">
@@ -108,11 +109,12 @@
           >
             <el-input
               v-model="formData.config.privateKeyContent"
-              :autosize="{ minRows: 8, maxRows: 8 }"
+              :autosize="{ minRows: 2, maxRows: 4 }"
               :style="{ width: '100%' }"
               placeholder="请上传 apiclient_key.pem 证书"
               readonly
               type="textarea"
+              :rows="2"
             />
           </el-form-item>
           <el-form-item label="" label-width="180px" prop="privateKeyContentFile">
@@ -145,6 +147,47 @@
               前往微信商户平台查看证书序列号
             </a>
           </el-form-item>
+          <el-form-item label="public_key.pem 证书" label-width="180px" prop="config.publicKeyContent">
+            <el-input
+              v-model="formData.config.publicKeyContent"
+              :autosize="{ minRows: 2, maxRows: 4 }"
+              :style="{ width: '100%' }"
+              placeholder="请上传 public_key.pem 证书"
+              readonly
+              type="textarea"
+              :rows="2"
+            />
+          </el-form-item>
+          <el-form-item label="" label-width="180px" prop="publicKeyContentFile">
+            <el-upload
+              ref="publicKeyContentFile"
+              :before-upload="pemFileBeforeUpload"
+              :http-request="publicKeyContentUpload"
+              :limit="1"
+              accept=".pem"
+              action=""
+            >
+              <el-button type="primary">
+                <Icon class="mr-5px" icon="ep:upload" />
+                点击上传
+              </el-button>
+            </el-upload>
+          </el-form-item>
+          <el-form-item label="公钥 ID" label-width="180px" prop="config.publicKeyId">
+            <el-input
+              v-model="formData.config.publicKeyId"
+              clearable
+              placeholder="请输入公钥 ID"
+            />
+          </el-form-item>
+          <el-form-item label-width="180px">
+            <a
+              href="https://pay.weixin.qq.com/doc/v3/merchant/4012153196"
+              target="_blank"
+            >
+              微信支付公钥产品简介及使用说明
+            </a>
+          </el-form-item>
         </div>
         <el-form-item label="备注" label-width="180px" prop="remark">
           <el-input v-model="formData.remark" :style="{ width: '100%' }" />
@@ -184,7 +227,9 @@ const formData = ref<any>({
     keyContent: '',
     privateKeyContent: '',
     certSerialNo: '',
-    apiV3Key: ''
+    apiV3Key: '',
+    publicKeyContent: '',
+    publicKeyId: ''
   }
 })
 const formRules = {
@@ -201,6 +246,8 @@ const formRules = {
     { required: true, message: '请上传 apiclient_key.pem 证书', trigger: 'blur' }
   ],
   'config.certSerialNo': [{ required: true, message: '请输入证书序列号', trigger: 'blur' }],
+  'config.publicKeyContent': [{ required: true, message: '请上传 public_key.pem 证书', trigger: 'blur' }],
+  'config.publicKeyId': [{ required: true, message: '请输入公钥 ID', trigger: 'blur' }],
   'config.apiV3Key': [{ required: true, message: '请上传 api V3 密钥值', trigger: 'blur' }]
 }
 const formRef = ref() // 表单 Ref
@@ -267,7 +314,9 @@ const resetForm = (appId, code) => {
       keyContent: '',
       privateKeyContent: '',
       certSerialNo: '',
-      apiV3Key: ''
+      apiV3Key: '',
+      publicKeyContent: '',
+      publicKeyId: ''
     }
   }
   formRef.value?.resetFields()
@@ -318,4 +367,15 @@ const keyContentUpload = async (event) => {
   }
   readFile.readAsDataURL(event.file) // 读成 base64
 }
+
+/**
+ * 读取 public_key.pem 到 publicKeyContent 字段
+ */
+const publicKeyContentUpload = async (event) => {
+  const readFile = new FileReader()
+  readFile.onload = (e: any) => {
+    formData.value.config.publicKeyContent = e.target.result
+  }
+  readFile.readAsText(event.file)
+}
 </script>

+ 5 - 1
src/views/report/goview/index.vue

@@ -6,7 +6,11 @@
   </ContentWrap>
 </template>
 <script lang="ts" setup>
+import { getAccessToken, getRefreshToken } from '@/utils/auth'
+
 defineOptions({ name: 'GoView' })
 
-const src = ref(import.meta.env.VITE_GOVIEW_URL)
+const src = ref(
+  `${import.meta.env.VITE_GOVIEW_URL}?accessToken=${getAccessToken()}&refreshToken=${getRefreshToken()}`
+)
 </script>

+ 15 - 0
src/views/report/jmreport/bi.vue

@@ -0,0 +1,15 @@
+<template>
+  <doc-alert title="大屏设计器" url="https://doc.iocoder.cn/screen/" />
+
+  <ContentWrap :bodyStyle="{ padding: '0px' }" class="!mb-0">
+    <IFrame :src="src" />
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { getRefreshToken } from '@/utils/auth'
+
+defineOptions({ name: 'JimuBI' })
+
+// 使用 getRefreshToken() 方法,而不使用 getAccessToken() 方法的原因:积木报表无法方便的刷新访问令牌
+const src = ref(import.meta.env.VITE_BASE_URL + '/drag/list?token=' + getRefreshToken())
+</script>

+ 0 - 1
uno.config.ts

@@ -37,7 +37,6 @@ ${selector}:before {
   position: absolute;
   top: 0;
   left: 0;
-  width: 1px;
   height: 100%;
   background-color: var(--el-border-color);
   z-index: 3;