以整个视口为单位,使用滑轮滚动或导航触发来达到展示效果的网站是一种需求。一种是自己手写 js + css 来达到这种效果。还有一种使用 fullpage.js 来达到这个效果。当然,使用 swiperjs 来实现这效果也是妥妥的赞,还很丝滑。
https://github.com/alvarotrigo/fullPage.js
https://swiperjs.com/get-started
主要是 fullpage.js 是需要花钱购买服务,而 swiperjs 是开源使用的。并且 swiperjs 不仅仅可以做这种视口滚动效果。
所以,这里选择使用 swiperjs 来完成目标。
DEMO 展示效果如下图所示(gif 使用 https://www.mnggiflab.com/ 录制和压缩):

基础环境
初始化一个 vue 项目。
pnpm create vue
cd swiperFullpage
pnpm install
pnpm format
pnpm dev
创建选项如下图所示。

然后,创建一个版本管理。
git init
git add .
git commit -m 'initialize'
先去掉 components 里的页面和默认样式。只留 App.vue 文件
rm -rf ./src/components
rm -rf ./src/assets
然后,修改 App.vue 文件。
<template>
<h2>Home</h2>
</template>
删除 main.ts 中的样式引入。运行起来看看。没问题,添加 git 版本控制。
git add . && git commit -m '删除默认页面和样式'
使用
先安装 swiper。
pnpm add swiper
开始在 App.vue 中编写相关的代码
<template>
<swiper
direction="vertical"
:modules="[FreeMode, Mousewheel]"
:space-between="0"
:slides-per-view="'auto'"
:allow-touch-move="false"
:pagination="{ clickable: true }"
:mousewheel="{ forceToAxis: true, sensitivity: 10, releaseOnEdges: true }"
style="height: 100vh"
class="swiper-pointer-events"
@swiper="onSwiper"
@slideChange="onSlideChange"
ref="swiperRef"
>
<swiper-slide class="slide" style="background: #444">
<div class="slide-content">
<h2>点绛唇·屏却相思</h2>
<p>屏却相思,近来知道都无益</p>
<p>不成抛掷,梦里终相觅</p>
<p>醒后楼台,与梦俱明灭</p>
<p>西窗白,纷纷凉月,一院丁香雪</p>
</div>
</swiper-slide>
<swiper-slide class="slide" style="background: #333">
<div class="slide-content">
<h2>《望江南》</h2>
<p>多少恨,昨夜梦魂中</p>
<p>还似旧时游上苑</p>
<p>车如流水马如龙</p>
<p>花月正春风</p>
</div>
</swiper-slide>
<swiper-slide class="slide" style="background: #222">
<div class="slide-content">
<h2>《蟾宫曲·春情》</h2>
<p>平生不会相思</p>
<p>才会相思,便害相思</p>
<p>身似浮萍,心如飞絮,气若游丝,</p>
<p>空一缕余香在此</p>
</div>
</swiper-slide>
<swiper-slide class="slide slide-footer" style="height: 240px; background: #111">
<p>© 2025 Vini123.Com All rights reserved.</p>
</swiper-slide>
</swiper>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Swiper, SwiperSlide } from 'swiper/vue'
import { FreeMode, Mousewheel } from 'swiper/modules'
import 'swiper/css'
const swiperRef = ref()
const activeIndex = ref(0)
const onSwiper = (swiper: any) => {
swiperRef.value = swiper
activeIndex.value = swiper.activeIndex
}
const onSlideChange = () => {
if (swiperRef.value) {
activeIndex.value = swiperRef.value.activeIndex
}
}
</script>
<style>
html,
body {
margin: 0;
padding: 0;
}
</style>
<style scoped>
.slide {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
}
.slide-footer {
color: #fff;
align-items: flex-end;
padding-bottom: 20px;
}
.slide-content {
color: #fff;
text-align: center;
background: rgba(0, 0, 0, 0.4);
padding: 30px 100px;
border-radius: 24px;
}
</style>
这里要注意一点。并不是所有的页面都是整个视口的高度。比如底部的信息。这个时候需要设置 slides-per-view:auto 和 FreeMode。其他的根据实际情况操作。这里页面高度是整个视口的高度 100vh,有的时候,swiper 是某个页面的一部分,高度自然不一样。
运行起来看看,提交版本控制。
git add .
git commit -m '安装 swiper,编写 demo'
dify 使用 docker 部署是最方便的。只是拉取镜像的时候经常会失败。
配置 docker 镜像可方便拉取。
{
"registry-mirrors": [
"https://docker.1panel.live",
"https://docker.nju.edu.cn",
"https://docker.m.daocloud.io",
"https://dockerproxy.com",
"https://hub-mirror.c.163.com",
"https://docker.mirrors.ustc.edu.cn",
"https://registry.docker-cn.com",
"https://registry.cn-hangzhou.aliyuncs.com"
]
}
AI 对话项目,会用到流式返回。一般客户端不会直接调用 AI 对话,而是调用自己的服务,自己的服务再调用 AI. 服务端可以通过 websocket 的方式来达到这个效果,也可以通过 SSE 的方式来达到这个效果。而 AI 也可能会提供这两种方式来提供服务。这样一来,项目就可能有四种组合方式来进行 AI 对话。如下:
| 客户端 | 服务端 | AI |
|---|---|---|
| APP、浏览器 | websocket | websocket |
| APP、浏览器 | websocket | SSE |
| APP、浏览器 | SSE | websocket |
| APP、浏览器 | SSE | SSE |
https://developer.mozilla.org/zh-CN/docs/Web/API/Server-sent_events/Using_server-sent_events
找 AI
websocket 阵营
https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
SSE 阵营
https://agents.baidu.com/docs/develop/out-deployment/conversation/
尝试实现四种组合
从最简单的开始。服务端通过 SSE 的方式, AI 也是 SSE 的方式。
先封装一个 curl 工具包,创建 internal/pkg/curl/curl.go,内容如下。
package curl
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/gclient"
)
type HttpClient struct {
client *gclient.Client
}
func NewHttpClient() *HttpClient {
return &HttpClient{
client: g.Client(),
}
}
func (h *HttpClient) Get(ctx context.Context, url string, headers map[string]string) (string, error) {
res, err := h.client.Get(ctx, url, headers)
if err != nil {
return "", err
}
defer res.Close()
return res.ReadAllString(), nil
}
func (h *HttpClient) Post(ctx context.Context, url string, data interface{}, headers map[string]string) (string, error) {
for k, v := range headers {
h.client.SetHeader(k, v)
}
res, err := h.client.Post(ctx, url, data, headers)
if err != nil {
return "", err
}
defer res.Close()
return res.ReadAllString(), nil
}
func (h *HttpClient) PostStream(ctx context.Context, url string, data interface{}, headers map[string]string) (io.ReadCloser, error) {
// 将 data 转换为 io.Reader
var body io.Reader
switch v := data.(type) {
case string:
body = bytes.NewReader([]byte(v))
case []byte:
body = bytes.NewReader(v)
default:
return nil, fmt.Errorf("unsupported data type: %T", v)
}
// 创建 HTTP 请求
req, err := http.NewRequestWithContext(ctx, "POST", url, body)
if err != nil {
return nil, err
}
// 设置请求头
for k, v := range headers {
req.Header.Set(k, v)
}
// 发起请求
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
// 检查响应状态码
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("request failed with status code: %d", resp.StatusCode)
}
return resp.Body, nil
}
现在来完善 chat 接口。
AI 聊天,会用到流式返回的接口。用 goframe 框架尝试尝试。
开始
初始化项目
gf init stream.demo -u
cd stream.demo
gf run main.go
默认路由信息是这样的。
build: main.go
go build -o .\main.exe main.go
.\main.exe
build running pid: 44364
2025-07-11T13:44:19.160+08:00 [INFO] pid[44364]: http server started listening on [:8000]
2025-07-11T13:44:19.160+08:00 [INFO] {ac79854f201c51188f16b244dba07b30} swagger ui is serving at address: http://127.0.0.1:8000/swagger/
2025-07-11T13:44:19.161+08:00 [INFO] {ac79854f201c51188f16b244dba07b30} openapi specification is serving at address: http://127.0.0.1:8000/api.json
ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE
----------|--------|------------|-------------------------------------------------------------|----------------------------------
:8000 | ALL | /api.json | github.com/gogf/gf/v2/net/ghttp.(*Server).openapiSpec |
----------|--------|------------|-------------------------------------------------------------|----------------------------------
:8000 | GET | /hello | stream.demo/internal/controller/hello.(*ControllerV1).Hello | ghttp.MiddlewareHandlerResponse
----------|--------|------------|-------------------------------------------------------------|----------------------------------
:8000 | ALL | /swagger/* | github.com/gogf/gf/v2/net/ghttp.(*Server).swaggerUI | HOOK_BEFORE_SERVE
----------|--------|------------|-------------------------------------------------------------|----------------------------------
提交版本
git init -b main
git add .
git commit -m 'initialize'
定义接口,创建控制器,定义路由
新建 api/chat/v1/chat.go 文件,定义以下请求和返回
package v1
import "github.com/gogf/gf/v2/frame/g"
type ChatReq struct {
g.Meta `path:"/chat" method:"post" tags:"聊天" summary:"聊天请求"`
Uid uint64 `v:"required#用户ID不能为空" json:"uid" dc:"用户ID"`
Message string `v:"required#消息内容不能为空" json:"message" dc:"消息内容"`
}
type ChatRes struct {
IsEnd bool `json:"is_end" dc:"是否结束"`
}
执行 gf gen ctrl 生成控制器。修改 internal/cmd/cmd.go,增加路由配置。
import (
……
"stream.demo/internal/controller/hello"
)
……
s.Group("/api", func(group *ghttp.RouterGroup) {
group.Bind(
chat.NewV1(),
)
})
再运行 gf run main.go,把项目跑起来看看。发现 chat 路由已经好了。
build: main.go
go build -o .\main.exe main.go
.\main.exe
build running pid: 44148
2025-07-11T14:13:29.676+08:00 [INFO] pid[44148]: http server started listening on [:8000]
2025-07-11T14:13:29.676+08:00 [INFO] {5065f8ccb71d5118823d646fdde40224} swagger ui is serving at address: http://127.0.0.1:8000/swagger/
2025-07-11T14:13:29.677+08:00 [INFO] {5065f8ccb71d5118823d646fdde40224} openapi specification is serving at address: http://127.0.0.1:8000/api.json
ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE
----------|--------|------------|-------------------------------------------------------------|----------------------------------
:8000 | ALL | /api.json | github.com/gogf/gf/v2/net/ghttp.(*Server).openapiSpec |
----------|--------|------------|-------------------------------------------------------------|----------------------------------
:8000 | POST | /api/chat | stream.demo/internal/controller/chat.(*ControllerV1).Chat | ghttp.MiddlewareHandlerResponse
----------|--------|------------|-------------------------------------------------------------|----------------------------------
:8000 | GET | /hello | stream.demo/internal/controller/hello.(*ControllerV1).Hello | ghttp.MiddlewareHandlerResponse
----------|--------|------------|-------------------------------------------------------------|----------------------------------
:8000 | ALL | /swagger/* | github.com/gogf/gf/v2/net/ghttp.(*Server).swaggerUI | HOOK_BEFORE_SERVE
----------|--------|------------|-------------------------------------------------------------|----------------------------------
开始流式 demo
编辑 internal/controller/chat/chat_v1_chat.go
package chat
import (
"context"
"fmt"
"time"
"github.com/gogf/gf/v2/net/ghttp"
v1 "stream.demo/api/chat/v1"
)
func (c *ControllerV1) Chat(ctx context.Context, req *v1.ChatReq) (res *v1.ChatRes, err error) {
r := ghttp.RequestFromCtx(ctx)
r.Response.Header().Set("Content-Type", "text/event-stream")
r.Response.Header().Set("Cache-Control", "no-cache")
r.Response.Header().Set("Connection", "keep-alive")
r.Response.Header().Set("Content-Encoding", "identity")
for i := range 10 {
data := fmt.Sprintf(`{"uid":%d, "message":"%s","index":%d}`, req.Uid, req.Message, i)
r.Response.Writefln("data: %s\n\n", data)
r.Response.Flush()
time.Sleep(time.Millisecond * 1000)
}
fmt.Println("发送结束")
res = &v1.ChatRes{}
res.IsEnd = true
return
}
重新运行项目。
测试
可以使用 curl 命令,可以使用 apifox 这种工具,也可以使用浏览器。 这里使用浏览器加 js 的方式来搞。
使用 chrome 打开 http://127.0.0.1:8000 ,然后打开控制台。输入以下测试代码,回车。
fetch("http://127.0.0.1:8000/api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
message: "舞低杨柳楼心月,歌尽桃花扇底风",
uid: 1,
}),
})
.then(readStream)
.catch(console.error);
async function readStream(res) {
const reader = res.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let lines = buffer.split("\n\n");
buffer = lines.pop();
for (const chunk of lines) {
const dataLine = chunk.trim();
if (dataLine.startsWith("data: ")) {
const data = dataLine.slice(6);
console.log(getNowTime(), "消息:", data);
}
}
}
}
function getNowTime() {
const date = new Date();
return date.toLocaleTimeString("zh-CN", {
hour12: false,
timeZone: "Asia/Shanghai",
});
}
截图如下:

提交版本控制。
git add .
git commit -m '增加 chat 接口,并测试流式响应'
nginx 处理
如果走 nginx 代理,需要处理关闭相应缓冲
location /api/ {
proxy_pass http://你的后端服务地址;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off;
proxy_cache off;
proxy_request_buffering off;
chunked_transfer_encoding on;
}
111
后台管理系统,个人审美还是更喜欢 element-plus 多一点。如果组件更丰富点就更好了。
https://element-plus.org/zh-CN/guide/installation.html
安装
# 安装
pnpm add element-plus
# 自动导入,安装unplugin-vue-components 和 unplugin-auto-import 这两款插件
pnpm add -D unplugin-vue-components unplugin-auto-import
配置自动导入
编辑 vite.config.ts
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
// ...
plugins: [
// ...
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
})
追加 AutoImport 和 Components 配置。完整的配置文件如下:
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vite.dev/config/
export default defineConfig({
base: '/admin/',
build: {
outDir: 'admin',
emptyOutDir: true,
chunkSizeWarningLimit: 3000,
rollupOptions: {
output: {
entryFileNames: 'assets/[name].[hash].js', // 入口文件名
chunkFileNames: 'assets/[name].[hash].js', // chunk 文件名
assetFileNames: 'assets/[name].[hash].[ext]', // 静态资源文件名
},
},
},
plugins: [
vue(),
vueDevTools(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
})
下边进行尝试使用
使用
编辑 src/views/home/index.vue,增加按钮组件,尝试下效果。
<template>
<div>
<h1>HI, {{ user.name }} , Welcome to the Home Page</h1>
<h2>Email: {{ user.email }}</h2>
<h2>Api: {{ api }}</h2>
<div>
<el-button type="primary" @click="test">Click Me</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const user = ref<User>({
id: 1,
name: 'John Doe',
email: 'john.doe@example.com',
createdAt: new Date(),
})
const api = ref(import.meta.env.VITE_API_BASE_URL)
function test() {
alert('Button Clicked!')
}
</script>
<style scoped>
h1 {
color: #42b983;
font-size: 2em;
}
</style>
运行起来,效果如下图所示。

项目打包,如果访问位置非对应根目录,是需要配置的。打包输出目录也是需要配置的(默认是 dist目录),chunk 也需要配置。
pnpm lint 执行检查的时候,应该对打包的文件忽略检查。这个也需要配置的。现在一个一个的来。
配置打包
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
base: '/admin/',
build: {
outDir: 'admin',
emptyOutDir: true,
chunkSizeWarningLimit: 3000,
rollupOptions: {
output: {
entryFileNames: 'assets/[name].[hash].js', // 入口文件名
chunkFileNames: 'assets/[name].[hash].js', // chunk 文件名
assetFileNames: 'assets/[name].[hash].[ext]', // 静态资源文件名
},
},
},
plugins: [vue(), vueDevTools()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
})
这里输出目录是 admin,基础路径也是 admin。 配置的警官文件大小,设置了打包输出的文件名等。
执行打包 pnpm build,会将项目打包到 admin 目录下。
但是,当执行 pnpm lint 检查的时候,会对 admin 目录下的代码进行检查。这个是不需要检查的。所以需要配置 eslint.confg.ts 来控制该行为。
配置 eslint.confg.ts 来忽略检查
import { globalIgnores } from 'eslint/config'
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
import pluginVue from 'eslint-plugin-vue'
import pluginOxlint from 'eslint-plugin-oxlint'
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
// import { configureVueProject } from '@vue/eslint-config-typescript'
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
export default defineConfigWithVueTs(
{
name: 'app/files-to-lint',
files: ['**/*.{ts,mts,tsx,vue}'],
},
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
pluginVue.configs['flat/essential'],
vueTsConfigs.recommended,
...pluginOxlint.configs['flat/recommended'],
skipFormatting,
{
name: 'app/rules',
rules: {
'vue/multi-word-component-names': 'off',
},
},
{
name: 'app/ignore',
ignores: ['admin/**'],
},
)
这个追加了以下配置。
{
name: 'app/ignore',
ignores: ['admin/**'],
},
最后,编辑 .gitignore 文件, 对 git 进行配置。让 git 忽略对 admin 目录里的文件进行版本控制。
初始化项目时,在根目录会有一个 .vscode 目录。
里边有一个 extensions.json 文件,配置了推荐的 vscode 插件。
里边有一个 settings.json 文件,配置了 vscode 一些行为控制。比如保存时,自动格式化,使用什么插件格式化等。可以单独设置 vue,json,css 等文件的格式化插件。如下:
{
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"tsconfig.json": "tsconfig.*.json, env.d.ts",
"vite.config.*": "jsconfig*, vitest.config.*, cypress.config.*, playwright.config.*",
"package.json": "package-lock.json, pnpm*, .yarnrc*, yarn*, .eslint*, eslint*, .oxlint*, oxlint*, .prettier*, prettier*, .editorconfig"
},
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
},
"editor.formatOnSave": true,
"[vue]": {
"editor.defaultFormatter": "Vue.volar"
},
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
}
该配置文件不进行版本控制。可以增加一个 settings.json.example 来进行版本控制。
手动格式化和检查
# 手动格式
pnpm format
# 手动检查
pnpm lint
https://cn.vite.dev/guide/env-and-mode
在本地运行和打包使用不同的配置的时候,环境变量的配置就很有作用。比如本地运行调用接口用的域名是 https://test.xxx.com,正式上线调用的接口是 https://www.xxx.com。
先创建智能提示
编辑 env.d.ts 文件,内容如下。
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
创建 env 文件
项目根目录添加 .env 文件。添加以下内容。
VITE_API_BASE_URL=https://test.xxx.com
项目根目录添加 .env.production 文件。添加以下内容。
VITE_API_BASE_URL=https://www.xxx.com
修改 ./src/home/index.vue 文件,测试
<template>
<div>
<h1>HI, {{ user.name }} , Welcome to the Home Page</h1>
<h2>Email: {{ user.email }}</h2>
<h2>Api: {{ api }}</h2>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const user = ref<User>({
id: 1,
name: 'John Doe',
email: 'john.doe@example.com',
createdAt: new Date(),
})
const api = ref(import.meta.env.VITE_API_BASE_URL)
</script>
<style scoped>
h1 {
color: #42b983;
font-size: 2em;
}
</style>
测试环境
pnpm dev
生产环境
pnpm dev --mode production
结果如图:

