go17/vblog/web/README.md
2025-01-19 10:06:22 +08:00

593 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# web
Vblog 前端项目
## 清理模版代码
- 清理组件
- 清理样式
- 清理js文件
## 添加登录页面
- LoginPage.vue
```vue
<template>
<div>登录页面</div>
</template>
<script setup></script>
<style lang="css" scoped></style>
```
- 添加路有
```js
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/login',
name: 'login',
component: () => import('../views/common/LoginPage.vue'),
},
],
})
```
- App组件添加路由视图入口
```vue
<script setup></script>
<template>
<RouterView></RouterView>
</template>
<style scoped></style>
```
## 安装UI插件
https://gitee.com/infraboard/go-course/blob/master/day20/vue3-ui.md
```js
// 加载UI组件
import ArcoVue from '@arco-design/web-vue'
import '@arco-design/web-vue/dist/arco.css'
// 额外引入图标库
import ArcoVueIcon from '@arco-design/web-vue/es/icon'
app.use(ArcoVue)
app.use(ArcoVueIcon)
```
## Login Page开发
```vue
<template>
<div class="content">
<div class="login-container">
<div class="login-title">欢迎登录博客系统</div>
<div class="login-form">
<a-form size="large" :model="loginForm" @submit="handleSubmit">
<a-form-item
field="username"
hide-label
hide-asterisk
:rules="{ required: true, message: '请输入用户名' }"
>
<a-input
style="background-color: #fff;"
v-model="loginForm.username"
placeholder="请输入用户名"
>
<template #prefix>
<icon-user />
</template>
</a-input>
</a-form-item>
<a-form-item
style="margin-top: 12px"
field="password"
hide-label
hide-asterisk
:rules="{ required: true, message: '请输入密码' }"
>
<a-input-password
style="background-color: #fff;"
v-model="loginForm.password"
placeholder="请输入密码"
allow-clear
>
<template #prefix>
<icon-lock />
</template>
</a-input-password>
</a-form-item>
<a-form-item field="remember_me" hide-label hide-asterisk>
<a-checkbox style="margin-left: auto;" v-model="loginForm.remember_me" value="boolean"
>记住</a-checkbox
>
</a-form-item>
<a-form-item hide-label hide-asterisk>
<a-button type="primary" style="width: 100%;" html-type="submit">登录</a-button>
</a-form-item>
</a-form>
</div>
</div>
</div>
</template>
<script setup>
import { reactive } from 'vue'
const loginForm = reactive({
username: '',
password: '',
remember_me: false,
})
const handleSubmit = (data) => {
if (data.errors === undefined) {
console.log(data.values)
}
}
</script>
<style lang="css" scoped>
.content {
height: 100%;
width: 100%;
justify-content: center;
align-items: center;
display: flex;
background-color: rgb(243, 238, 238);
}
.login-container {
width: 460px;
height: 320px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.3);
/* 增加透明度 */
border-radius: 15px;
backdrop-filter: blur(15px);
/* 增加模糊程度 */
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2);
}
.login-title {
margin-top: 12px;
margin-bottom: 12px;
font-size: 20px;
font-weight: 500;
color: var(--color-neutral-8);
}
.login-form {
padding: 20px;
height: 100%;
width: 100%;
}
</style>
```
## 对接后端
调用后端的Restful接口, 需要一个http客户端
- XMLHTTPRequest
- fetch
- axios: https://www.axios-http.cn/docs/intro
```shell
npm install axios
```
### 后端API
```js
export const client = axios.create({
baseURL: 'http://127.0.0.1:8080',
timeout: 3000,
})
```
```js
import { client } from './client'
export const LOGIN = (data) =>
client({
url: '/api/vblog/v1/tokens',
method: 'POST',
data: data,
})
```
### 页面使用
```js
const summitLoading = ref(false)
const handleSubmit = async (data) => {
if (data.errors === undefined) {
try {
summitLoading.value = true
await LOGIN(data.values)
} catch (error) {
console.log(error)
} finally {
summitLoading.value = false
}
}
}
```
```sh
Access to XMLHttpRequest at 'http://127.0.0.1:8080/api/vblog/v1/tokens' from origin 'http://localhost:5173' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
```
### 如何处理CORS
- 1. 服务端 允许跨越访问, 服务端需要配置一个CROS 中间件,返回 允许的 origin列表, 允许 origin localhost 访问
- 2. 前端使用代码, 前端先访问 -- proxy -- backend
推荐: 前端使用代理 , 最终的效果是 前端和后端部署在一起
前端如何配置代理: (vite)
```js
// https://vite.dev/config/
export default defineConfig({
plugins: [vue(), vueJsx(), vueDevTools()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
proxy: {
'/api': 'http://localhost:8080',
},
},
})
```
http client在请求的时候不能指定baseURL: http://127.0.0.1:8080/api/vblog/v1/tokens,
```
http://localhost:5173/api/vblog/v1/tokens --> http://localhost:8080/vblog/v1/tokens
```
移除baseURL配置
```js
export const client = axios.create({
baseURL: '',
timeout: 3000,
})
```
### API 访问报错处理
添加一个响应拦截器来进行响应的处理
```js
// http 客户端配置一个拦截器
client.interceptors.response.use(
// 请求成功的拦截器
(value) => {
return value
},
// 请求失败
(error) => {
let msg = error.message
try {
msg = error.response.data.message
} catch (error) {
console.log(error)
}
Message.error(msg)
},
)
```
## 路由嵌套与布局
https://router.vuejs.org/zh/guide/essentials/nested-routes.html
```js
const routes = [
{
path: '/user/:id',
component: User,
children: [
{
// 当 /user/:id/profile 匹配成功
// UserProfile 将被渲染到 User 的 <router-view> 内部
path: 'profile',
component: UserProfile,
},
{
// 当 /user/:id/posts 匹配成功
// UserPosts 将被渲染到 User 的 <router-view> 内部
path: 'posts',
component: UserPosts,
},
],
},
]
```
```js
// <a-menu-item key="backend_blog_list">文章管理</a-menu-item>
// <a-menu-item key="backend_tag_list">标签管理</a-menu-item>
const handleMenuItemClick = (key) => {
// https://router.vuejs.org/zh/guide/essentials/navigation.html
router.push({ name: key })
}
```
## 登录与退出
```js
const summitLoading = ref(false)
const handleSubmit = async (data) => {
if (data.errors === undefined) {
try {
summitLoading.value = true
const tk = await LOGIN(data.values)
// 保存当前用户的登录状态
token.value.access_token = tk.access_token
token.value.ref_user_name = tk.ref_user_name
token.value.refresh_token = tk.refresh_token
// 需要把用户重定向到Home页面去了
router.push({ name: 'blog_management' })
} catch (error) {
console.log(error)
} finally {
summitLoading.value = false
}
}
}
```
```js
const logout = () => {
// 需要调用Logout接口的, 自己添加下退出接口
// 之前的登录状态给清理掉, store
token.value = undefined
// 需要重定向到登录页面
router.push({ name: 'login' })
}
```
## 导航守卫
https://router.vuejs.org/zh/guide/advanced/navigation-guards.html
```js
const router = createRouter({ ... })
router.beforeEach((to, from) => {
// ...
// 返回 false 以取消导航
return false
})
```
```js
router.beforeEach((to, from) => {
const whiteList = ['login']
if (!whiteList.includes(to.name)) {
// 需要做权限判断
if (isLogin()) {
return true
}
// 返回跳转后的页面
return { name: 'login' }
}
})
```
## 覆盖UI的默认样式
css, :deep(), 用于选择需要覆盖的class进行覆盖
:deep 选择器并不是一个通用的 CSS 选择器,而是特定于某些框架(如 Vue.js 和 Svelte中用于 Scoped CSS 的一个特性。它的主要目的是允许开发者在使用 Scoped CSS 的情况下,能够针对子组件或外部组件的样式进行覆盖。
```js
<style lang="css" scoped>
:deep(.page-header .arco-page-header-wrapper) {
padding-left: 0px;
}
```
## 列表页
- 页头/过滤条件/表格展示
后端分页, 默认的Table使用的前端分页
```html
<div class="pagination">
<a-pagination
:current="blogQueryRequest.page_number"
:page-size="blogQueryRequest.page_size"
:page-size-options="[2, 10, 20, 30, 50]"
@change="pageNumberChanged"
@page-size-change="pageSizeChanged"
:total="data.total"
show-total
show-jumper
show-page-size
/>
</div>
```
```js
// 实现分页
const pageNumberChanged = (current) => {
blogQueryRequest.page_number = current
// 基于新的变化重新请求数据
queryData()
}
const pageSizeChanged = (pageSize) => {
blogQueryRequest.page_size = pageSize
blogQueryRequest.page_number = 1
// 基于新的变化重新请求数据
queryData()
}
```
## Menu状态保存
```js
import { useStorage } from '@vueuse/core'
export const systemConfig = useStorage(
'system_config',
{
selected_current_menu_item_key: '',
},
localStorage,
{
mergeDefaults: true,
},
)
```
点击时,保存这个状态
```js
const handleMenuItemClick = (key) => {
// https://router.vuejs.org/zh/guide/essentials/navigation.html
router.push({ name: key })
systemConfig.value.selected_current_menu_item_key = key
}
```
```html
<a-menu
@menu-item-click="handleMenuItemClick"
breakpoint="xl"
:default-selected-keys="[systemConfig.selected_current_menu_item_key]"
auto-open
>
</a-menu>
```
## 文章删除
1. 准备好你的后端接口:
```go
// DeleteBlog implements blog.Service.
func (b *BlogServiceImpl) DeleteBlog(ctx context.Context, in *blog.DeleteBlogRequest) error {
ins, err := b.DescribeBlog(ctx, blog.NewDescribeBlogRequest(in.Id))
if err != nil {
return err
}
return datasource.DBFromCtx(ctx).Where("id = ?", ins.Id).Delete(ins).Error
}
// DescribeBlog implements blog.Service.
func (b *BlogServiceImpl) DescribeBlog(ctx context.Context, in *blog.DescribeBlogRequest) (*blog.Blog, error) {
ins := &blog.Blog{}
err := datasource.DBFromCtx(ctx).Where("id = ?", in.Id).First(ins).Error
if err != nil {
return nil, err
}
return ins, nil
}
```
```go
r.DELETE(":id", h.DeleteBlog)
func (h *BlogApiHandler) DeleteBlog(ctx *gin.Context) {
in := blog.NewDeleteBlogRequest(ctx.Param("id"))
err := h.blog.DeleteBlog(ctx.Request.Context(), in)
if err != nil {
response.Failed(ctx, err)
return
}
response.Success(ctx, "ok")
}
```
2. SetCookie的问题: localhost
```go
// 打印下日志, ioc
domain := application.Get().Domain()
log.L().Debug().Msgf("cookie domain: %s", domain)
// 设置Cookie
ctx.SetCookie(token.COOKIE_NAME, ins.AccessToken, ins.AccessTokenExpireTTL(), "/", domain, false, true)
```
```toml
[app]
name = "vblog"
description = "app desc"
address = "http://localhost/"
```
3. 调用API, 实现删除, 刷新页面(2个操作的先后关系)
```js
// 删除函数
const delteLoadding = ref(0)
const deleteBlog = async (id) => {
try {
delteLoadding.value = id
// 等待这个操作执行完成
await DELTE_BLOG(id)
// 刷新数据
queryData()
} finally {
delteLoadding.value = 0
}
}
```
4. 删除按钮Loadding控制:
```html
<a-button :loading="delteLoadding === record.id" status="danger" @click="deleteBlog(record.id)">
<template #icon>
<icon-delete />
</template>
删除
</a-button>
```
## 文章删除提醒
```vue
<a-popconfirm
:okLoading="delteLoadding === record.id"
@ok="deleteBlog(record.id)"
type="warning"
:content="`是否需要删除【${record.title}】?`"
>
<a-button :loading="delteLoadding === record.id" status="danger">
<template #icon>
<icon-delete />
</template>
删除
</a-button>
</a-popconfirm>
```
## 文章的创建
选择一个mardown的编辑器