AddModelDialog.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. <template>
  2. <el-dialog :visible="dialogVisible" @update:visible="handleVisibleChange" width="57%" center
  3. custom-class="custom-dialog" :show-close="false" class="center-dialog">
  4. <div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
  5. <div style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
  6. {{ $t('modelConfigDialog.addModel') }}
  7. </div>
  8. <button class="custom-close-btn" @click="handleClose">
  9. ×
  10. </button>
  11. <!-- 模型信息部分 -->
  12. <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
  13. <div style="font-size: 20px; font-weight: bold; color: #3d4566;">{{ $t('modelConfigDialog.modelInfo') }}</div>
  14. <div style="display: flex; align-items: center; gap: 20px;">
  15. <div style="display: flex; align-items: center;">
  16. <span style="margin-right: 8px;">{{ $t('modelConfigDialog.enable') }}</span>
  17. <el-switch v-model="formData.isEnabled" class="custom-switch"></el-switch>
  18. </div>
  19. <div style="display: none; align-items: center;">
  20. <span style="margin-right: 8px;">{{ $t('modelConfigDialog.setDefault') }}</span>
  21. <el-switch v-model="formData.isDefault" class="custom-switch"></el-switch>
  22. </div>
  23. </div>
  24. </div>
  25. <div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
  26. <el-form :model="formData" label-width="100px" label-position="left" class="custom-form">
  27. <div style="display: flex; gap: 20px; margin-bottom: 0;">
  28. <el-form-item :label="$t('modelConfigDialog.modelId')" prop="id" style="flex: 1;">
  29. <el-input v-model="formData.id" :placeholder="$t('modelConfigDialog.enterModelId')" class="custom-input-bg"
  30. maxlength="32"></el-input>
  31. </el-form-item>
  32. </div>
  33. <div style="display: flex; gap: 20px; margin-bottom: 0;">
  34. <el-form-item :label="$t('modelConfigDialog.modelName')" prop="modelName" style="flex: 1;">
  35. <el-input v-model="formData.modelName" :placeholder="$t('modelConfigDialog.enterModelName')"
  36. class="custom-input-bg"></el-input>
  37. </el-form-item>
  38. <el-form-item :label="$t('modelConfigDialog.modelCode')" prop="modelCode" style="flex: 1;">
  39. <el-input v-model="formData.modelCode" :placeholder="$t('modelConfigDialog.enterModelCode')"
  40. class="custom-input-bg"></el-input>
  41. </el-form-item>
  42. </div>
  43. <div style="display: flex; gap: 20px; margin-bottom: 0;">
  44. <el-form-item :label="$t('modelConfigDialog.supplier')" prop="supplier" style="flex: 1;">
  45. <el-select v-model="formData.supplier" :placeholder="$t('modelConfigDialog.selectSupplier')"
  46. class="custom-select custom-input-bg" style="width: 100%;" @focus="loadProviders" filterable>
  47. <el-option v-for="item in providers" :key="item.value" :label="item.label" :value="item.value" />
  48. </el-select>
  49. </el-form-item>
  50. <el-form-item :label="$t('modelConfigDialog.sortOrder')" prop="sortOrder" style="flex: 1;">
  51. <el-input v-model="formData.sort" type="number" :placeholder="$t('modelConfigDialog.enterSortOrder')"
  52. class="custom-input-bg"></el-input>
  53. </el-form-item>
  54. </div>
  55. <el-form-item :label="$t('modelConfigDialog.docLink')" prop="docLink" style="margin-bottom: 27px;">
  56. <el-input v-model="formData.docLink" :placeholder="$t('modelConfigDialog.enterDocLink')"
  57. class="custom-input-bg"></el-input>
  58. </el-form-item>
  59. <el-form-item :label="$t('modelConfigDialog.remark')" prop="remark" class="prop-remark">
  60. <el-input v-model="formData.remark" type="textarea" :rows="3"
  61. :placeholder="$t('modelConfigDialog.enterRemark')" :autosize="{ minRows: 3, maxRows: 5 }"
  62. class="custom-input-bg"></el-input>
  63. </el-form-item>
  64. </el-form>
  65. <div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">{{
  66. $t('modelConfigDialog.callInfo') }}</div>
  67. <div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
  68. <el-form :model="formData.configJson" label-width="auto" label-position="left" class="custom-form">
  69. <div v-for="(row, rowIndex) in chunkedCallInfoFields" :key="rowIndex"
  70. style="display: flex; gap: 20px; margin-bottom: 0;">
  71. <el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop" style="flex: 1;">
  72. <el-input v-model="formData.configJson[field.prop]" :placeholder="field.placeholder"
  73. :type="field.type || 'text'" class="custom-input-bg" :show-password="field.type === 'password'">
  74. </el-input>
  75. </el-form-item>
  76. </div>
  77. </el-form>
  78. </div>
  79. <div style="display: flex;justify-content: center;">
  80. <el-button type="primary" @click="confirm" class="save-btn" :loading="saving" :disabled="saving">
  81. {{ $t('modelConfigDialog.save') }}
  82. </el-button>
  83. </div>
  84. </el-dialog>
  85. </template>
  86. <script>
  87. import Api from '@/apis/api';
  88. export default {
  89. name: 'AddModelDialog',
  90. props: {
  91. visible: { type: Boolean, required: true },
  92. modelType: { type: String, required: true }
  93. },
  94. data() {
  95. return {
  96. saving: false,
  97. providers: [],
  98. dialogVisible: false,
  99. providersLoaded: false,
  100. providerFields: [],
  101. currentProvider: null,
  102. formData: {
  103. id: '',
  104. modelName: '',
  105. modelCode: '',
  106. supplier: '',
  107. sort: 1,
  108. docLink: '',
  109. remark: '',
  110. isEnabled: true,
  111. isDefault: true,
  112. configJson: {}
  113. }
  114. }
  115. },
  116. watch: {
  117. visible(val) {
  118. this.dialogVisible = val;
  119. if (val) {
  120. this.initConfigJson();
  121. } else {
  122. this.resetForm();
  123. }
  124. },
  125. 'formData.supplier'(newVal) {
  126. this.currentProvider = this.providers.find(p => p.value === newVal);
  127. this.providerFields = this.currentProvider?.fields || [];
  128. this.initDynamicConfig();
  129. }
  130. },
  131. computed: {
  132. dynamicCallInfoFields() {
  133. return this.providerFields;
  134. },
  135. chunkedCallInfoFields() {
  136. const chunkSize = 2;
  137. const result = [];
  138. for (let i = 0; i < this.dynamicCallInfoFields.length; i += chunkSize) {
  139. result.push(this.dynamicCallInfoFields.slice(i, i + chunkSize));
  140. }
  141. return result;
  142. }
  143. },
  144. methods: {
  145. loadProviders() {
  146. if (this.providersLoaded)
  147. return
  148. Api.model.getModelProviders(this.modelType, (data) => {
  149. this.providers = data.map(item => ({
  150. label: item.name,
  151. value: item.providerCode,
  152. fields: JSON.parse(item.fields || '[]').map(f => ({
  153. label: f.label,
  154. prop: f.key,
  155. type: f.type === 'password' ? 'password' : 'text',
  156. placeholder: `请输入${f.key}`
  157. }))
  158. }))
  159. this.providersLoaded = true
  160. })
  161. },
  162. initConfigJson() {
  163. const defaultConfig = {};
  164. this.providerFields.forEach(field => {
  165. defaultConfig[field.prop] = '';
  166. });
  167. this.formData.configJson = { ...defaultConfig };
  168. },
  169. handleVisibleChange(val) {
  170. this.dialogVisible = val;
  171. this.$emit('update:visible', val);
  172. if (!val) {
  173. this.resetForm();
  174. }
  175. },
  176. handleClose() {
  177. this.saving = false;
  178. this.$emit('update:visible', false);
  179. },
  180. initDynamicConfig() {
  181. const newConfig = {};
  182. this.providerFields.forEach(field => {
  183. newConfig[field.prop] = this.formData.configJson[field.prop] || '';
  184. });
  185. this.formData.configJson = newConfig;
  186. },
  187. confirm() {
  188. this.saving = true;
  189. // 校验模型ID不能为纯文字或空格
  190. if (this.formData.id && !this.validateModelId(this.formData.id)) {
  191. this.$message.error(this.$t('modelConfigDialog.invalidModelId'));
  192. this.saving = false;
  193. return;
  194. }
  195. if (!this.formData.supplier) {
  196. this.$message.error(this.$t('addModelDialog.requiredSupplier'));
  197. this.saving = false;
  198. return;
  199. }
  200. const submitData = {
  201. id: this.formData.id || '',
  202. modelName: this.formData.modelName || '',
  203. modelCode: this.formData.modelCode || '',
  204. supplier: this.formData.supplier,
  205. sort: this.formData.sort || 1,
  206. docLink: this.formData.docLink || '',
  207. remark: this.formData.remark || '',
  208. isEnabled: this.formData.isEnabled ? 1 : 0,
  209. isDefault: this.formData.isDefault ? 1 : 0,
  210. provideCode: this.formData.supplier,
  211. configJson: {
  212. ...this.formData.configJson,
  213. type: this.formData.supplier
  214. }
  215. };
  216. try {
  217. this.$emit('confirm', submitData);
  218. this.$emit('update:visible', false);
  219. this.resetForm();
  220. } catch (e) {
  221. console.error(e);
  222. } finally {
  223. this.saving = false;
  224. }
  225. },
  226. resetForm() {
  227. this.saving = false;
  228. this.formData = {
  229. id: '',
  230. modelName: '',
  231. modelCode: '',
  232. supplier: '',
  233. sort: 1,
  234. docLink: '',
  235. remark: '',
  236. isEnabled: true,
  237. isDefault: true,
  238. configJson: {}
  239. };
  240. // 重置加载状态
  241. this.providers = [];
  242. this.providersLoaded = false;
  243. // 重置字段配置
  244. this.providerFields = [];
  245. this.currentProvider = null;
  246. },
  247. // 校验模型ID:不能为纯文字或空格
  248. validateModelId(modelId) {
  249. if (!modelId || typeof modelId !== 'string') {
  250. return false;
  251. }
  252. // 去除首尾空格
  253. const trimmedId = modelId.trim();
  254. // 检查是否为空或纯空格
  255. if (trimmedId === '') {
  256. return false;
  257. }
  258. // 检查是否只包含字母(纯文字)
  259. if (/^[a-zA-Z]+$/.test(trimmedId)) {
  260. return false;
  261. }
  262. // 检查是否包含空格
  263. if (/\s/.test(trimmedId)) {
  264. return false;
  265. }
  266. // 允许字母、数字、下划线、连字符
  267. if (!/^[a-zA-Z0-9_-]+$/.test(trimmedId)) {
  268. return false;
  269. }
  270. return true;
  271. }
  272. }
  273. }
  274. </script>
  275. <style>
  276. .custom-dialog {
  277. position: relative;
  278. border-radius: 20px;
  279. overflow: hidden;
  280. background: white;
  281. padding-bottom: 17px;
  282. }
  283. .custom-dialog .el-dialog__header {
  284. padding: 0;
  285. border-bottom: none;
  286. }
  287. .center-dialog {
  288. display: flex;
  289. align-items: center;
  290. justify-content: center;
  291. }
  292. .center-dialog .el-dialog {
  293. margin: 0 0 auto !important;
  294. display: flex;
  295. flex-direction: column;
  296. }
  297. .custom-close-btn {
  298. position: absolute;
  299. top: 20px;
  300. right: 20px;
  301. width: 35px;
  302. height: 35px;
  303. border-radius: 50%;
  304. border: 2px solid #cfcfcf;
  305. background: none;
  306. font-size: 30px;
  307. font-weight: lighter;
  308. color: #cfcfcf;
  309. cursor: pointer;
  310. display: flex;
  311. align-items: center;
  312. justify-content: center;
  313. z-index: 1;
  314. padding: 0;
  315. outline: none;
  316. }
  317. .custom-close-btn:hover {
  318. color: #409EFF;
  319. border-color: #409EFF;
  320. }
  321. .custom-select .el-input__suffix {
  322. background: #e6e8ea;
  323. right: 6px;
  324. width: 20px;
  325. height: 20px;
  326. display: flex;
  327. justify-content: center;
  328. align-items: center;
  329. top: 9px;
  330. }
  331. .custom-select .el-input__suffix-inner {
  332. display: flex;
  333. align-items: center;
  334. justify-content: center;
  335. width: 100%;
  336. }
  337. .custom-select .el-icon-arrow-up:before {
  338. content: "";
  339. display: inline-block;
  340. width: 0;
  341. height: 0;
  342. border-left: 5px solid transparent;
  343. border-right: 5px solid transparent;
  344. border-top: 7px solid #c0c4cc;
  345. position: relative;
  346. top: -2px;
  347. transform: rotate(180deg);
  348. }
  349. .custom-form .el-form-item {
  350. margin-bottom: 20px;
  351. }
  352. .custom-form .el-form-item__label {
  353. color: #3d4566;
  354. font-weight: normal;
  355. text-align: right;
  356. padding-right: 20px;
  357. }
  358. .custom-form .el-form-item.prop-remark .el-form-item__label {
  359. margin-top: -4px;
  360. }
  361. .custom-input-bg .el-input__inner::-webkit-input-placeholder,
  362. .custom-input-bg .el-textarea__inner::-webkit-input-placeholder {
  363. color: #9c9f9e;
  364. }
  365. .custom-input-bg .el-input__inner,
  366. .custom-input-bg .el-textarea__inner {
  367. background-color: #ffffff;
  368. }
  369. .save-btn {
  370. background: #e6f0fd;
  371. color: #237ff4;
  372. border: 1px solid #b3d1ff;
  373. width: 150px;
  374. height: 40px;
  375. font-size: 16px;
  376. transition: all 0.3s ease;
  377. }
  378. .save-btn:hover {
  379. background: linear-gradient(to right, #237ff4, #9c40d5);
  380. color: white;
  381. border: none;
  382. }
  383. .custom-switch .el-switch__core {
  384. border-radius: 20px;
  385. height: 23px;
  386. background-color: #c0ccda;
  387. width: 35px;
  388. padding: 0 20px;
  389. }
  390. .custom-switch .el-switch__core:after {
  391. width: 15px;
  392. height: 15px;
  393. background-color: white;
  394. top: 3px;
  395. left: 4px;
  396. transition: all .3s;
  397. }
  398. .custom-switch.is-checked .el-switch__core {
  399. border-color: #b5bcf0;
  400. background-color: #cfd7fa;
  401. padding: 0 20px;
  402. }
  403. .custom-switch.is-checked .el-switch__core:after {
  404. left: 100%;
  405. margin-left: -18px;
  406. background-color: #1b47ee;
  407. }
  408. [style*="display: flex"] {
  409. gap: 20px;
  410. }
  411. .custom-input-bg .el-input__inner {
  412. height: 32px;
  413. }
  414. </style>