快速入门:Google 表单插件

使用集合让一切井井有条 根据您的偏好保存内容并对其进行分类。

请完成本页面其余部分介绍的步骤,并在大约 10 分钟内创建了一个 Google 表单插件,该插件使用触发器在用户回复表单时发送电子邮件。若要查看插件在完成后的呈现效果,只需从 Google 表单插件商店安装表单通知即可。

设置

  1. 创建新的 Google 表单。 如果出现欢迎屏幕,请为表单提供标题并选择主题。
  2. 在新表单中,点击更多 菜单,然后选择脚本编辑器...。如果出现欢迎屏幕,请点击 Blank Project
  3. 依次选择菜单项 File > New > HTML file 创建新的 HTML 文件。 将此文件命名为 sidebar(Apps 脚本会自动添加 .html 扩展名)。
  4. 重复上一步骤,再创建一个名为 about.html 的 HTML 文件。
  5. 将这些文件的内容分别替换为以下内容:

code.gs

表单/通知/通知.gs
/**
 * @OnlyCurrentDoc
 *
 * The above comment directs Apps Script to limit the scope of file
 * access for this add-on. It specifies that this add-on will only
 * attempt to read or modify the files in which the add-on is used,
 * and not all of the user's files. The authorization request message
 * presented to users will reflect this limited scope.
 */

/**
 * A global constant String holding the title of the add-on. This is
 * used to identify the add-on in the notification emails.
 */
const ADDON_TITLE = 'Form Notifications';

/**
 * A global constant 'notice' text to include with each email
 * notification.
 */
const NOTICE = 'Form Notifications was created as an sample add-on, and is' +
  ' meant for' +
'demonstration purposes only. It should not be used for complex or important' +
'workflows. The number of notifications this add-on produces are limited by the' +
'owner\'s available email quota; it will not send email notifications if the' +
'owner\'s daily email quota has been exceeded. Collaborators using this add-on on' +
'the same form will be able to adjust the notification settings, but will not be' +
'able to disable the notification triggers set by other collaborators.';

/**
 * Adds a custom menu to the active form to show the add-on sidebar.
 *
 * @param {object} e The event parameter for a simple onOpen trigger. To
 *     determine which authorization mode (ScriptApp.AuthMode) the trigger is
 *     running in, inspect e.authMode.
 */
function onOpen(e) {
  try {
    FormApp.getUi()
        .createAddonMenu()
        .addItem('Configure notifications', 'showSidebar')
        .addItem('About', 'showAbout')
        .addToUi();
  } catch (e) {
    // TODO (Developer) - Handle exception
    Logger.log('Failed with error: %s', e.error);
  }
}

/**
 * Runs when the add-on is installed.
 *
 * @param {object} e The event parameter for a simple onInstall trigger. To
 *     determine which authorization mode (ScriptApp.AuthMode) the trigger is
 *     running in, inspect e.authMode. (In practice, onInstall triggers always
 *     run in AuthMode.FULL, but onOpen triggers may be AuthMode.LIMITED or
 *     AuthMode.NONE).
 */
function onInstall(e) {
  onOpen(e);
}

/**
 * Opens a sidebar in the form containing the add-on's user interface for
 * configuring the notifications this add-on will produce.
 */
function showSidebar() {
  try {
    const ui = HtmlService.createHtmlOutputFromFile('sidebar')
        .setTitle('Form Notifications');
    FormApp.getUi().showSidebar(ui);
  } catch (e) {
    // TODO (Developer) - Handle exception
    Logger.log('Failed with error: %s', e.error);
  }
}

/**
 * Opens a purely-informational dialog in the form explaining details about
 * this add-on.
 */
function showAbout() {
  try {
    const ui = HtmlService.createHtmlOutputFromFile('about')
        .setWidth(420)
        .setHeight(270);
    FormApp.getUi().showModalDialog(ui, 'About Form Notifications');
  } catch (e) {
    // TODO (Developer) - Handle exception
    Logger.log('Failed with error: %s', e.error);
  }
}

/**
 * Save sidebar settings to this form's Properties, and update the onFormSubmit
 * trigger as needed.
 *
 * @param {Object} settings An Object containing key-value
 *      pairs to store.
 */
function saveSettings(settings) {
  try {
    PropertiesService.getDocumentProperties().setProperties(settings);
    adjustFormSubmitTrigger();
  } catch (e) {
    // TODO (Developer) - Handle exception
    Logger.log('Failed with error: %s', e.error);
  }
}

/**
 * Queries the User Properties and adds additional data required to populate
 * the sidebar UI elements.
 *
 * @return {Object} A collection of Property values and
 *     related data used to fill the configuration sidebar.
 */
function getSettings() {
  try {
    const settings = PropertiesService.getDocumentProperties().getProperties();

    // Use a default email if the creator email hasn't been provided yet.
    if (!settings.creatorEmail) {
      settings.creatorEmail = Session.getEffectiveUser().getEmail();
    }

    // Get text field items in the form and compile a list
    //   of their titles and IDs.
    const form = FormApp.getActiveForm();
    const textItems = form.getItems(FormApp.ItemType.TEXT);

    settings.textItems = [];
    for (let i = 0; i < textItems.length; i++) {
      settings.textItems.push({
        title: textItems[i].getTitle(),
        id: textItems[i].getId()
      });
    }
    return settings;
  } catch (e) {
    // TODO (Developer) - Handle exception
    Logger.log('Failed with error: %s', e.error);
  }
}

/**
 * Adjust the onFormSubmit trigger based on user's requests.
 */
function adjustFormSubmitTrigger() {
  try {
    const form = FormApp.getActiveForm();
    const triggers = ScriptApp.getUserTriggers(form);
    const settings = PropertiesService.getDocumentProperties();
    const triggerNeeded =
      settings.getProperty('creatorNotify') === 'true' ||
      settings.getProperty('respondentNotify') === 'true';

    // Create a new trigger if required; delete existing trigger
    // if it is not needed.
    let existingTrigger = null;
    for (let i = 0; i < triggers.length; i++) {
      if (triggers[i].getEventType() === ScriptApp.EventType.ON_FORM_SUBMIT) {
        existingTrigger = triggers[i];
        break;
      }
    }
    if (triggerNeeded && !existingTrigger) {
      const trigger = ScriptApp.newTrigger('respondToFormSubmit')
          .forForm(form)
          .onFormSubmit()
          .create();
    } else if (!triggerNeeded && existingTrigger) {
      ScriptApp.deleteTrigger(existingTrigger);
    }
  } catch (e) {
    // TODO (Developer) - Handle exception
    Logger.log('Failed with error: %s', e.error);
  }
}

/**
 * Responds to a form submission event if an onFormSubmit trigger has been
 * enabled.
 *
 * @param {Object} e The event parameter created by a form
 *      submission; see
 *      https://developers.google.com/apps-script/understanding_events
 */
function respondToFormSubmit(e) {
  try {
    const settings = PropertiesService.getDocumentProperties();
    const authInfo = ScriptApp.getAuthorizationInfo(ScriptApp.AuthMode.FULL);

    // Check if the actions of the trigger require authorizations that have not
    // been supplied yet -- if so, warn the active user via email (if possible).
    // This check is required when using triggers with add-ons to maintain
    // functional triggers.
    if (authInfo.getAuthorizationStatus() ===
      ScriptApp.AuthorizationStatus.REQUIRED) {
      // Re-authorization is required. In this case, the user needs to be alerted
      // that they need to reauthorize; the normal trigger action is not
      // conducted, since authorization needs to be provided first. Send at
      // most one 'Authorization Required' email a day, to avoid spamming users
      // of the add-on.
      sendReauthorizationRequest();
    } else {
      // All required authorizations have been granted, so continue to respond to
      // the trigger event.

      // Check if the form creator needs to be notified; if so, construct and
      // send the notification.
      if (settings.getProperty('creatorNotify') === 'true') {
        sendCreatorNotification();
      }

      // Check if the form respondent needs to be notified; if so, construct and
      // send the notification. Be sure to respect the remaining email quota.
      if (settings.getProperty('respondentNotify') === 'true' &&
        MailApp.getRemainingDailyQuota() > 0) {
        sendRespondentNotification(e.response);
      }
    }
  } catch (e) {
    // TODO (Developer) - Handle exception
    Logger.log('Failed with error: %s', e.error);
  }
}


/**
 * Called when the user needs to reauthorize. Sends the user of the
 * add-on an email explaining the need to reauthorize and provides
 * a link for the user to do so. Capped to send at most one email
 * a day to prevent spamming the users of the add-on.
 */
function sendReauthorizationRequest() {
  try {
    const settings = PropertiesService.getDocumentProperties();
    const authInfo = ScriptApp.getAuthorizationInfo(ScriptApp.AuthMode.FULL);
    const lastAuthEmailDate = settings.getProperty('lastAuthEmailDate');
    const today = new Date().toDateString();
    if (lastAuthEmailDate !== today) {
      if (MailApp.getRemainingDailyQuota() > 0) {
        const template =
          HtmlService.createTemplateFromFile('authorizationEmail');
        template.url = authInfo.getAuthorizationUrl();
        template.notice = NOTICE;
        const message = template.evaluate();
        MailApp.sendEmail(Session.getEffectiveUser().getEmail(),
            'Authorization Required',
            message.getContent(), {
              name: ADDON_TITLE,
              htmlBody: message.getContent()
            });
      }
      settings.setProperty('lastAuthEmailDate', today);
    }
  } catch (e) {
    // TODO (Developer) - Handle exception
    Logger.log('Failed with error: %s', e.error);
  }
}

/**
 * Sends out creator notification email(s) if the current number
 * of form responses is an even multiple of the response step
 * setting.
 */
function sendCreatorNotification() {
  try {
    const form = FormApp.getActiveForm();
    const settings = PropertiesService.getDocumentProperties();
    let responseStep = settings.getProperty('responseStep');
    responseStep = responseStep ? parseInt(responseStep) : 10;

    // If the total number of form responses is an even multiple of the
    // response step setting, send a notification email(s) to the form
    // creator(s). For example, if the response step is 10, notifications
    // will be sent when there are 10, 20, 30, etc. total form responses
    // received.
    if (form.getResponses().length % responseStep === 0) {
      const addresses = settings.getProperty('creatorEmail').split(',');
      if (MailApp.getRemainingDailyQuota() > addresses.length) {
        const template =
          HtmlService.createTemplateFromFile('creatorNotification');
        template.summary = form.getSummaryUrl();
        template.responses = form.getResponses().length;
        template.title = form.getTitle();
        template.responseStep = responseStep;
        template.formUrl = form.getEditUrl();
        template.notice = NOTICE;
        const message = template.evaluate();
        MailApp.sendEmail(settings.getProperty('creatorEmail'),
            form.getTitle() + ': Form submissions detected',
            message.getContent(), {
              name: ADDON_TITLE,
              htmlBody: message.getContent()
            });
      }
    }
  } catch (e) {
    // TODO (Developer) - Handle exception
    Logger.log('Failed with error: %s', e.error);
  }
}

/**
 * Sends out respondent notification emails.
 *
 * @param {FormResponse} response FormResponse object of the event
 *      that triggered this notification
 */
function sendRespondentNotification(response) {
  try {
    const form = FormApp.getActiveForm();
    const settings = PropertiesService.getDocumentProperties();
    const emailId = settings.getProperty('respondentEmailItemId');
    const emailItem = form.getItemById(parseInt(emailId));
    const respondentEmail = response.getResponseForItem(emailItem)
        .getResponse();
    if (respondentEmail) {
      const template =
        HtmlService.createTemplateFromFile('respondentNotification');
      template.paragraphs = settings.getProperty('responseText').split('\n');
      template.notice = NOTICE;
      const message = template.evaluate();
      MailApp.sendEmail(respondentEmail,
          settings.getProperty('responseSubject'),
          message.getContent(), {
            name: form.getTitle(),
            htmlBody: message.getContent()
          });
    }
  } catch (e) {
    // TODO (Developer) - Handle exception
    Logger.log('Failed with error: %s', e.error);
  }
}

边栏

表单/通知/边栏.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">
    <!-- The CSS package above applies Google styling to buttons and other elements. -->
    <style>
    .branding-below {
      bottom: 54px;
      top: 0;
    }
    .branding-text {
      left: 7px;
      position: relative;
      top: 3px;
    }
    .logo {
      vertical-align: middle;
    }
    .width-100 {
      width: 100%;
      box-sizing: border-box;
      -webkit-box-sizing: border-box;
      -moz-box-sizing: border-box;
    }
    label {
      font-weight: bold;
    }
    #creator-options,
    #respondent-options {
      background-color: #eee;
      border-color: #eee;
      border-width: 5px;
      border-style: solid;
      display: none;
    }
    #creator-email,
    #respondent-email,
    #button-bar,
    #submit-subject {
      margin-bottom: 10px;
    }

    #response-step {
      display: inline;
    }
    </style>
  </head>
  <body>
    <div class="sidebar branding-below">
      <form>
        <div class="block">
          <input type="checkbox" id="creator-notify">
          <label for="creator-notify">Notify me</label>
        </div>
        <div class="block form-group" id="creator-options">
          <label for="creator-email">
            My email addresses (comma-separated)
          </label>
          <input type="text" class="width-100" id="creator-email">
          <label for="response-step">Send notifications after every</label>
          <input type="number" id="response-step" value="10"
              min="1" max="99999"> responses (default 10)
        </div>

        <div class="block">
          <input type="checkbox" id="respondent-notify">
          <label for="respondent-notify">Notify respondents</label>
        </div>
        <div class="block form-group" id="respondent-options">
          <label for="respondent-email">
            Which question asks for their email?
          </label>
          <select class="width-100" id="respondent-email"></select>
          <label for="submit-subject">
            Notification email subject:
          </label>
          <input type="text" class="width-100" id="submit-subject">
          <label for="submit-notice">Notification email body:</label>
          <textarea rows="8" cols="40" id="submit-notice"
              class="width-100"></textarea>
        </div>

        <div class="block" id="button-bar">
          <button class="action" id="save-settings">Save</button>
        </div>
      </form>
    </div>

    <div class="sidebar bottom">
      <img alt="Add-on logo" class="logo" width="25"
          src="https://g-suite-documentation-images.firebaseapp.com/images/newFormNotificationsicon.png">
      <span class="gray branding-text">Form Notifications by Google</span>
    </div>

    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js">
    </script>
    <script>
      /**
       * On document load, assign required handlers to each element,
       * and attempt to load any saved settings.
       */
      $(function() {
        $('#save-settings').click(saveSettingsToServer);
        $('#creator-notify').click(toggleCreatorNotify);
        $('#respondent-notify').click(toggleRespondentNotify);
        $('#response-step').change(validateNumber);
        google.script.run
           .withSuccessHandler(loadSettings)
           .withFailureHandler(showStatus)
           .withUserObject($('#button-bar').get())
           .getSettings();
      });

      /**
       * Callback function that populates the notification options using
       * previously saved values.
       *
       * @param {Object} settings The saved settings from the client.
       */
      function loadSettings(settings) {
        $('#creator-email').val(settings.creatorEmail);
        $('#response-step').val(!settings.responseStep ?
           10 : settings.responseStep);
        $('#submit-subject').val(!settings.responseSubject ?
           'Thank you for filling out our form!' :
           settings.responseSubject);
        $('#submit-notice').val(!settings.responseText ?
           'Thank you for responding to our form!' :
           settings.responseText);

        if (settings.creatorNotify === 'true') {
          $('#creator-notify').prop('checked', true);
          $('#creator-options').show();
        }

        if (settings.respondentNotify === 'true') {
          $('#respondent-notify').prop('checked', true);
          $('#respondent-options').show();
        }

        // Fill the respondent email select box with the
        // titles given to the form's text Items. Also include
        // the form Item IDs as values so that they can be
        // easily recovered during the Save operation.
        for (var i = 0; i < settings.textItems.length; i++) {
          var option = $('<option>').attr('value', settings.textItems[i]['id'])
              .text(settings.textItems[i]['title']);
          $('#respondent-email').append(option);
        }
        $('#respondent-email').val(settings.respondentEmailItemId);
      }

      /**
       * Toggles the visibility of the form creator notification options.
       */
      function toggleCreatorNotify() {
        $('#status').remove();
        if ($('#creator-notify').is(':checked')) {
          $('#creator-options').show();
        } else {
          $('#creator-options').hide();
        }
      }

      /**
       * Toggles the visibility of the form sumbitter notification options.
       */
      function toggleRespondentNotify() {
        $('#status').remove();
        if($('#respondent-notify').is(':checked')) {
          $('#respondent-options').show();
        } else {
          $('#respondent-options').hide();
        }
      }

      /**
       * Ensures that the entered step is a number between 1
       * and 99999, inclusive.
       */
      function validateNumber() {
        var value = $('#response-step').val();
        if (!value) {
          $('#response-step').val(10);
        } else if (value < 1) {
          $('#response-step').val(1);
        } else if (value > 99999) {
          $('#response-step').val(99999);
        }
      }

      /**
       * Collects the options specified in the add-on sidebar and sends them to
       * be saved as Properties on the server.
       */
      function saveSettingsToServer() {
        this.disabled = true;
        $('#status').remove();
        var creatorNotify = $('#creator-notify').is(':checked');
        var respondentNotify = $('#respondent-notify').is(':checked');
        var settings = {
          'creatorNotify': creatorNotify,
          'respondentNotify': respondentNotify
        };

        // Only save creator options if notify is turned on
        if (creatorNotify) {
          settings.responseStep = $('#response-step').val();
          settings.creatorEmail = $('#creator-email').val().trim();

          // Abort save if entered email is blank
          if (!settings.creatorEmail) {
            showStatus('Enter an owner email', $('#button-bar'));
            this.disabled = false;
            return;
          }
        }

        // Only save respondent options if notify is turned on
        if (respondentNotify) {
          settings.respondentEmailItemId = $('#respondent-email').val();
          settings.responseSubject = $('#submit-subject').val();
          settings.responseText = $('#submit-notice').val();
        }

        // Save the settings on the server
        google.script.run
            .withSuccessHandler(
              function(msg, element) {
                showStatus('Saved settings', $('#button-bar'));
                element.disabled = false;
              })
            .withFailureHandler(
              function(msg, element) {
                showStatus(msg, $('#button-bar'));
                element.disabled = false;
              })
            .withUserObject(this)
            .saveSettings(settings);
      }

      /**
       * Inserts a div that contains an status message after a given element.
       *
       * @param {String} msg The status message to display.
       * @param {Object} element The element after which to display the Status.
       */
      function showStatus(msg, element) {
         var div = $('<div>')
             .attr('id', 'status')
             .attr('class','error')
             .text(msg);
        $(element).after(div);
      }
    </script>
  </body>
</html>

about.html

表单/通知/about.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">
    <!-- The CSS package above applies Google styling to buttons and other elements. -->
  </head>
  <body>
    <div>
      <p>
      <i>Form Notifications</i> was created as an sample add-on, and is meant
      for demonstration purposes only. It should not be used for complex or
      important workflows.
      </p>
      <p>
      The number of notifications this add-on produces are limited by the owner's
      available email quota; it will not send email notifications if the owner's
      daily email quota has been exceeded. Collaborators using this add-on on the
      same form will be able to adjust the notification settings, but will not be
      able to disable the notification triggers set by other collaborators.
      </p>
    </div>
  </body>
</html>
  1. 创建另外三个 HTML 文件:authorizationEmail.html creatorNotification.html respondentNotification.html。将这些文件的内容分别替换为以下内容:

AuthorizationEmail.html

表单/通知/授权电子邮件.html
<p>The Google Forms add-on <i>Form Notifications</i> is set to run automatically
whenever a form is submitted. The add-on was recently updated and it needs you
to re-authorize it to run on your behalf.</p>

<p>The add-on's automatic functions are temporarily disabled until you
re-authorize the add-on. You can accomplish this by opening one of the forms
using the add-on and running the add-on through the menu. Alternatively, you can
click this link to approve authorization directly:</p>

