@epxing/business 5.3.0-beta.2 → 5.3.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/API.md +115 -111
  2. package/README.md +44 -44
  3. package/dist/src/base/AiSkillEditor.js +1 -1
  4. package/dist/src/base/AiSkillEditor.vue.js +2 -0
  5. package/dist/src/base/AppConfigFormItem.js +1 -3
  6. package/dist/src/base/AppConfigFormItem.vue.js +210 -85
  7. package/dist/src/base/CommonFieldList.js +1 -1
  8. package/dist/src/base/CommonFieldList.vue.js +2 -1
  9. package/dist/src/base/ConfigModuleIndex.js +38 -1
  10. package/dist/src/base/ManageAppNav.js +1 -1
  11. package/dist/src/base/ManageMenu.js +1 -1
  12. package/dist/src/base/RoleBindTrace.js +253 -0
  13. package/dist/src/base/RoleGroupManage.js +1037 -0
  14. package/dist/src/base/ServiceMenu.js +7 -6
  15. package/dist/src/components/AiList.js +1278 -3
  16. package/dist/src/components/EntityCheckboxFind.vue.js +14 -10
  17. package/dist/src/components/EntityRightFormItem.js +1 -5
  18. package/dist/src/components/EntityRightFormItem.vue.js +3 -1
  19. package/dist/src/components/EntitySystemEr.js +492 -0
  20. package/dist/src/components/HallNav.vue.js +7 -36
  21. package/dist/src/components/IconFontLibrary.js +22 -1
  22. package/dist/src/components/IconFontList.js +21 -1
  23. package/dist/src/components/PrintFormList.js +2 -1
  24. package/dist/src/components/PrintFormList.vue.js +3 -1
  25. package/dist/src/components/WikiWorkbench.js +153 -129
  26. package/dist/src/components/authorization/AuthorizationInterface.js +3 -1
  27. package/dist/src/components/custom-bpmn/PropertiesView.js +1991 -1942
  28. package/dist/src/components/icon-gallery/IconGallery.js +193 -0
  29. package/dist/src/components/icon-gallery/mixin.js +150 -0
  30. package/dist/src/components/wiki-workbench/WikiIntake.js +86 -93
  31. package/dist/src/components/wiki-workbench/WikiIntake.vue.js +153 -73
  32. package/dist/src/index.js +26 -20
  33. package/dist/src/public-components.js +16 -14
  34. package/dist/src/route-components.js +5 -5
  35. package/dist/src/system.component.index.js +1 -1
  36. package/dist/src/system.component.js +8 -3
  37. package/dist/src/system.component.lazy.js +1 -1
  38. package/dist/src/views/Add.js +1 -1
  39. package/dist/src/views/Add.vue.js +8 -3
  40. package/dist/src/views/AiToolManager.js +792 -317
  41. package/dist/src/views/App.js +3 -45
  42. package/dist/src/views/AppRoleSelect.js +3 -230
  43. package/dist/src/views/Error.js +3 -207
  44. package/dist/src/views/ExcelDesign.js +326 -316
  45. package/dist/src/views/JvmMemoryMonitor.js +463 -0
  46. package/dist/src/views/Module.js +3 -170
  47. package/dist/src/views/WikiBinding.js +88 -25
  48. package/dist/style.css +2867 -1487
  49. package/package.json +12 -6
  50. package/src/base/AiSkillEditor.vue +2 -0
  51. package/src/base/AppConfigFormItem.vue +211 -145
  52. package/src/base/CommonFieldList.vue +9 -6
  53. package/src/base/ConfigModuleIndex.vue +37 -1
  54. package/src/base/ManageAppNav.vue +1188 -1188
  55. package/src/base/ManageMenu.vue +933 -933
  56. package/src/base/RoleBindTrace.vue +364 -0
  57. package/src/base/RoleGroupManage.vue +1160 -0
  58. package/src/base/ServiceMenu.vue +6 -5
  59. package/src/components/AiList.vue +439 -113
  60. package/src/components/EntityCheckboxFind.vue +12 -8
  61. package/src/components/EntityRightFormItem.vue +9 -10
  62. package/src/components/EntitySystemEr.vue +517 -0
  63. package/src/components/HallNav.vue +6 -33
  64. package/src/components/IconFontLibrary.vue +27 -60
  65. package/src/components/IconFontList.vue +13 -64
  66. package/src/components/PrintFormList.vue +33 -27
  67. package/src/components/WikiWorkbench.vue +90 -60
  68. package/src/components/authorization/AuthorizationInterface.vue +2 -0
  69. package/src/components/custom-bpmn/CustomPalette.js +1 -1
  70. package/src/components/custom-bpmn/CustomRenderer.js +1 -1
  71. package/src/components/custom-bpmn/PropertiesView.vue +35 -4
  72. package/src/components/custom-bpmn/index.js +1 -1
  73. package/src/components/icon-gallery/IconGallery.vue +130 -0
  74. package/src/components/icon-gallery/mixin.jsx +109 -0
  75. package/src/components/wiki-workbench/WikiIntake.vue +178 -104
  76. package/src/index.js +24 -20
  77. package/src/public-components.js +1 -0
  78. package/src/route-components.js +4 -4
  79. package/src/system.component.index.js +1 -1
  80. package/src/views/Add.vue +8 -5
  81. package/src/views/AiToolManager.vue +326 -4
  82. package/src/views/App.vue +4 -53
  83. package/src/views/AppRoleSelect.vue +4 -334
  84. package/src/views/Error.vue +4 -63
  85. package/src/views/ExcelDesign.vue +24 -7
  86. package/src/views/JvmMemoryMonitor.vue +284 -0
  87. package/src/views/Module.vue +4 -210
  88. package/src/views/WikiBinding.vue +42 -0
  89. package/types/app.d.ts +1 -0
  90. package/types/dept-find.d.ts +1 -1
  91. package/types/index.d.ts +15 -14
  92. package/types/popup.d.ts +2 -2
  93. package/types/route-components.d.ts +1 -1
  94. package/types/user-find.d.ts +1 -1
  95. package/dist/assets/role_select-DTqRlFNz.webp +0 -0
  96. package/dist/src/components/AiList.vue.js +0 -93
  97. package/dist/src/components/IconFontLibrary.vue.js +0 -67
  98. package/dist/src/components/IconFontList.vue.js +0 -79
@@ -183,8 +183,9 @@ export default {
183
183
  this.clickItem = { id: item.id, ...item }
184
184
  const { detailPageFlag, detailPageFlagM } = this.$configs
185
185
  // this.load = false
186
- this.pageFlag = this.mode === 'pc' ? detailPageFlag : detailPageFlagM
187
- this.$emit('createPage', {
186
+ const isMobileMenu = this.mode === 'move' || (this.mode === 'all' && item.TERMINAL === 'move')
187
+ this.pageFlag = isMobileMenu ? detailPageFlagM : detailPageFlag
188
+ this.emitCreatePage({
188
189
  metaId: 'FM_MODULE',
189
190
  flag: this.pageFlag,
190
191
  component: 'entity-add-page',
@@ -207,7 +208,7 @@ export default {
207
208
  return
208
209
  }
209
210
  const { detailPageFlag, detailPageFlagM } = this.$configs
210
- this.$emit('createPage', {
211
+ this.emitCreatePage({
211
212
  metaId: 'FM_MODULE',
212
213
  flag: this.mode === 'pc' ? detailPageFlag : detailPageFlagM,
213
214
  component: 'entity-add-page',
@@ -224,7 +225,7 @@ export default {
224
225
  const { detailPageFlag, detailPageFlagM } = this.configs
225
226
  this.load = false
226
227
  this.pageFlag = this.mode === 'pc' ? detailPageFlag : detailPageFlagM
227
- this.$emit('createPage', {
228
+ this.emitCreatePage({
228
229
  metaId: 'FM_MODULE',
229
230
  flag: this.pageFlag,
230
231
  pageType: 'EDIT',
@@ -256,7 +257,7 @@ export default {
256
257
  const defaultValue = { }
257
258
  defaultValue[field] = id
258
259
  defaultValue[field + '#'] = text
259
- this.$emit('createPage', {
260
+ this.emitCreatePage({
260
261
  metaId: 'FM_MODULE',
261
262
  flag: this.mode === 'pc' ? addPageFlag : addPageFlagM,
262
263
  component: 'entity-add-page',
@@ -1,132 +1,458 @@
1
- <script>
2
- // @jsx-vue3(T30 JSX 批:Vue3 JSX 语法,@vue/babel-plugin-jsx 编译)
1
+ <template>
2
+ <div class="ai-registry" :style="{ height: registryHeight ? registryHeight + 'px' : null }" v-loading="registryLoading">
3
+ <div class="ai-registry__heading">
4
+ <div><h2>模型注册</h2><p>按接入组管理模型,一次配置连接,按需添加多个模型。</p></div>
5
+ <el-button size="small" icon="el-icon-plus" @click="openGroup()">新建接入组</el-button>
6
+ </div>
7
+ <el-alert v-if="registryError" :title="registryError" type="error" :closable="false" show-icon>
8
+ <el-button type="text" @click="reloadData">重新加载</el-button>
9
+ </el-alert>
10
+ <div v-if="registryReady" class="ai-registry__layout">
11
+ <aside class="ai-registry__groups">
12
+ <div class="ai-registry__between"><strong>接入组</strong><span class="ai-registry__muted">{{ realGroups.length }} 个组</span></div>
13
+ <el-input v-model="groupKeyword" size="small" placeholder="搜索接入组" prefix-icon="el-icon-search" clearable />
14
+ <div class="ai-registry__group-list">
15
+ <button v-for="group in visibleGroups" :key="group.ID || 'ungrouped'" type="button" class="ai-registry__group" :class="{ 'is-active': activeGroupId === group.ID }" @click="selectGroup(group.ID)">
16
+ <el-image v-if="group.LOGO" class="ai-registry__group-logo" :src="$util.getFileUrl(group.LOGO)" fit="contain"><template #error><i class="el-icon-folder-opened" /></template></el-image><i v-else :class="group.ID ? 'el-icon-folder-opened' : 'el-icon-folder'" />
17
+ <span><strong>{{ group.NAME }}</strong><small>{{ group.ID ? codeName(providerOptions, group.TYPE) : '保留原有独立连接' }}</small></span>
18
+ <em>{{ group.MODEL_COUNT }}</em>
19
+ </button>
20
+ <p v-if="!visibleGroups.length" class="ai-registry__muted">没有匹配的接入组</p>
21
+ </div>
22
+ <p class="ai-registry__group-tip"><i class="el-icon-info" /> 一个组对应一套接入配置,同一提供商可创建多个接入组。</p>
23
+ </aside>
24
+ <div class="ai-registry__detail">
25
+ <section class="ai-registry__panel" v-if="activeGroup.ID">
26
+ <div class="ai-registry__between">
27
+ <div class="ai-registry__identity"><span class="ai-registry__icon"><el-image v-if="activeGroup.LOGO" :src="$util.getFileUrl(activeGroup.LOGO)" fit="contain"><template #error><i class="el-icon-connection" /></template></el-image><i v-else class="el-icon-connection" /></span><div><h3>{{ activeGroup.NAME }}</h3><p v-show="connectionExpanded">{{ activeGroup.NOTE || '管理组内模型与共享连接' }}</p></div></div>
28
+ <div><el-button size="small" type="text" :icon="connectionExpanded ? 'el-icon-arrow-up' : 'el-icon-arrow-down'" @click="connectionExpanded = !connectionExpanded">{{ connectionExpanded ? '收起' : '展开' }}</el-button><el-button size="small" icon="el-icon-setting" @click="openGroup(activeGroup)">编辑组配置</el-button></div>
29
+ </div>
30
+ <div v-show="connectionExpanded" class="ai-registry__connection">
31
+ <div><small>模型提供商</small><span>{{ codeName(providerOptions, activeGroup.TYPE) }}</span></div>
32
+ <div><small>接口基础地址</small><code>{{ activeGroup.BASE_URL }}</code></div>
33
+ <div><small>接口密钥</small><span>{{ activeGroup.HAS_SECRET ? '•••••••••••• 已配置' : '未配置' }}</span></div>
34
+ </div>
35
+ <div v-show="connectionExpanded" class="ai-registry__notice"><i class="el-icon-link" /> 组内模型共用以上连接配置,修改后对本组全部模型生效。</div>
36
+ </section>
37
+ <el-alert v-else title="未分组模型继续使用各自的原有连接配置。编辑模型并选择接入组后,将改为使用组内共享连接。" type="info" :closable="false" show-icon />
38
+ <section class="ai-registry__panel ai-registry__models">
39
+ <div class="ai-registry__between"><h3>{{ activeGroup.ID ? '组内模型' : '未分组模型' }} <el-tag size="small" type="info">{{ activeGroup.MODEL_COUNT || 0 }} 个</el-tag></h3><el-button type="primary" size="small" icon="el-icon-plus" @click="openModel()">添加模型</el-button></div>
40
+ <div class="ai-registry__filters">
41
+ <el-input v-model="modelKeyword" size="small" placeholder="搜索模型名称或编号" prefix-icon="el-icon-search" clearable @input="scheduleSearch" />
42
+ <el-select v-model="modelCategory" size="small" placeholder="全部分类" clearable @change="getData(1)"><el-option v-for="item in categoryOptions" :key="item.code" :label="item.name" :value="item.code" /></el-select>
43
+ <el-select v-model="modelStatus" size="small" placeholder="全部状态" clearable @change="getData(1)"><el-option label="已启用" value="1" /><el-option label="已停用" value="0" /></el-select>
44
+ </div>
45
+ <el-alert v-if="modelError" :title="modelError" type="error" :closable="false" show-icon />
46
+ <div class="ai-registry__table">
47
+ <el-table v-loading="loading" :data="data" height="100%" empty-text="暂无模型,请添加模型或调整筛选条件">
48
+ <el-table-column label="模型名称 / 编号" min-width="220"><template #default="scope"><div class="ai-registry__name">{{ scope.row.NAME }} <el-tag v-if="scope.row.IS_DEFAULT === '1'" size="small">分类默认</el-tag></div><code class="ai-registry__model-code">{{ scope.row.CODE }}</code></template></el-table-column>
49
+ <el-table-column label="分类" min-width="100"><template #default="scope"><el-tag size="small" type="info" class="ai-registry__category" :class="'ai-registry__category--' + scope.row.MODE_TYPE">{{ codeName(categoryOptions, scope.row.MODE_TYPE) }}</el-tag></template></el-table-column>
50
+ <el-table-column label="能力" min-width="160"><template #default="scope"><el-tag v-if="scope.row.THINK === '1'" size="small" type="info">慢思考</el-tag> <el-tag v-if="scope.row.SEARCH === '1'" size="small" type="info">联网搜索</el-tag> <el-tag v-if="scope.row.MULTIMODAL === '1'" size="small" type="info">多模态</el-tag><span v-if="!hasCapabilities(scope.row)" class="ai-registry__muted">—</span></template></el-table-column>
51
+ <el-table-column label="输入上限 / 向量维度" min-width="170"><template #default="scope">{{ formatLimit(scope.row.MAX_TOKEN) }} <span class="ai-registry__muted">{{ scope.row.MODE_TYPE === 'embedding' ? '维' : 'tokens' }}</span></template></el-table-column>
52
+ <el-table-column label="状态" width="100"><template #default="scope"><el-switch :model-value="scope.row.ENABLE === '1'" :disabled="mutationLoading" @change="toggleModel(scope.row, $event)" /></template></el-table-column>
53
+ <el-table-column label="操作" width="220"><template #default="scope"><el-button size="small" type="text" @click="openModel(scope.row)">编辑</el-button><el-button size="small" type="text" :disabled="scope.row.IS_DEFAULT === '1' || scope.row.ENABLE !== '1' || mutationLoading" @click="setDefault(scope.row)">设为默认</el-button><el-button size="small" type="text" class="ai-registry__delete" :disabled="mutationLoading" @click="deleteModel(scope.row)">删除</el-button></template></el-table-column>
54
+ </el-table>
55
+ </div>
56
+ <div class="ai-registry__footer"><span class="ai-registry__muted">默认模型按分类设置,跨接入组保持唯一。</span><el-pagination layout="total, prev, pager, next" :total="total" :current-page="currentPage" :page-size="20" @current-change="getData" /></div>
57
+ </section>
58
+ </div>
59
+ </div>
60
+ <el-dialog :title="groupForm.ID ? '编辑组配置' : '新建接入组'" v-model="groupDialog" width="640px" append-to-body :close-on-click-modal="false" :before-close="closeGroup">
61
+ <div class="ai-registry__dialog">
62
+ <el-alert :title="groupForm.ID ? '修改连接配置将影响本组全部模型;密钥留空时保留原配置。' : '先配置接入渠道,再向组内添加模型。以下连接信息由组内模型共用。'" type="info" :closable="false" show-icon />
63
+ <el-form ref="groupForm" :model="groupForm" label-position="top" :rules="groupRules" size="small">
64
+ <el-form-item label="组图标(可选)"><div class="ai-registry__logo-editor"><el-image v-if="groupForm.LOGO" class="ai-registry__logo-preview" :src="$util.getFileUrl(groupForm.LOGO)" fit="contain" /><el-upload action="#" accept=".png,.jpg,.jpeg,.gif" :show-file-list="false" :http-request="uploadGroupLogo" :disabled="saving || logoUploading"><el-button size="small" :loading="logoUploading" :disabled="saving">上传图标</el-button></el-upload><el-button v-if="groupForm.LOGO" type="text" :disabled="saving || logoUploading" @click="groupForm.LOGO = ''">移除</el-button></div></el-form-item>
65
+ <div class="ai-registry__form-row"><el-form-item label="组名称" prop="NAME"><el-input v-model.trim="groupForm.NAME" maxlength="100" /></el-form-item><el-form-item label="模型提供商" prop="TYPE"><el-select v-model="groupForm.TYPE"><el-option v-for="item in providerOptions" :key="item.code" :label="item.name" :value="item.code" /></el-select></el-form-item></div>
66
+ <el-form-item label="接口基础地址" prop="BASE_URL"><el-input v-model.trim="groupForm.BASE_URL" placeholder="https://…/v1" maxlength="1024" /></el-form-item>
67
+ <el-form-item :label="groupForm.ID ? '接口密钥(留空保留原值)' : '接口密钥'" prop="API_SECRET" :rules="groupForm.ID ? [] : [{ required: true, message: '请输入接口密钥', trigger: 'blur' }]"><el-input v-model="groupForm.API_SECRET" type="password" autocomplete="new-password" maxlength="1024" /></el-form-item>
68
+ <el-form-item label="应用 ID(可选)"><el-input v-model.trim="groupForm.APP_ID" /></el-form-item><el-form-item label="分组说明(可选)"><el-input v-model.trim="groupForm.NOTE" maxlength="500" /></el-form-item>
69
+ </el-form>
70
+ </div>
71
+ <template #footer><span ><el-button size="small" :disabled="saving || logoUploading" @click="groupDialog = false">取消</el-button><el-button size="small" type="primary" :loading="saving" :disabled="logoUploading" @click="saveGroup">{{ groupForm.ID ? '保存组配置' : '创建并添加模型' }}</el-button></span></template>
72
+ </el-dialog>
73
+ <el-dialog :title="modelForm.ID ? '编辑模型' : '添加模型'" v-model="modelDialog" width="680px" append-to-body :close-on-click-modal="false" :before-close="closeModel">
74
+ <div class="ai-registry__dialog">
75
+ <el-alert :title="modelForm.GROUP_ID ? '自动使用所选接入组的提供商、接口地址和密钥。' : '此模型尚未分组,继续保留原有连接;选择接入组后将使用组配置。'" type="info" :closable="false" show-icon />
76
+ <el-form ref="modelForm" :model="modelForm" :rules="modelRules" label-position="top" size="small">
77
+ <el-form-item label="所属接入组" prop="GROUP_ID"><el-select v-model="modelForm.GROUP_ID"><el-option v-if="modelForm.ID && !originalModelGroup" label="未分组(保留原配置)" value="" /><el-option v-for="group in realGroups" :key="group.ID" :label="group.NAME" :value="group.ID" /></el-select></el-form-item>
78
+ <div class="ai-registry__form-row"><el-form-item label="模型名称" prop="NAME"><el-input v-model.trim="modelForm.NAME" maxlength="100" /></el-form-item><el-form-item label="模型编号" prop="CODE"><el-input v-model.trim="modelForm.CODE" maxlength="100" /></el-form-item></div>
79
+ <div class="ai-registry__form-row"><el-form-item label="模型分类" prop="MODE_TYPE"><el-select v-model="modelForm.MODE_TYPE"><el-option v-for="item in categoryOptions" :key="item.code" :label="item.name" :value="item.code" /></el-select></el-form-item><el-form-item :label="modelForm.MODE_TYPE === 'embedding' ? '向量维度' : '输入上限(tokens)'" prop="MAX_TOKEN"><el-input-number v-model="modelForm.MAX_TOKEN" :min="1" :max="2147483647" :precision="0" /></el-form-item></div>
80
+ <el-form-item label="模型能力"><el-checkbox v-model="modelForm.THINK" true-label="1" false-label="0">慢思考</el-checkbox><el-checkbox v-model="modelForm.SEARCH" true-label="1" false-label="0">联网搜索</el-checkbox><el-checkbox v-model="modelForm.MULTIMODAL" true-label="1" false-label="0">多模态</el-checkbox></el-form-item>
81
+ <el-form-item><el-checkbox v-model="modelForm.ENABLE" true-label="1" false-label="0">启用模型</el-checkbox><el-checkbox v-model="modelForm.IS_DEFAULT" true-label="1" false-label="0">设为该分类默认模型</el-checkbox></el-form-item>
82
+ <div v-if="modelForm.ID && !modelForm.GROUP_ID" class="ai-registry__legacy"><p class="ai-registry__muted">原有独立连接配置</p><el-form-item label="模型提供商"><el-select v-model="modelForm.TYPE"><el-option v-for="item in providerOptions" :key="item.code" :label="item.name" :value="item.code" /></el-select></el-form-item><el-form-item label="接口基础地址"><el-input v-model.trim="modelForm.BASE_URL" /></el-form-item><el-form-item label="接口密钥(留空保留原值)"><el-input v-model="modelForm.API_SECRET" type="password" autocomplete="new-password" /></el-form-item><el-form-item label="应用 ID"><el-input v-model.trim="modelForm.APP_ID" /></el-form-item></div>
83
+ <p class="ai-registry__muted">模型编号需与接入渠道提供的调用标识一致。</p>
84
+ </el-form>
85
+ </div>
86
+ <template #footer><span ><el-button size="small" :disabled="saving" @click="modelDialog = false">取消</el-button><el-button size="small" type="primary" :loading="saving" @click="saveModel">{{ modelForm.ID ? '保存模型' : '添加到本组' }}</el-button></span></template>
87
+ </el-dialog>
88
+ </div>
89
+ </template>
3
90
 
91
+ <script>
4
92
  import EntityList from '@epxing/components/src/entity-list'
93
+
94
+ const modelFields = ['ID', 'NAME', 'CODE', 'GROUP_ID', 'MODE_TYPE', 'MAX_TOKEN', 'ENABLE', 'IS_DEFAULT', 'THINK', 'SEARCH', 'MULTIMODAL']
95
+
5
96
  export default {
6
97
  name: 'epx-ai-list',
98
+ // 保留平台列表的页面上下文、权限入口和刷新事件,重写注册页布局与取数。
7
99
  extends: EntityList,
8
- methods: {
9
- command (key, item) {
10
- if (key === 'edit') {
11
- this.editEntity({}, item)
12
- } else if (key === 'remove') {
13
- this.removeEntity({}, item)
14
- } else if (key === 'default') {
15
- this.setDefault(item)
100
+ data () {
101
+ return {
102
+ data: [],
103
+ loading: false,
104
+ registryHeight: 0,
105
+ connectionExpanded: false,
106
+ currentPage: 1,
107
+ total: 0,
108
+ groups: [],
109
+ activeGroupId: null,
110
+ groupKeyword: '',
111
+ modelKeyword: '',
112
+ modelCategory: '',
113
+ modelStatus: '',
114
+ registryReady: false,
115
+ registryLoading: false,
116
+ registryError: '',
117
+ modelError: '',
118
+ providerOptions: [],
119
+ categoryOptions: [],
120
+ groupDialog: false,
121
+ modelDialog: false,
122
+ groupForm: {},
123
+ modelForm: {},
124
+ originalModelGroup: '',
125
+ saving: false,
126
+ logoUploading: false,
127
+ mutationLoading: false,
128
+ // 序号隔离快速切组、搜索产生的旧响应。
129
+ requestSerial: 0,
130
+ searchTimer: null,
131
+ groupRules: {
132
+ NAME: [{ required: true, message: '请输入组名称', trigger: 'blur' }],
133
+ TYPE: [{ required: true, message: '请选择模型提供商', trigger: 'change' }],
134
+ BASE_URL: [{ required: true, message: '请输入接口基础地址', trigger: 'blur' }]
135
+ },
136
+ modelRules: {
137
+ NAME: [{ required: true, message: '请输入模型名称', trigger: 'blur' }],
138
+ CODE: [{ required: true, message: '请输入模型编号', trigger: 'blur' }],
139
+ MODE_TYPE: [{ required: true, message: '请选择模型分类', trigger: 'change' }],
140
+ MAX_TOKEN: [{ required: true, type: 'number', min: 1, message: '请输入正整数', trigger: 'change' }]
16
141
  }
142
+ }
143
+ },
144
+ computed: {
145
+ realGroups () {
146
+ return this.groups.filter(group => group.ID)
17
147
  },
18
- setDefault (item) {
19
- this.$http.update({
20
- action: 'setDefault',
21
- entityId: this.$metaId,
22
- id: item.ID
23
- })
24
- .then(() => {
25
- this.$message.success('修改成功')
26
- this.reloadData()
27
- })
28
- .catch(err => {
29
- this.$message.error(err.message)
30
- })
148
+ visibleGroups () {
149
+ const keyword = this.groupKeyword.trim().toLowerCase()
150
+ return this.groups.filter(group => group.NAME.toLowerCase().includes(keyword))
151
+ },
152
+ activeGroup () {
153
+ return this.groups.find(group => group.ID === this.activeGroupId) || {}
31
154
  }
32
155
  },
33
- render () {
34
- return (
35
- <div class="container">
36
- <ul class="ai-list">
37
- <li class="ai-item ai-item--add" onClick={() => this.addEntity({})}>
38
- <div class="ai-item__add-btn">
39
- <i class="el-icon-plus"></i>
40
- </div>
41
- <span>添加大模型</span>
42
- </li>
43
- {
44
- this.data.map(item => (
45
- <li class="ai-item" onClick={() => this.viewEntity({}, item)}>
46
- <div class="ai-item__content">
47
- { item.LOGO && <img src={this.$util.getFileUrl(item.LOGO)} /> }
48
- </div>
49
- <div class="ai-item__title">
50
- <span>{item.NAME}{item.IS_DEFAULT === '1' && <epx-tag plain class="ai-item__default">默认</epx-tag>}</span>
51
- <el-dropdown class="ai-item__dropdown" onCommand={command => this.command(command, item)} v-slots={{
52
- dropdown: () => (<el-dropdown-menu>
53
- <el-dropdown-item command="edit"><i class="el-icon-edit-outline"></i>编辑</el-dropdown-item>
54
- {item.IS_DEFAULT !== '1' && <el-dropdown-item command="default"><i class="el-icon-star-off"></i>设为默认</el-dropdown-item>}
55
- <el-dropdown-item command="remove" divided style={{ color: 'red' }}><i class="el-icon-delete"></i>删除</el-dropdown-item>
56
- </el-dropdown-menu>)
57
- }}>
58
- <i class="ai-item__operate el-icon-more" onClick={e => e.stopPropagation()} ></i>
59
-
60
- </el-dropdown>
61
- </div>
62
- </li>
63
- ))
64
- }
65
-
66
- </ul>
67
- </div>
68
- )
156
+ mounted () {
157
+ this.registryPage = this.$el.closest('.epx-base-page')
158
+ if (this.registryPage) {
159
+ this.registryResizeObserver = new ResizeObserver(this.updateRegistryHeight)
160
+ this.registryResizeObserver.observe(this.registryPage)
161
+ this.updateRegistryHeight()
162
+ }
163
+ },
164
+ beforeUnmount () {
165
+ if (this.registryResizeObserver) this.registryResizeObserver.disconnect()
166
+ clearTimeout(this.searchTimer)
167
+ this.requestSerial++
168
+ },
169
+ methods: {
170
+ updateRegistryHeight () {
171
+ const page = this.registryPage
172
+ if (!page || !page.clientHeight) return
173
+ const offset = this.$el.getBoundingClientRect().top - page.getBoundingClientRect().top + page.scrollTop
174
+ const margin = parseFloat(getComputedStyle(this.$el).marginBottom) || 0
175
+ this.registryHeight = Math.max(0, Math.floor(page.clientHeight - offset - margin))
176
+ },
177
+ initComponent () {
178
+ this.reloadData()
179
+ },
180
+ async reloadData () {
181
+ if (this.registryLoading) return
182
+ this.registryLoading = true
183
+ this.registryError = ''
184
+ try {
185
+ // 摘要只含安全连接信息与服务端统计;代码项直接复用平台码表。
186
+ const [groups, providers, categories] = await Promise.all([
187
+ this.$http.$post({ entityId: this.$metaId, action: 'registryGroups' }),
188
+ this.$http.getCodes({ key: 'AI_TYPE' }),
189
+ this.$http.getCodes({ key: 'MODE_FUN' })
190
+ ])
191
+ this.groups = groups
192
+ this.providerOptions = providers
193
+ this.categoryOptions = categories
194
+ if (!groups.some(group => group.ID === this.activeGroupId)) this.activeGroupId = groups.length ? groups[0].ID : ''
195
+ this.registryReady = true
196
+ await this.getData(this.currentPage)
197
+ } catch (err) {
198
+ this.registryError = (err && (err.msg || err.message)) || '加载模型接入组失败'
199
+ this.$message.error(this.registryError)
200
+ } finally {
201
+ this.registryLoading = false
202
+ }
203
+ },
204
+ async getData (page = 1, callback) {
205
+ if (!this.registryReady) return
206
+ const serial = ++this.requestSerial
207
+ this.loading = true
208
+ this.modelError = ''
209
+ this.currentPage = Number(page) || 1
210
+ try {
211
+ // 标准列表接口负责分页与过滤,显式投影避免列表传输密钥。
212
+ const params = {
213
+ entityId: this.$metaId,
214
+ page: this.currentPage,
215
+ limit: 20,
216
+ columnFields: modelFields.map(attriName => ({ attriName })),
217
+ order_field: 'NAME',
218
+ order_mode: 'asc',
219
+ 'scope.MODE_TYPE': this.modelCategory,
220
+ 'scope.ENABLE': this.modelStatus,
221
+ ...(this.activeGroupId ? { 'scope.GROUP_ID': this.activeGroupId } : { 'scope.GROUP_ID#is_null': '1' })
222
+ }
223
+ const filters = (this.scopeParams || []).map(item => ({ ...item }))
224
+ if (this.modelKeyword.trim()) {
225
+ if (filters.length) filters[filters.length - 1].link = 'and'
226
+ filters.push(
227
+ { lb: '(', field: 'NAME', mode: 'like', values: this.modelKeyword.trim(), link: 'or' },
228
+ { field: 'CODE', mode: 'like', values: this.modelKeyword.trim(), rb: ')' }
229
+ )
230
+ }
231
+ if (filters.length) params.scopeParams = JSON.stringify(filters)
232
+ const result = await this.$http.list(params)
233
+ if (serial !== this.requestSerial) return
234
+ this.data = result.entitys || []
235
+ this.total = result.total || 0
236
+ if (callback) callback(this.data)
237
+ } catch (err) {
238
+ if (serial !== this.requestSerial) return
239
+ this.data = []
240
+ this.total = 0
241
+ this.modelError = (err && (err.msg || err.message)) || '加载模型失败'
242
+ this.$message.error(this.modelError)
243
+ } finally {
244
+ if (serial === this.requestSerial) this.loading = false
245
+ }
246
+ },
247
+ selectGroup (id) {
248
+ this.activeGroupId = id
249
+ this.modelKeyword = ''
250
+ this.modelCategory = ''
251
+ this.modelStatus = ''
252
+ clearTimeout(this.searchTimer)
253
+ this.getData(1)
254
+ },
255
+ scheduleSearch () {
256
+ clearTimeout(this.searchTimer)
257
+ this.searchTimer = setTimeout(() => this.getData(1), 250)
258
+ },
259
+ codeName (options, code) {
260
+ const item = options.find(item => item.code === code)
261
+ return item ? item.name : code
262
+ },
263
+ formatLimit (value) {
264
+ return Number(value || 0).toLocaleString()
265
+ },
266
+ hasCapabilities (model) {
267
+ return ['THINK', 'SEARCH', 'MULTIMODAL'].some(key => model[key] === '1')
268
+ },
269
+ openGroup (group) {
270
+ this.groupForm = { NAME: '', TYPE: '', BASE_URL: '', APP_ID: '', NOTE: '', LOGO: '', ...group, API_SECRET: '' }
271
+ this.groupDialog = true
272
+ this.$nextTick(() => this.$refs.groupForm.clearValidate())
273
+ },
274
+ async uploadGroupLogo ({ file }) {
275
+ this.logoUploading = true
276
+ try {
277
+ const meta = await this.$http.getMeta({ metaId: 'FM_AI_MODEL_GROUP' })
278
+ const attribute = meta.attributes.LOGO
279
+ if (!attribute) throw new Error('接入组图标字段尚未配置')
280
+ const result = await this.$http.$post({ action: 'uploadOfEntity', metaId: 'FM_AI_MODEL_GROUP', attributeId: attribute.id, file }, { requestType: 'formData' })
281
+ const id = result.bean || result.id
282
+ if (!id) throw new Error((result.fileCheck && result.fileCheck.checkResult) || '上传图标失败')
283
+ this.groupForm.LOGO = id
284
+ } catch (err) {
285
+ this.$message.error((err && (err.msg || err.message)) || '上传图标失败')
286
+ } finally {
287
+ this.logoUploading = false
288
+ }
289
+ },
290
+ async saveGroup () {
291
+ if (this.saving || this.logoUploading) return
292
+ const valid = await this.$refs.groupForm.validate().catch(() => false)
293
+ if (!valid) return
294
+ this.saving = true
295
+ const create = !this.groupForm.ID
296
+ try {
297
+ const id = await this.$http.$post({ entityId: this.$metaId, action: 'saveModelGroup', bean: { ...this.groupForm } })
298
+ this.groupForm.API_SECRET = ''
299
+ this.groupDialog = false
300
+ this.activeGroupId = id
301
+ this.currentPage = 1
302
+ await this.reloadData()
303
+ this.$message.success('接入组已保存')
304
+ if (create && !this.registryError) this.openModel()
305
+ } catch (err) {
306
+ this.$message.error((err && (err.msg || err.message)) || '保存接入组失败')
307
+ } finally {
308
+ this.saving = false
309
+ }
310
+ },
311
+ async openModel (model) {
312
+ if (!model && !this.activeGroupId) {
313
+ if (!this.realGroups.length) return this.openGroup()
314
+ this.selectGroup(this.realGroups[0].ID)
315
+ }
316
+ try {
317
+ // 标准详情读取保留旧模型的独立连接,密钥始终留空,不将脱敏值回写。
318
+ const detail = model ? await this.$http.get({ entityId: this.$metaId, id: model.ID }) : null
319
+ const value = detail ? detail.data : {}
320
+ this.modelForm = {
321
+ NAME: '',
322
+ CODE: '',
323
+ GROUP_ID: this.activeGroupId,
324
+ MODE_TYPE: 'text',
325
+ MAX_TOKEN: 32768,
326
+ ENABLE: '1',
327
+ IS_DEFAULT: '0',
328
+ THINK: '0',
329
+ SEARCH: '0',
330
+ MULTIMODAL: '0',
331
+ ...value,
332
+ API_SECRET: ''
333
+ }
334
+ this.modelForm.GROUP_ID = this.modelForm.GROUP_ID || ''
335
+ this.modelForm.MAX_TOKEN = Number(this.modelForm.MAX_TOKEN)
336
+ this.originalModelGroup = this.modelForm.GROUP_ID
337
+ this.modelDialog = true
338
+ this.$nextTick(() => this.$refs.modelForm.clearValidate())
339
+ } catch (err) {
340
+ this.$message.error((err && (err.msg || err.message)) || '读取模型配置失败')
341
+ }
342
+ },
343
+ async saveModel () {
344
+ if (this.saving) return
345
+ const valid = await this.$refs.modelForm.validate().catch(() => false)
346
+ if (!valid) return
347
+ this.saving = true
348
+ try {
349
+ await this.$http.$post({ entityId: this.$metaId, action: 'saveRegistryModel', bean: { ...this.modelForm } })
350
+ this.modelForm.API_SECRET = ''
351
+ this.modelDialog = false
352
+ this.activeGroupId = this.modelForm.GROUP_ID || ''
353
+ await this.reloadData()
354
+ this.$message.success('模型已保存')
355
+ } catch (err) {
356
+ this.$message.error((err && (err.msg || err.message)) || '保存模型失败')
357
+ } finally {
358
+ this.saving = false
359
+ }
360
+ },
361
+ async toggleModel (model, enabled) {
362
+ this.mutationLoading = true
363
+ try {
364
+ await this.$http.$post({ entityId: this.$metaId, action: 'setModelEnabled', id: model.ID, enabled: enabled ? '1' : '0' })
365
+ await this.reloadData()
366
+ } catch (err) {
367
+ this.$message.error((err && (err.msg || err.message)) || '修改模型状态失败')
368
+ } finally {
369
+ this.mutationLoading = false
370
+ }
371
+ },
372
+ async setDefault (model) {
373
+ this.mutationLoading = true
374
+ try {
375
+ await this.$http.$post({ entityId: this.$metaId, action: 'setDefault', id: model.ID })
376
+ await this.reloadData()
377
+ this.$message.success('已设置分类默认模型')
378
+ } catch (err) {
379
+ this.$message.error((err && (err.msg || err.message)) || '设置默认模型失败')
380
+ } finally {
381
+ this.mutationLoading = false
382
+ }
383
+ },
384
+ async deleteModel (model) {
385
+ try {
386
+ await this.$confirm('确认删除模型“' + model.NAME + '”?删除后无法恢复。', '删除模型', { type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' })
387
+ this.mutationLoading = true
388
+ await this.$http.removeMulti({ entityId: this.$metaId, ids: model.ID })
389
+ await this.reloadData()
390
+ this.$message.success('模型已删除')
391
+ } catch (err) {
392
+ if (err !== 'cancel' && err !== 'close') this.$message.error((err && (err.msg || err.message)) || '删除模型失败')
393
+ } finally {
394
+ this.mutationLoading = false
395
+ }
396
+ },
397
+ closeGroup (done) {
398
+ if (!this.saving && !this.logoUploading) { this.groupForm.API_SECRET = ''; done() }
399
+ },
400
+ closeModel (done) {
401
+ if (!this.saving) { this.modelForm.API_SECRET = ''; done() }
402
+ }
69
403
  }
70
404
  }
71
405
  </script>
72
406
 
73
407
  <style lang="scss" scoped>
74
- .ai-list {
75
- display: flex;
76
- flex-wrap: wrap;
77
- }
78
- .ai-item {
79
- margin: 10px;
80
- width: calc(16.6% - 20px);
81
- background: var(--epx-bg-card);
82
- border-radius: var(--epx-radius-base);
83
- overflow: hidden;
84
- }
85
- .ai-item__content {
86
- width: 100%;
87
- height: 132px;
88
- background: url(@/assets/images/ai_bg.webp) no-repeat center;
89
- background-size:cover ;
90
- display: flex;
91
- justify-content: center;
92
- align-items: center;
93
- >img {
94
- height: 80px;
95
- }
96
- }
97
- .ai-item__title {
98
- padding: 0 20px;
99
- height: 48px;
100
- display: flex;
101
- align-items: center;
102
- justify-content: space-between;
103
- }
104
- .ai-item__operate {
105
- cursor: pointer;
106
- }
107
- .ai-item--add {
108
- color: #444;
109
- line-height: 20px;
408
+ .ai-registry {
409
+ box-sizing: border-box;
410
+ height: 100%;
411
+ min-height: 0;
110
412
  display: flex;
111
413
  flex-direction: column;
112
- align-items: center;
113
- justify-content: center;
114
- cursor: pointer;
115
- }
116
- .ai-item__add-btn {
117
- width: 100px;
118
- height: 100px;
119
- border: 1px dashed var(--epx-border);
120
- margin-bottom: 10px;
121
- text-align: center;
122
- i {
123
- font-size: 28px;
124
- line-height: 100px;
125
- color: var(--epx-text-secondary);
414
+ overflow: hidden;
415
+ padding: 12px 16px;
416
+ color: var(--color-text-primary, #444);
417
+ h2, h3, p { margin: 0; }
418
+ h2 { font-size: 18px; font-weight: 600; white-space: nowrap; }
419
+ h3 { font-size: 16px; font-weight: 600; }
420
+ &__heading, &__between { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
421
+ &__heading { flex-shrink: 0; margin-bottom: 12px; > div { display: flex; align-items: baseline; gap: 12px; } p { font-size: 12px; color: var(--color-text-secondary, #888); } }
422
+ &__layout { flex: 1; min-height: 0; display: grid; grid-template-columns: 240px minmax(0, 1fr); gap: 12px; overflow: hidden; }
423
+ &__groups, &__panel { border-radius: 4px; background: var(--background-color, #fff); padding: 16px; }
424
+ &__groups { padding: 16px; display: flex; flex-direction: column; gap: 16px; min-height: 0; overflow: hidden; > :not(.ai-registry__group-list) { flex-shrink: 0; } }
425
+ &__group-list { flex: 1; min-height: 0; overflow-y: auto; }
426
+ &__group-logo { width: 32px; height: 32px; flex-shrink: 0; }
427
+ &__logo-editor { display: flex; align-items: center; gap: 12px; }
428
+ &__logo-preview { width: 48px; height: 48px; }
429
+ &__group { display: flex; align-items: center; gap: 12px; padding: 16px 12px; margin-bottom: 8px; width: 100%; border: 1px solid transparent; border-radius: 4px; background: transparent; text-align: left; cursor: pointer; font: inherit; color: inherit; strong { display: block; font-weight: 500; } small { display: block; margin-top: 4px; color: var(--color-text-secondary, #888); } em { margin-left: auto; font-style: normal; } &:hover { background: var(--background-color-base, #f5f7fa); } &.is-active { background: var(--epx-bg-card, #edf5ff); border-color: transparent; color: var(--el-color-primary, #1765c1); box-shadow: inset 2px 0 #0874e7; } }
430
+ &__group-tip { font-size: 12px; line-height: 1.8; color: var(--color-text-secondary, #888); border-top: 1px solid var(--border-color-base, #e4e7ed); padding-top: 16px; }
431
+ &__detail { display: flex; flex-direction: column; gap: 12px; min-width: 0; min-height: 0; overflow: hidden; > :not(.ai-registry__models) { flex-shrink: 0; } }
432
+ &__models { flex: 1; min-height: 0; display: flex; flex-direction: column; > :not(.ai-registry__table) { flex-shrink: 0; } }
433
+ &__table { flex: 1; min-height: 0; overflow: hidden; &:deep() { .el-table td, .el-table th { padding: 4px 0; } .el-table .cell { line-height: 18px; } } }
434
+ &__identity { display: flex; gap: 12px; align-items: center; p { margin-top: 8px; color: var(--color-text-secondary, #888); } }
435
+ &__icon { display: grid; place-items: center; height: 36px; width: 36px; flex-shrink: 0; background: var(--background-color-base, #f2f3f7); font-size: 22px; border-radius: 4px; .el-image { width: 32px; height: 32px; } }
436
+ &__connection { display: grid; grid-template-columns: 1fr 2fr 1fr; gap: 24px; margin-top: 20px; padding: 20px 0; border-top: 1px solid var(--border-color-base, #e4e7ed); small { display: block; margin-bottom: 8px; color: var(--color-text-secondary, #888); } code { font-size: 13px; overflow-wrap: anywhere; } }
437
+ &__notice { background: var(--background-color-base, #f5f7fa); padding: 12px; color: var(--color-text-secondary, #888); font-size: 13px; }
438
+ &__filters { display: flex; gap: 12px; margin: 12px 0; > .el-input { max-width: 280px; } > .el-select { max-width: 150px; } }
439
+ &__name { font-weight: 600; line-height: 20px; }
440
+ &__model-code { display: block; line-height: 14px; font-size: 11px; color: var(--color-text-secondary, #888); }
441
+ &__category {
442
+ font-weight: 600;
443
+ &--text { color: var(--el-color-primary, #1765c1); background: var(--epx-bg, #eaf3ff); border-color: var(--epx-border, #cbdfff); }
444
+ &--multimodal { color: var(--epx-text, #7543bd); background: var(--epx-bg, #f3ecff); border-color: var(--epx-border, #e1d0fa); }
445
+ &--embedding { color: var(--epx-text, #21844a); background: var(--epx-bg-card, #eaf8ef); border-color: var(--epx-border, #c6e9d2); }
446
+ &--rerank { color: var(--epx-text-secondary, #ad6816); background: var(--epx-bg-card, #fff4e3); border-color: var(--epx-border, #f5d8a8); }
447
+ &--speech2text { color: var(--epx-text, #087e8b); background: var(--epx-bg, #e5f7fa); border-color: var(--epx-border, #b9e5ec); }
448
+ &--tts { color: var(--epx-text-secondary, #b63d78); background: var(--epx-bg-card, #ffedf5); border-color: var(--epx-border, #f5c9dd); }
126
449
  }
450
+ &__delete { color: var(--epx-text-secondary, #d64646); &:hover, &:focus { color: var(--epx-text, #b63131); } }
451
+ &__muted { color: var(--color-text-secondary, #888); font-size: 13px; }
452
+ &__footer { display: flex; gap: 16px; justify-content: space-between; align-items: center; padding-top: 8px; }
453
+ &__dialog { .el-form { margin-top: 20px; } .el-select, .el-input-number { width: 100%; } }
454
+ &__form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
455
+ &__legacy { border-top: 1px solid var(--border-color-base, #e4e7ed); padding-top: 16px; }
127
456
  }
128
- .ai-item__default {
129
- margin-left: 4px;
130
- }
131
-
457
+ @media (max-width: 1200px) { .ai-registry { &__layout { grid-template-columns: 210px minmax(0, 1fr); } &__panel { padding: 16px; } &__connection { grid-template-columns: 1fr; gap: 12px; } &__footer { align-items: flex-start; flex-direction: column; } } }
132
458
  </style>