生成用户隐私协议文件竟然还要花钱,看我帮家人们把价格打下来!

755 阅读3分钟

前言

在准备将谷歌浏览器插件上架应用市场时,必须填写隐私协议声明,并提供一个可直接访问的链接地址,无法上传 txt 文件。

然后我在网上找到一个工具,能快速生成隐私协议,只需要填写产品名称、作者、公司等信息,勾选相关选项,便可自动生成协议,确实非常方便。可惜的是,它竟然要收费 5 元才能一键在线部署,显然我是不打算支付这笔费用的!(狗头)😝

所以打算自己做一个并开源,让大家都能免费使用。

开始制作

直接 vite 起手一个模板项目,然后 tailwind 来写样式

上一篇文章分享过高效使用 cursor 的方法,我直接拿来网上收费软件的截图发给 cursor。 由于做的过程中没有截图记录,这里直接展示成品界面。

首页

image.png

填写产品名称后,进入到一个三步表单。收集用户输入的相关信息,这里使用react-hook-form 来管理Form表单相关的数据。

image.png image.png
  // 表单步骤
  const [currentStep, setCurrentStep] = useState<FormStep>(1);
  // 生成的内容结果
  const [generatedContent, setGeneratedContent] = useState<string>('');
  
  const {
    register,
    handleSubmit,
    watch,
  } = useForm<FormData>({
    defaultValues: {
      productName: productName,
      productType: 'App',
      collectUserInfo: true,
      collectInfo: {
        phone: false,
        email: false,
        preferences: false,
      },
      collectMethod: {
        registration: false,
        thirdParty: false,
      },
      scope: 'selfUse',
      protection: {
        encryption: false,
        keepAfterDelete: false,
      },
      deletion: {
        deleteWhenNotNeeded: false,
        allowUserDelete: false,
      },
      useCookies: false,
      attractChildren: false,
    },
  });

  // 监听收集信息开关的状态
  const collectUserInfo = watch('collectUserInfo');

  const onSubmit = (data: FormData) => {
    if (currentStep === 2) {
      // 生成隐私政策内容
      setGeneratedContent(JSON.stringify(data));
      nextStep();
    } else {
      nextStep();
    }
  };

  const nextStep = () => {
    if (currentStep < 3) {
      setCurrentStep((prev) => (prev + 1) as FormStep);
    }
  };

  const prevStep = () => {
    if (currentStep > 1) {
      setCurrentStep((prev) => (prev - 1) as FormStep);
    }
  };

Dom 部分我这里就主要展示下重要的内容吧,form 表单项就不贴在这里了,然后还需要封装一个在第三步呈现表单数据预览的 Result 组件。需要的可以在末尾查看源码。

 <div className="p-6 mx-auto max-w-4xl">
      {/* 步骤指示器 */}
      <div className="mb-8 ml-[100px] flex items-center justify-between px-6">
        {[
          { step: 1, label: '基本配置' },
          { step: 2, label: '附加选项' },
          { step: 3, label: '生成结果' },
        ].map(({ step, label }) => (
          <div key={step} className="flex flex-1 items-center">
            <div className="flex items-center">
              <div
                className={`
                flex h-8 w-8 items-center justify-center rounded-full
                ${
                  step === currentStep
                    ? 'bg-blue-600 text-white'
                    : step < currentStep
                      ? 'bg-green-500 text-white'
                      : 'bg-gray-200 text-gray-600'
                }
              `}
              >
                {step}
              </div>
              <span className="ml-2 text-sm text-gray-600">{label}</span>
            </div>
            {step < 3 && (
              <div
                className={`mx-4 h-[1px] flex-1 ${
                  step < currentStep ? 'bg-green-500' : 'bg-gray-200'
                }`}
              />
            )}
          </div>
        ))}
      </div>

      {currentStep === 3 ? (
        <Result
          onBack={() => setCurrentStep(2)}
          formData={JSON.parse(generatedContent)}
        />
      ) : (
        <form
          onSubmit={handleSubmit(onSubmit)}
          className="p-6 bg-white rounded-lg shadow-sm"
        >
          {currentStep === 1 && (
           <div>第一部分相关的表单项 这里省略不贴了...</div>
          )}

          {currentStep === 2 && (
           <div>>第二部分相关的表单项 这里省略不贴了...</div>
          )}

          <div className="flex justify-between mt-8">
            <button
              type="button"
              onClick={prevStep}
              className={`rounded-lg px-6 py-2 ${
                currentStep === 1
                  ? 'cursor-not-allowed bg-gray-100 text-gray-400'
                  : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
              }`}
              disabled={currentStep === 1}
            >
              {currentStep === 1 ? '取消' : '上一步'}
            </button>
            <button
              type="submit"
              className="px-6 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700"
            >
              {currentStep === 2 ? '确认生成' : '下一步'}
            </button>
          </div>
        </form>
      )}
    </div>

最后根据 Form 的数据呈现生成的协议内容。

PixPin_2025-02-21_13-55-38.gif

这里跟 AI 老师学到了如何将预览的 React 组件输出为 html 文件。原来有一个方法叫做outerHTML

developer.mozilla.org/zh-CN/docs/…

我这里生成文件夹的原因是为了后续上传 netify 需要是文件夹内放一个 index.html的形式。

const downloadHtmlFile = () => {
    if (templateRef.current) {
      const html = templateRef.current.outerHTML;
      const zip = new JSZip();
      zip.folder(formData.productName)?.file('index.html', html);

      zip.generateAsync({ type: 'blob' }).then(function (blob) {
        const element = document.createElement('a');
        element.href = URL.createObjectURL(blob);
        element.download = `${formData.productName}.zip`;
        document.body.appendChild(element);
        element.click();
        document.body.removeChild(element);
      });
    }
  };

然后用户点击下载 HTML后根据下方的教程指引部署到 netify,很简单,登录账号后把下载好的文件夹拖拽上传即可。

image.png image.png image.png image.png

然后就可以愉快的将获取到的这个在线地址贴在需要使用的地方啦~

使用 CI 部署到 github Page

在项目根目录创建 .github/workflows/deploy-pages.yml 文件

name: Deploy to GitHub Pages

on:
  push:
    branches:
      - main
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: "pages"
  cancel-in-progress: false

jobs:
  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: lts/*

      - name: Install pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 9

      - name: Install dependencies
        run: pnpm install

      - name: Build
        run: pnpm build

      - name: Setup Pages
        uses: actions/configure-pages@v4

      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: "dist"

      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

在 push 到仓库 setting 中开启 github action 这样我们代码 push 到 main 分支就可以自动更新到 github page 了

源码

在线地址: llmoskk.github.io/privacy-pol…

仓库源码: github.com/LLmoskk/pri…

结语

全程使用 cursor 辅助开发 总耗时 4 小时左右,主要费时在根据条件控制隐私协议的生成的内容上 😂,cursor 会删掉一些话,可能是文本太长了,我自己花时间人工校验的。希望对大家有帮助~