<p><a href="<?= url ?>">Click here</a> to re-authorize the add-on.</p>

<p>This notification email will be sent to you at most once per day until the
add-on is re-authorized.</p>

<hr>

<p style="font-size:80%">This automatic message was sent to you via the <i>Form
Notifications</i> add-on for Google Forms.
<?= notice ?></p>

creatorNotification.html

表单/通知/creatorNotification.html
<p><i>Form Notifications</i> (a Google Forms add-on) has detected that the form
titled <a href="<?= formUrl?>"><b><?= title ?></b></a> has received
<?= responses ?> responses so far.</p>

<p><a href="<?= summary ?>">Summary of form responses</a></p>

<p>You are receiving this email because an editor of this form configured
<i>Form Notifications</i> to alert you every time this form receives
<b><?= responseStep ?></b> responses.</p>

<p>To change this setting, or to stop receiving these notifications, have the
form owner or editors open the form and adjust the <i>Form Notifications</i>
add-on configuration via the "Configure notifications" menu item.</p>

<hr>

<p style="font-size:80%">This automatic message was sent to you via the <i>Form
Notifications</i> add-on for Google Forms.
<?= notice ?></p>

responseentNotification.html

表单/通知/回复通知.html
<? for (var i = 0; i < paragraphs.length; i++) { ?>
  <p><?= paragraphs[i] ?></p>
<? } ?>

<hr>

<p style="font-size:80%">This automatic message was sent to you via the <i>Form
Notifications</i> add-on for Google Forms.
<?= notice ?></p>
  1. 选择菜单项 File > Save all。将新脚本命名为“表单通知快速入门”,然后点击确定。(脚本的名称会在多个位置(包括授权对话框)向最终用户显示。)

完成此过程后,您将拥有一个包含 1 个脚本文件和 5 个 HTML 文件的项目。

试试看

  1. 切换回表单。使用添加事项选择框为表单添加文本问题。在问题标题下,输入电子邮件地址,然后点击完成。您可以根据需要创建其他表单内容。
  2. 几秒钟后,插件通知 菜单下会显示表单通知快速入门子菜单。(如果您为脚本选择了其他名称,系统会改为显示该名称。)点击插件和表单通知快速入门,然后在出现的对话框中点击配置通知
  3. 系统会显示一个请求脚本授权的对话框。点击继续
  4. 系统随即会显示插件边栏。如需进行测试,请点击通知我复选框,然后输入您的电子邮件地址。另外,将在以下时间后发送通知框设置为 '1'。点击保存
  5. 点击预览,然后提交回复。在此处,在表单元素中输入一些信息,然后点击提交。如果您的所有操作均正确无误,此插件会向您发送一封简短的通知,告知您有人回复了您的表单。如果您未更改在每次通知后发送通知复选框,插件要等到表单完成 10 次提交(默认)后才会通过电子邮件向您发送通知。
  6. 如需测试回复者通知,请返回边栏并点击通知回复者复选框。第一个选择框会列出表单中每个文本问题的问题标题。选择您标记的问题 电子邮件地址。如果未显示“电子邮件地址”选项,请确保您已向名为“电子邮件地址”的表单中添加文本项(如第 1 步所述),然后通过点击插件和表单通知快速入门重新打开边栏,并在出现的对话框中点击配置通知
  7. 更改通知电子邮件正文文本区域中的文本,说出您要告诉回复者哪些信息。
  8. 返回表单,填写其他提交内容,然后点击提交。 如果所有操作都正确无误,则这次应发送两封电子邮件:一封发送到表单创建者,另一封发送到表单回复者。表单创建者电子邮件地址是您在边栏中直接输入的电子邮件地址;表单回复者电子邮件地址是受访者在“电子邮件地址”表单中提供的电子邮件地址。

发布

这是一个示例插件,我们的教程到此结束。如果您要开发真正的插件,最后一步是发布该插件,以供其他人查找和安装。

了解详情

如需继续了解如何使用 Apps 脚本扩展 Google 文档,请查看以下资源: