diff --git a/db/zentao.sql b/db/zentao.sql index 2ed9fc3a14..f33cd11a6e 100644 --- a/db/zentao.sql +++ b/db/zentao.sql @@ -15941,8 +15941,9 @@ CREATE TABLE IF NOT EXISTS `zt_miniprogram` ( `createdDate` datetime NOT NULL, `editedBy` varchar(30) NOT NULL, `editedDate` datetime NOT NULL, - `tmp` enum('0','1') NOT NULL DEFAULT '1', + `publish` enum('0','1') NOT NULL DEFAULT '0', `deleted` enum('0','1') NOT NULL DEFAULT '0', + `prompt` text NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; @@ -15950,9 +15951,9 @@ CREATE TABLE IF NOT EXISTS `zt_miniprogramfields` ( `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, `appid` mediumint(8) unsigned NOT NULL, `name` varchar(30) NOT NULL, - `type` enum('radio', 'checkbox', 'input', 'template') DEFAULT 'input', - `value` text NOT NULL, - `optional` varchar(30) DEFAULT NULL, + `type` enum('radio', 'checkbox', 'text', 'textarea') DEFAULT 'text', + `placeholder` text DEFAULT NULL, + `options` text DEFAULT NULL, `required` enum('0', '1') DEFAULT '1', PRIMARY KEY (`id`), FOREIGN KEY (`appid`) REFERENCES `zt_miniprogram`(`id`) diff --git a/module/ai/control.php b/module/ai/control.php index 3e7b7b7224..009480c089 100644 --- a/module/ai/control.php +++ b/module/ai/control.php @@ -155,13 +155,17 @@ class ai extends control { if(!empty($_POST)) { - $requiredFields = array('category' => $this->lang->ai->miniPrograms->category, 'model' => $this->lang->prompt->model, 'name' => $this->lang->prompt->name); + $toNext = $_POST['toNext']; + unset($_POST['toNext']); + $requiredFields = array('category' => $this->lang->ai->miniPrograms->category, 'model' => $this->lang->prompt->model, 'name' => $this->lang->prompt->name, 'desc' => $this->lang->ai->miniPrograms->desc); $errors = $this->ai->verifyRequiredFields($requiredFields); if($errors !== false) return $this->sendError($errors); $isDuplicated = $this->ai->checkDuplicatedAppName($_POST['name']); if($isDuplicated) return $this->sendError(array('name' => $this->lang->ai->miniPrograms->field->duplicatedNameTip)); $id = $this->ai->createTmpMiniProgram(); - if($id !== false) return $this->sendSuccess(array('message' => $this->lang->saveSuccess, 'locate' => $this->createLink('ai', 'configuredMiniProgram', "appID=$id"))); + if($id === false) return $this->sendError(array('message' => $this->lang->fail)); + if($toNext === '1') return $this->sendSuccess(array('message' => $this->lang->saveSuccess, 'locate' => $this->createLink('ai', 'configuredMiniProgram', "appID=$id"))); + return $this->sendSuccess(array('message' => $this->lang->saveSuccess)); } $this->view->iconName = 'writinghand'; $this->view->iconTheme = 7; @@ -179,7 +183,12 @@ class ai extends control public function configuredMiniProgram($appID) { if(!is_numeric($appID)) return $this->locate('ai', 'createMiniProgram'); + if(!empty($_POST)) + { + $this->ai->saveMiniProgramFields($appID); + } $this->view->title = $this->lang->ai->miniPrograms->common; + $this->view->appid = $appID; $this->display(); } diff --git a/module/ai/css/configuredminiprogram.css b/module/ai/css/configuredminiprogram.css index df8d123a55..6de1643464 100644 --- a/module/ai/css/configuredminiprogram.css +++ b/module/ai/css/configuredminiprogram.css @@ -2,8 +2,31 @@ .field-configuration .drag-area > div {display: inline-flex; align-items: center; gap: 4px;} .options-container {display: flex; flex-direction: column; gap: 16px;} -#mainContent {position: fixed; left: 20px; right: 20px; top: 70px; bottom: 0; display: flex; background: #f0f2f5; padding: 0;} +#mainContent {position: fixed; left: 20px; right: 20px; top: 70px; bottom: 56px; display: flex; background: #f0f2f5; padding: 0;} #mainContent > div {height: 100%; background: #fff; display: flex; flex-direction: column;} #mainContent > div > header {background: #f8f8fa; height: 40px; display: flex; align-items: center; padding-left: 24px;} #mainContent > div > header > strong {font-size: 16px;} #mainContent > div > main {flex-grow: 1; overflow-y: auto;} + +.modal-body {padding-top: 0; padding-bottom: 0;} + +.area-title {display: flex; align-items: center; padding-bottom: 20px; overflow: hidden;} +.area-title > strong {font-size: 14px; text-wrap: nowrap;} +.area-title > span {font-size: 12px;} +.area-title > i {margin-left: 16px; margin-right: 4px;} + +.content-debug-area, +.prompt-design-area, +.prompt-preview-area, +.prompt-result-area, +.field-configuration-main {padding: 24px;} + +.prompt-preview-area .preview-container, +.prompt-result-area .preview-container {border: 1px solid #D8DBDE; background: #f8f8f8; position: absolute; left: 24px; right: 24px; bottom: 24px; padding: 8px;} +.prompt-preview-area .preview-container {top: 64px;} +.prompt-result-area .preview-container {top: 34px;} + +.table-form > tbody > tr > th {width: 80px; padding-left: 0; text-wrap: nowrap; overflow: hidden;} + +.text-muted {flex-shrink: 1;} + diff --git a/module/ai/js/configuredminiprogram.js b/module/ai/js/configuredminiprogram.js index 31282c3d36..0dbfb6ab4b 100644 --- a/module/ai/js/configuredminiprogram.js +++ b/module/ai/js/configuredminiprogram.js @@ -1,4 +1,9 @@ let $editingField = null; +let fieldIndex = 0; +/** + * @type {Map} + */ +const words = new Map(); /** * Handle the field type change events. @@ -58,9 +63,9 @@ function handleRemoveOptionClick(event) */ function handleAddOptionClick(event) { - const $newField = $(event.target).closest('.input-group').clone(); - $newField.find('input').prop('value', ''); - $(event.target).closest('.options-container').append($newField); + const template = document.getElementById('option-template'); + const $clone = $(document.importNode(template.content, true)); + $(event.target).closest('.options-container').append($clone); fixOptionsOrder(); } @@ -78,6 +83,7 @@ function revertOptions() const $optionStr = $optionsContainer.html(); $optionsContainer.append($optionStr); $optionsContainer.append($optionStr); + $optionsContainer.find('.input-group').first().find('button').last().attr('disabled', 'disabled'); $('.field-options') .children('td') @@ -199,11 +205,65 @@ function handleEditFieldClick(event) function handleRemoveFieldClick(event) { const result = window.confirm(deleteTip); - if(result) $(event.target).closest('tr').remove(); - const $button = $('.field-configuration-main > div'); - const $table = $('.field-configuration-main > table'); - if($button.hasClass('hidden')) $button.removeClass('hidden'); - if(!$table.hasClass('hidden')) $table.addClass('hidden'); + const $tr = $(event.target).closest('tr'); + const fieldID = $tr.attr('data-id'); + if(result) $tr.remove(); + $('.field-content').find(`[data-id="${fieldID}"]`).remove(); + + if($('.field-configuration').children().length === 0) $('.field-configuration').parent().addClass('hidden').prev().removeClass('hidden'); + words.delete($tr.find('th').prop('title')); + const innerHTML = $('#autocomplete-textarea').html(); + $('#autocomplete-textarea').html(innerHTML.replace(//g, '').replace(/<\/strong>/g, '')); + updatePromptPreview(); +} + +/** + * Check for duplicate field names. + * + * @param {string} name + * @returns {boolean} result + */ +const checkDuplicatedFieldName = (name) => Array.from(words.keys()).includes(name); + +/** + * Show field name error if error exists. + * + * @param {*} $required + * @param {HTMLFormElement} formData + * @returns {boolean} result + */ +function showFieldNameErrorIfExist($required, form) +{ + const $form = $(form); + const formData = new FormData(form); + const name = formData.get('field-name'); + $form.find('.text-danger.help-text').remove(); + $form.find('.has-error').removeClass('has-error'); + + if(!name) + { + $required + .addClass('has-error') + .append(`
${emptyWarning}
`); + return true; + } + + if(checkDuplicatedFieldName(name)) + { + $required + .addClass('has-error') + .append(`
${duplicatedWarning}
`); + return true; + } + + if(['radio', 'checkbox'].includes(formData.get('field-type')) && formData.getAll('option[]').filter(x => !!x).length === 0) + { + $form.find('.options-container').children().first().addClass('has-error').after(`
${emptyOptionWarning}
`); + return true; + } + + + return false; } /** @@ -213,25 +273,35 @@ function handleRemoveFieldClick(event) */ function handleSaveFieldClick() { - const formData = new FormData(document.querySelector('#add-field-modal form')); + const form = document.querySelector('#add-field-modal form'); + const formData = new FormData(form); const name = formData.get('field-name'); - if(!name) - { - const template = document.getElementById('text-danger-template'); - const $clone = $(document.importNode(template.content, true)); - $('#add-field-modal input[name="field-name"]') - .closest('.required') - .append($clone); - return; - } + const $required = $('#add-field-modal input[name="field-name"]').closest('.required'); + + if(showFieldNameErrorIfExist($required, form)) return; $('#add-field-modal').modal('hide'); const $fieldView = createFieldView(formData); + $fieldView.attr('data-id', `field-${++fieldIndex}`); $('#sortable-list').append($fieldView); + $fieldView.find('.picker').addClass('disabled'); + const $button = $('.field-configuration-main > div'); const $table = $('.field-configuration-main > table'); if(!$button.hasClass('hidden')) $button.addClass('hidden'); if($table.hasClass('hidden')) $table.removeClass('hidden'); + + const $fieldViewClone = createFieldView(formData); + $fieldViewClone.attr('data-id', $fieldView.attr('data-id')); + $fieldViewClone.children().last().remove(); + $fieldViewClone.find('.drag-area').removeClass('darg-area'); + $fieldViewClone.removeClass('srotable-item'); + $fieldViewClone.find('.icon-move').remove(); + $fieldViewClone.find('[readonly]').removeAttr('readonly'); + $fieldViewClone.find('.field-type').on('change', updatePromptPreview); + $('.field-content').append($fieldViewClone); + + words.set(name, $fieldView.attr('data-id')); } /** @@ -241,21 +311,31 @@ function handleSaveFieldClick() */ function handleSaveEditedFieldClick() { - const formData = new FormData(document.querySelector('#edit-field-modal form')); + const oldName = $editingField.find('.field-name').html(); + const form = document.querySelector('#edit-field-modal form'); + const formData = new FormData(form); const name = formData.get('field-name'); - if(!name) - { - const template = document.getElementById('text-danger-template'); - const $clone = $(document.importNode(template.content, true)); - $('#edit-field-modal input[name="field-name"]') - .closest('.required') - .append($clone); - return; - } + const $required = $('#edit-field-modal input[name="field-name"]').closest('.required'); + + if(showFieldNameErrorIfExist($required, form)) return; $('#edit-field-modal').modal('hide'); const $fieldView = createFieldView(formData); + $fieldView.attr('data-id', $editingField.attr('data-id')); $editingField.replaceWith($fieldView); + $fieldView.find('.picker').addClass('disabled'); + + const $fieldViewClone = createFieldView(formData); + $fieldViewClone.attr('data-id', $fieldView.attr('data-id')); + $fieldViewClone.children().last().remove(); + $fieldViewClone.children().first().removeClass('darg-area'); + $fieldViewClone.find('.icon-move').remove(); + $fieldViewClone.find('[readonly]').removeAttr('readonly'); + $fieldViewClone.find('.field-type').on('change', updatePromptPreview); + $(`.field-content [data-id="${$editingField.attr('data-id')}"]`).replaceWith($fieldViewClone); + + words.delete(oldName); + words.set(name, $fieldView.attr('data-id')); } /** @@ -282,7 +362,7 @@ function createFieldView(formData) function createFieldNameView(formData) { const name = formData.get('field-name'); - return $(` + return $(`
${name} @@ -300,24 +380,24 @@ function createFieldTypeView(formData) { const type = formData.get('field-type'); const required = formData.get('required') === '1'; - const $td = $(''); + const $td = $(``); if(required) $td.addClass('required'); if(type === 'text') { const placeholder = formData.get('placeholder') || pleaseInput; - $td.append(``); + $td.append(``); return $td; } if(type === 'textarea') { const placeholder = formData.get('placeholder') || pleaseInput; - $td.append(`