【AJAX项目】黑马头条——数据管理平台

目录

[一、 项目准备](#一、 项目准备)

[二、 验证码登录](#二、 验证码登录)

真正的验证码登录原理:

[三、 token的介绍](#三、 token的介绍)

概念:访问权限的令牌,本质上是一串字符串

创建:正确登录后,由后端签发并返回

作用:判断是否有登录状态等,控制访问权限

四、个人信息设置和axios请求拦截器

五、axios响应拦截器

优化-axios响应结果

[六、发布文章 - 富文本编辑器 (wangEditor插件)](#六、发布文章 - 富文本编辑器 (wangEditor插件))

七、发布文章-频道列表

[八、发布文章 - 封面设置](#八、发布文章 - 封面设置)

[九、发布文章 - 收集并保存](#九、发布文章 - 收集并保存)

[十、内容管理 - 文章列表展示](#十、内容管理 - 文章列表展示)

[十一、内容管理 - 筛选功能](#十一、内容管理 - 筛选功能)

[十二、内容管理 - 分页功能](#十二、内容管理 - 分页功能)

[十三、内容管理 - 删除功能](#十三、内容管理 - 删除功能)

十四、内容管理-编辑文章-回显

十五、编辑文字-保存

[总结不易~ 本章节对我有很大收获,希望对你也是!!!](#总结不易~ 本章节对我有很大收获,希望对你也是!!!)


本节素材已上传至Gitee:ajax_study: 这是ajax、Node.j学习的仓库 - Gitee.comhttps://gitee.com/liu-yihao-hhh/ajax_study/tree/master/%E9%A1%B9%E7%9B%AE-%E6%95%B0%E6%8D%AE%E7%AE%A1%E7%90%86

一、 项目准备

技术:

  • 基于BootStrap 搭建网站标签和样式
  • 集成wangEditor插件实现富文本编辑器
  • 使用原生JS完成增删改查等业务
  • 基于axios与黑马头条线上接口交互
  • 使用axios拦截器进行权限判断

包含html, css, js, 静态图片, 第三方插件

目录管理:

  • assets: 资源文件夹(图片、字体等)
  • lib: 资料文件夹(第三方插件,例如:form-serialize)
  • page: 页面文件夹
  • utils: 实用程序文件夹(工具插件)

二、 验证码登录

  • 目标:完成验证码登录,后端设置验证码默认为246810
  • 原因:因为短信接口不是免费的,防止攻击者恶意盗刷

步骤:

  • 再utils/request.js配置axios请求基地址
  • 作用:提取公共前缀地址,配置后axios请求时都会baseURL + url 防止项目太大,基地址发生变化会导致所有页面请求数据地址全要变动
  • 收集手机号和验证码数据
  • 基于axios调用验证码登录接口
  • 使用BootStrap的Alert警告框反馈结果给用户

收集表单数据,上传到服务器,然后通过try......catch......来判断是否请求成功,通过myAlert函数来进行登录效果弹窗

javascript 复制代码
// 1.2 收集手机号和验证码数据
document.querySelector('.btn').addEventListener('click', async () => {
  const form = document.querySelector('.login-form')
  const data = serialize(form, { hash: true, empty: true })
  console.log(data)

  try {
    // 1.3 基于 axios 调用验证码登录接口
    const response = await axios.post('/v1_0/authorizations', data)
    const result = response.data
    console.log('#', result)
    myAlert(true, '登录成功')
  } catch (error) {
    myAlert(false, error.response.data.message)
  }
})

alert检查验证码和手机号是否正确的弹窗

javascript 复制代码
// 弹窗插件
// 需要先准备 alert 样式相关的 DOM
/**
 * BS 的 Alert 警告框函数,2秒后自动消失
 * @param {*} isSuccess 成功 true,失败 false
 * @param {*} msg 提示消息
 */
function myAlert(isSuccess, msg) {
  const myAlert = document.querySelector('.alert')
  myAlert.classList.add(isSuccess ? 'alert-success' : 'alert-danger')
  myAlert.innerHTML = msg
  myAlert.classList.add('show')

  setTimeout(() => {
    myAlert.classList.remove(isSuccess ? 'alert-success' : 'alert-danger')
    myAlert.innerHTML = ''
    myAlert.classList.remove('show')
  }, 2000)
}

真正的验证码登录原理:

  1. 用户操作触发:用户在登录界面输入手机号码,主动点击 "发送验证码" 按钮,发起获取验证码流程,这是整个登录校验的起始交互 。
  2. 调用发送接口:系统携带用户输入的手机号码,调用专门用于发送短信验证码的服务器接口,传递手机号码参数,请求服务端执行发送验证码前置操作 。
  3. 服务端生成校验码:服务器接收到请求后,为该手机号码生成唯一的验证码,同时记录验证码生成时间,将这些信息存储在服务器,用于后续验证匹配 。
  4. 调用运营商通道:服务器携带手机号码,调用运营商(电信、移动、联通 )提供的短信发送接口,借助运营商网络能力传递验证码下发指令 。
  5. 基站下发短信:运营商通过基站,以无连接的短信方式,将包含验证码的信息发送到指定手机号码对应的终端设备 。
  6. 反馈发送状态:运营商将短信发送结果(成功 / 失败等 )返回给服务器,告知验证码是否已成功进入下发流程 。
  7. 返回前端结果:服务器把验证码发送成功的状态反馈给前端页面,让用户知晓验证码已尝试下发 。
  8. 用户填写验证码:用户查看手机收到的短信,获取验证码内容后,手动填写到登录页面的验证码输入框,完成信息回填 。
  9. 发起登录验证:用户点击 "登录",系统携带手机号码和填写的验证码,调用服务器的验证码登录验证接口,提交验证请求 。
  10. 服务端校验逻辑:服务器接收请求后,拿收到的手机号码、验证码,与第 3 步生成存储的验证码记录(含号码、验证码、生成时间 )比对,校验验证码是否正确、是否在有效期(基于生成时间判断 ),然后返回登录成功或失败结果 。

三、 token的介绍

概念:访问权限的令牌,本质上是一串字符串

创建:正确登录后,由后端签发并返回

复制代码
eyJzX0BAiOiJkV1QLChbc6IojT2UziIN19.eyJpc3MiOiJodHRwczovL3dL5dy5pdmNhc3RQVyJ2F1iOiJ3iCVjIof0wIY2THNnGEYJN5CO
UZDYLT110WOdGQYZ2ZIzUYJtxW11amR10j0wWZMMNJet2UY5DGYz3L7G4MKcAT2M2K62KtgNDcz21YtwFUj0xJ0nK
wJKxLcJleHAiOjE20DE20YOT9FJkvIGauMp1Z6w.co0eqX4SCR6v8VbouuPzYVw84

作用:判断是否有登录状态等,控制访问权限

注意:前端只能判断token有无,而后端才能判断token的有效性

token的使用

目标:只有登录状态,才可以访问内容页面

步骤:

  1. 在 utils/auth.js 中判断无 token 令牌字符串,则强制跳转到登录页 (手动修改地址栏测试)
  2. 在登录成功后,保存 token 令牌字符串到本地,再跳转到首页 (手动修改地址栏测试)

观察一下没有进行登录,也就没用本地存储的token,会被踢回登录页面

javascript 复制代码
// 1.1 判断无 token 令牌字符串,则强制跳转到登录页
const token = localStorage.getItem('token')
if(!token) {
  localStorage.href = '../login/index.html'
}

登录成功后,将当前服务器返回的token进行本地存储

javascript 复制代码
// 1.2 收集手机号和验证码数据
document.querySelector('.btn').addEventListener('click', async () => {
  const form = document.querySelector('.login-form')
  const data = serialize(form, { hash: true, empty: true })
  console.log(data)

  try {
    // 1.3 基于 axios 调用验证码登录接口
    const response = await axios.post('/v1_0/authorizations', data)
    const result = response.data
    console.log('#', result)
    myAlert(true, '登录成功')
    // 登录成功后 保存token令牌到本地 并跳转到列表表表面
    localStorage.setItem('token', result.data.token)
    console.log(result.data)
    setTimeout(() => {
      // 延迟跳转 让alert警告框停留一会
      location.href = '../content/index.html'
    }, 1500)

  } catch (error) {
    myAlert(false, error.response.data.message)
  }
})

四、个人信息设置和axios请求拦截器

axios请求拦截器:发起请求之前,触发的配置函数,对请求参数进行额外配置

javascript 复制代码
axios.interceptors.request.use(function (config) {
    const token = localStorage.getItem('token');
    token && (config.headers.Authorization = `Bearer ${token}`);
    // 在发送请求之前做些什么
    return config;
}, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
});
  • 身份验证:如上述例子,在每次请求发送到服务器前,检查并添加用户的身份验证 token,确保只有已登录且拥有合法权限的用户请求才能被服务器处理 。
  • 请求参数统一处理:可以在拦截器中对所有请求的参数进行统一的格式化、添加公共参数(如时间戳、设备标识等) 等操作。
  • 请求头统一设置 :除了设置身份验证信息,还可以统一设置Content-TypeAccept等请求头字段,确保所有请求的格式符合服务器要求。

在request.js文件中设置axios请求拦截器

javascript 复制代码
// 目标2:设置个人信息
// 2.1 在utils/request.js 设置请求拦截器 同意携带token
// 2.2 请求个人信息并设置到页面
// utils/request.js
axios.interceptors.request.use(function (config) {
  // 统一携带 token
  const token = localStorage.getItem('token')
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
}, function (error) {
  return Promise.reject(error)
})

在每次 HTTP 请求发送前自动添加身份验证令牌(Token),确保后端能够识别请求者的身份。这样,所有需要身份验证的 API 请求都会自动携带 Token,无需在每个请求中手动设置。

五、axios响应拦截器

axios响应拦截器:作用是在请求返回结果(进入 then/catch 前)统一处理响应,让你不用在每个请求里重复写逻辑。

javascript 复制代码
axios.interceptors.response.use(
  function (response) { 
    // 1. 响应成功(状态码 2xx 范围)时触发
    return response; 
  },
  function (error) { 
    // 2. 响应失败(状态码非 2xx,或请求报错)时触发
    return Promise.reject(error); 
  }
);

Axios 响应拦截器会在所有请求完成后 自动触发,无论请求成功(状态码 2xx)还是失败(状态码非 2xx 或网络错误)。它的本质是一个中间层 ,在响应数据到达你的业务代码(.then().catch())之前执行。

无论哪一个页面发生响应401请求错误,都会在这段代码里进行拦截,并且跳转到登录页面。

javascript 复制代码
// 响应拦截器
axios.interceptors.response.use(
  function (response) {
    // 1. 响应成功(状态码 2xx 范围)时触发
    return response;
  },
  function (error) {
    // 2. 响应失败(状态码非 2xx,或请求报错)时触发
    console.dir(error)
    if (error?.response?.status === 401) {
      alert('身份验证失败,请重新登陆')
      localStorage.clear()
      location.href = '../login/index.html'
    }
    return Promise.reject(error);
  }
);

当我修改了token值后,后端服务器发现token值对不上了,就被axios响应拦截了。

打个比方:

  • 请求拦截器是 "快递员",负责把包裹(请求)送到仓库(后端)。
  • 后端是 "仓库安检员",检查包裹里的 token(身份)是否合法。
  • 响应拦截器是 "你",收到仓库(后端)退回的 "身份不符" 包裹(401 响应),然后决定怎么处理(提示重新登录)。

总结:

  • 响应回到then和catch之前,触发的拦截函数,对响应结果统一处理时axios响应拦截器
  • axios响应拦截器状态为2xx触发成功回调,其他则触发失败回调函数

优化-axios响应结果

axios直接接收服务器返回的响应结果 就是return response.data,返回后,其他文件得到服务器返回的数据都可以少一层.data操作

javascript 复制代码
// 响应拦截器
axios.interceptors.response.use(
  function (response) {
    // 1. 响应成功(状态码 2xx 范围)时触发
    const result = response.data
    return result
  },
  function (error) {
    // 2. 响应失败(状态码非 2xx,或请求报错)时触发
    console.dir(error)
    if (error?.response?.status === 401) {
      alert('身份验证失败,请重新登陆')
      localStorage.clear()
      location.href = '../login/index.html'
    }
    return Promise.reject(error);
  }
);

这是因为 Axios 响应拦截器的 "全局预处理" 特性 ,能在响应返回业务代码前,统一 "剥掉" response.data 这一层。

六、发布文章 - 富文本编辑器 (wangEditor插件)

富文本:带样式,多格式的文本,在前端一般使用标签配合内联样式实现

就是跟elementpuls组件库一样

javascript 复制代码
                <div id="editor---wrapper">
                  <div id="toolbar-container"><!-- 工具栏 --></div>
                  <div id="editor-container"><!-- 编辑器 --></div>
                </div>

editor.js文件

javascript 复制代码
// 富文本编辑器
// 创建编辑器函数,创建工具栏函数
const { createEditor, createToolbar } = window.wangEditor

// 编辑器配置对象
const editorConfig = {
  // 占位提示文字
  placeholder: '发布文章内容...',
  // 编辑器变化时回调函数
  onChange(editor) {
    // 获取富文本内容
    const html = editor.getHtml()
    // 也可以同步到 <textarea>
    // 为了后续快速收集整个表单内容做铺垫
    document.querySelector('.publish-content').value = html
  }
}

// 创建编辑器
const editor = createEditor({
  // 创建位置
  selector: '#editor-container',
  // 默认内容
  html: '<p><br></p>',
  // 配置项
  config: editorConfig,
  // 配置集成模式(default 全部)(simple 简洁)
  mode: 'default', // or 'simple'
})

// 工具栏配置对象
const toolbarConfig = {}

// 创建工具栏
const toolbar = createToolbar({
  // 为指定编辑器创建工具栏
  editor,
  // 工具栏创建的位置
  selector: '#toolbar-container',
  // 工具栏配置对象
  config: toolbarConfig,
  // 配置集成模式
  mode: 'default', // or 'simple'
})

七、发布文章-频道列表

很简单,就是通过axios来获取服务器的数据,然后进行数据渲染

javascript 复制代码
// 1.1 获取频道列表数据
const setChannleList = async () => {
  const res = await axios('/v1_0/channels')
  console.log(res.data)
  const htmlStr = '<option value="" selected="">请选择文章频道</option>' + res.data.channels.map(item => `<option value="${item.id}">${item.name}</option>`).join('')
  document.querySelector('.form-select').innerHTML = htmlStr
}
setChannleList()

八、发布文章 - 封面设置

axios获取图片后进行切换,对 + 进行显示和隐藏,然后为了再次能够点击进行触发,那么就要进行代码的优化,对图片进行绑定点击事件 ,当进行点击的时候,图片文件再次自动触发click()事件

javascript 复制代码
// 目标2:封面设置
// 选择文件并保存在FormData
document.querySelector('.img-file').addEventListener('change', async e => {
  const file = e.target.files[0]
  const fd = new FormData()
  fd.append('image', file)
  // 单独上传图片并得到图片 URL 网址
  const response = await axios.post('/v1_0/upload', fd)
  console.log(response)
  // 回显并切回 img 标签展示(隐藏 + 号上传标签)
  const imgUrl = response.data.url
  document.querySelector('.rounded').src = imgUrl
  document.querySelector('.rounded').classList.add('show')
  document.querySelector('.place').classList.add('hide')
})

// 优化: 点击 img 可以重新切换封面
// 思路: img 点击 => 用js方式触发文件选择元素click 事件方法
document.querySelector('.rounded').addEventListener('click', () => {
  document.querySelector('.img-file').click()
})

九、发布文章 - 收集并保存

  • 基于form-serialize插件收集表单数据对象
  • 基于axios提交到服务器保存
  • 调用Alert警告框反馈结果给用户
  • 重置表单并跳转到列表页

利用serialize插件进行表单数据获取,然后通过axios.post进行数据提交 提交成功后进行表单数据清空和页面跳转

javascript 复制代码
// 发布文章 - 收集并保存
document.querySelector('.send').addEventListener('click', async () => {
  const form = document.querySelector('.art-form')
  const data = serialize(form, { hash: true, empty: true })
  // 发布文章的时候 不需要id属性 可以删除id 为后续做编辑使用
  delete data.id
  // 自己收集封面图片地址并保存到data对象中
  data.cover = {
    type: 1, // 封面类型
    images: [document.querySelector('.rounded').src] // 封面图片 URL
  }

  try {
    // axios 提交到服务器
    const response = await axios.post('/v1_0/mp/articles', data)
    // 调用提示用户
    myAlert(true, '发布成功')
    // 重置表单
    form.reset()
    // 封面要手动重置
    document.querySelector('.rounded').src = ''
    document.querySelector('.rounded').classList.remove('show')
    document.querySelector('.place').classList.remove('hide')
    // 富文本编辑器重置
    editor.setHtml('')

    setTimeout(() => {
      location.href = '../content/index.html'
    }, 1500)
  } catch (error) {
    myAlert(false, '发布错误')
  }
})

十、内容管理 - 文章列表展示

这里第一步就是用到axios查询语法

javascript 复制代码
axios.get('/接口地址', {
  params: {
    键1: 值1,
    键2: 值2
  }
})

axios 会自动把 params 中的键值对拼接到 URL 上,形成查询字符串。
然后对html结构进行map编辑生成一个join()字符串,进行渲染

javascript 复制代码
// 1.1 准备查询参数对象
const queryObj = {
  status: '', // 文章状态 (1- 待审核 2-审核通过) 空字符串 - 全部
  channel_id: '', // 文章频道 id 空字符串 - 全部
  page: 1, // 当前页码
  per_page: 2 // 当前页面条数
}

const setArtileList = async () => {
  const response = await axios('/v1_0/mp/articles', { params: { queryObj } })
  console.log(response)
  // 展示到指定标签的结构中
  const htmlStr = response.data.results.map(item => `
      <tr>
        <td>
          <img src="${item.cover.type === 0 ? `https://img2.baidu.com/it/u=2640406343,1419332367&amp;fm=253&amp;fmt=auto&amp;app=138&amp;f=JPEG?w=708&amp;h=500 ` : item.cover.images[0]}" alt="">
        </td>
        <td>${item.title}</td>
        <td>
          ${item.status === 1 ? `<span class="badge text-bg-success">审核通过</span>` : `<span class="badge text-bg-primary">待审核</span>`}
        </td>
        <td>
          <span>${item.pubdate}</span>
        </td>
        <td>
          <span>${item.read_count}</span>
        </td>
        <td>
          <span>${item.comment_count}</span>
        </td>
        <td>
          <span>${item.like_count}</span>
        </td>
        <td>
          <i class="bi bi-pencil-square edit"></i>
          <i class="bi bi-trash3 del"></i>
        </td>
      </tr>
    `).join('')
  console.log(htmlStr)
  document.querySelector('.art-list').innerHTML = htmlStr
}

setArtileList()

十一、内容管理 - 筛选功能

  • 设置频道列表数据
  • 监听筛选条件改变,保存查询信息到查询参数对象
  • 点击筛选时, 传递查询参数对象到服务器
  • 获取匹配数据,覆盖到页面展示

通过将筛选内容进行赋值,传入服务器得到筛选后的结果进行返回,再次进行渲染

javascript 复制代码
// 1.1 准备查询参数对象
const queryObj = {
  status: '', // 文章状态 (1- 待审核 2-审核通过) 空字符串 - 全部
  channel_id: '', // 文章频道 id 空字符串 - 全部
  page: 1, // 当前页码
  per_page: 2 // 当前页面条数
}
javascript 复制代码
// 监听筛选条件改变, 保存查询信息到查询参数对象
// 筛选状态标记数字 -> change事件 -> 绑定到查询参数对象上
document.querySelectorAll('.form-check-input').forEach(radio => {
  radio.addEventListener('change', e => {
    queryObj.status = e.target.value
  })
})

// 筛选频道 id -> chenge事件 -> 绑定到查询参数对象上
document.querySelector('.form-select').addEventListener('change', e => {
  queryObj.channel_id = e.target.value
})

// 点击筛选时 传递查询参数对象到服务器
document.querySelector('.sel-btn').addEventListener('click', () => {
  setArtileList()
})

十二、内容管理 - 分页功能

  • 保存并设置文章总条数
  • 点击下一页,做临界值判断,并切换页码参数请求最新数据

首先就是要设置分页总共有多少条,才能判断上一页和下一页的临界条件

javascript 复制代码
// 目标3 分页功能
// 3.2 点击下一页 做临界判断
document.querySelector('.next').addEventListener('click', e => {
  // 当前页码小于最大页码数
  if (queryObj.page < Math.ceil(totalCount / queryObj.per_page)) {
    queryObj.page++
    setArtileList()
  }
})

// 3.3 点击上一页
document.querySelector('.last').addEventListener('click', e => {
  if (queryObj.page > 1) {
    queryObj.page--
    document.querySelector('.page-now').innerHTML = `第${queryObj.page}页`
    setArtileList()
  }
})

十三、内容管理 - 删除功能

  1. 关联文章id到删除图标
  2. 点击删除获取文章id
  3. 调用删除接口,传递文章id到服务器
  4. 重写获取文章列表,再次渲染
javascript 复制代码
// 目标四:删除文章
document.querySelector('.art-list').addEventListener('click', async e => {
  // 判断删除的元素
  if (e.target.classList.contains('del')) {
    const delId = e.target.parentNode.dataset.id
    // 4.3 调用删除接口 传递文章 id 到服务器
    console.log(delId)
    const response = await axios.delete(`/v1_0/mp/articles/${delId}`)

    // 4.5 删除最后一页的最后一条 需要自动向前翻页
    const children = document.querySelector('.art-list').children
    if (children.length === 1 && queryObj.page !== 1) {
      queryObj.page--
      document.querySelector('.page-now').innerHTML = `第${queryObj.page}页`
    }
    setArtileList()
  }
})

十四、内容管理-编辑文章-回显

  1. 页面跳转传参(URL查询参数方式)
  2. 发布文章页面接收参数判断(共用同一套表单)
  3. 修改标题和文章按钮
  4. 获取文章详情数据回显表单
javascript 复制代码
  // 进行文章的编辑
  // 自调用函数
  ; (function () {
    const paramsStr = location.search
    const parmas = new URLSearchParams(paramsStr)
    parmas.forEach(async (value, key) => {
      // 当前有要编辑的文章id 被传入过来
      console.log('#', value, key)
      if (key === 'id') {
        document.querySelector('.title span').innerHTML = '修改文章'
        document.querySelector('.send').innerHTML = '修改'
        const response = await axios(`/v1_0/mp/articles/${value}`)
        console.log(response)
        // 组织我需要的数据对象
        const dataObj = {
          channel_id: response.data.channel_id,
          title: response.data.title,
          rounded: response.data.cover.images[0],
          content: response.data.content,
          id: response.data.id
        }

        // 遍历数据对象属性 映射到页面
        Object.keys(dataObj).forEach(key => {
          if (key === 'rounded') {
            if (dataObj[key]) {
              document.querySelector('.rounded').src = dataObj[key]
              document.querySelector('.rounded').classList.add('show')
              document.querySelector('.place').classList.add('hide')
            }
            else if (key === 'content') {
              // 富文本内容
              editor.setHtml(dataObj[key])
            }
            else document.querySelector(`[name=${key}]`).value = dataObj[key]
          }
        })
      }
    })
  })()

十五、编辑文字-保存

当按钮内容为"修改"时,收集表单中的文章数据(包括标题、内容、频道等),并使用 serialize 函数将其转为对象格式。随后,通过 DOM 获取封面图片地址,并组装成符合接口要求的 cover 数据结构(包含封面类型和图片数组)。接着,调用 axios.put 向后端发送修改文章的请求,请求地址中携带文章的 id,请求体中包含完整的文章数据和封面信息。整个操作被包装在 try...catch 中,用于捕获可能出现的错误,并根据请求结果通过 myAlert 提示用户是否修改成功。

javascript 复制代码
document.querySelector('.send').addEventListener('click', async e => {
  if (e.target.innerHTML !== '修改') return
  // 修改文章
  const form = document.querySelector('.art-form')
  const data = serialize(form, { hash: true, empty: true })
  try {
    const response = await axios.put(`/v1_0/mp/articles/${data.id}`, { ...data, cover: { type: document.querySelector('.rounded').src ? 1 : 0, images: [document.querySelector('.rounded').src] } })
    console.log(response)
    myAlert(true, '修改文章成功')
  }
  catch (error) {
    myAlert(false, '修改错误')
  }

})

总结不易~ 本章节对我有很大收获,希望对你也是!!!

相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax