26 KiB
26 KiB
校园活动组织与报名系统 - 前端开发规范
1. 命名规范
1.1 文件命名
| 类型 | 规范 | 示例 |
|---|---|---|
| Vue 组件 | PascalCase | ActivityCard.vue, NavBar.vue |
| TypeScript 文件 | camelCase | useAuth.ts, request.ts |
| 样式文件 | kebab-case | global.scss, variables.scss |
| 常量文件 | camelCase | constant.ts |
| 类型定义文件 | kebab-case.d.ts | user.d.ts, activity.d.ts |
1.2 变量命名
// 常量:全大写下划线分隔
const API_BASE_URL = '/api/v1'
const MAX_FILE_SIZE = 1024 * 1024 * 5
// 变量:小驼峰
const userName = 'zhangsan'
const activityList = []
// 布尔值:is/has/can 前缀
const isLoading = false
const hasPermission = true
const canEdit = false
// 私有属性:下划线前缀
const _privateData = {}
// 组件 props:小驼峰
const props = defineProps<{
activityId: number
showDetail: boolean
}>()
// 事件名:on 前缀
const emit = defineEmits<{
onSubmit: [data: FormData]
onCancel: []
}>()
1.3 函数命名
// 获取数据:get 前缀
function getActivityList() {}
function getUserInfo() {}
// 设置数据:set 前缀
function setToken(token: string) {}
// 处理事件:handle 前缀
function handleSubmit() {}
function handleClick() {}
// 判断逻辑:is/has/can 前缀
function isAdmin() {}
function hasRegistered() {}
function canCheckIn() {}
// 格式化:format 前缀
function formatDate(date: Date) {}
function formatPrice(price: number) {}
// 转换:to 前缀
function toArray(data: any) {}
function toString(value: any) {}
// 异步操作:async/await
async function fetchActivities() {}
async function submitRegistration() {}
1.4 CSS 类命名 (BEM 规范)
// Block__Element--Modifier
// 活动卡片
.activity-card {
// Element
&__header {}
&__title {}
&__content {}
&__footer {}
&__image {}
// Modifier
&--active {}
&--disabled {}
&--loading {}
}
// 示例
<div class="activity-card activity-card--active">
<div class="activity-card__header">
<h3 class="activity-card__title">活动标题</h3>
</div>
<div class="activity-card__content">...</div>
</div>
1.5 路由命名
// 路由 name:PascalCase
{
path: '/activities',
name: 'ActivityList',
component: () => import('@/views/activity/List.vue')
}
// 路由 path:kebab-case
{
path: '/my/registrations',
name: 'MyRegistrations',
component: () => import('@/views/registration/MyList.vue')
}
2. 代码风格规范
2.1 Vue 组件结构
<template>
<!-- 模板内容 -->
</template>
<script setup lang="ts">
// 1. 类型导入
import type { Activity } from '@/types/activity'
// 2. 组件导入
import ActivityCard from '@/components/activity/ActivityCard.vue'
// 3. 工具/API 导入
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { getActivityList } from '@/api/activity'
// 4. Props 定义
const props = defineProps<{
activityId: number
}>()
// 5. Emits 定义
const emit = defineEmits<{
onUpdate: [data: Activity]
}>()
// 6. 响应式数据
const loading = ref(false)
const activities = ref<Activity[]>([])
// 7. 计算属性
const filteredActivities = computed(() => {
return activities.value.filter(item => item.status === 1)
})
// 8. 方法定义
function handleRefresh() {
// ...
}
async function fetchData() {
// ...
}
// 9. 生命周期
onMounted(() => {
fetchData()
})
// 10. Watch(如需要)
watch(
() => props.activityId,
(newVal) => {
// ...
}
)
</script>
<style lang="scss" scoped>
// 样式
</style>
2.2 TypeScript 规范
// 1. 明确类型声明,避免使用 any
// Bad
const data: any = {}
// Good
interface ActivityData {
id: number
title: string
}
const data: ActivityData = { id: 1, title: '活动' }
// 2. 使用 interface 定义对象类型
interface User {
id: number
username: string
name: string
role: 0 | 1
}
// 3. 使用 type 定义联合类型
type ActivityStatus = 0 | 1 | 2 | 3
type RequestMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
// 4. 使用枚举定义常量集合
enum ActivityStatusEnum {
PENDING = 0, // 未开始
OPEN = 1, // 报名中
ONGOING = 2, // 进行中
ENDED = 3 // 已结束
}
// 5. 泛型使用
interface ApiResponse<T> {
code: number
message: string
data: T
timestamp: number
}
// 6. 可选属性使用 ?
interface CreateActivityParams {
title: string
description?: string
coverImage?: string
}
// 7. 只读属性使用 readonly
interface Config {
readonly apiUrl: string
readonly timeout: number
}
2.3 API 请求规范
// src/api/activity.ts
import request from '@/utils/request'
import type { Activity, ActivityListParams, CreateActivityParams } from '@/types/activity'
import type { ApiResponse, PageResult } from '@/types/api'
/**
* 获取活动列表
* @param params 查询参数
*/
export function getActivityList(params: ActivityListParams) {
return request.get<ApiResponse<PageResult<Activity>>>('/activities', { params })
}
/**
* 获取活动详情
* @param id 活动ID
*/
export function getActivityDetail(id: number) {
return request.get<ApiResponse<Activity>>(`/activities/${id}`)
}
/**
* 创建活动(管理员)
* @param data 活动数据
*/
export function createActivity(data: CreateActivityParams) {
return request.post<ApiResponse<{ id: number }>>('/activities', data)
}
/**
* 更新活动(管理员)
* @param id 活动ID
* @param data 更新数据
*/
export function updateActivity(id: number, data: Partial<CreateActivityParams>) {
return request.put<ApiResponse<null>>(`/activities/${id}`, data)
}
/**
* 删除活动(管理员)
* @param id 活动ID
*/
export function deleteActivity(id: number) {
return request.delete<ApiResponse<null>>(`/activities/${id}`)
}
2.4 Store 规范
// src/stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { UserInfo } from '@/types/user'
import { login, getUserInfo, refreshToken } from '@/api/auth'
export const useUserStore = defineStore('user', () => {
// State
const token = ref<string | null>(localStorage.getItem('accessToken'))
const refreshTokenValue = ref<string | null>(localStorage.getItem('refreshToken'))
const userInfo = ref<UserInfo | null>(null)
// Getters
const isLoggedIn = computed(() => !!token.value)
const isAdmin = computed(() => userInfo.value?.role === 1)
const userName = computed(() => userInfo.value?.name || '')
// Actions
async function loginAction(username: string, password: string) {
try {
const res = await login({ username, password })
token.value = res.data.accessToken
refreshTokenValue.value = res.data.refreshToken
userInfo.value = res.data.userInfo
localStorage.setItem('accessToken', res.data.accessToken)
localStorage.setItem('refreshToken', res.data.refreshToken)
return true
} catch (error) {
return false
}
}
function logout() {
token.value = null
refreshTokenValue.value = null
userInfo.value = null
localStorage.removeItem('accessToken')
localStorage.removeItem('refreshToken')
}
async function fetchUserInfo() {
try {
const res = await getUserInfo()
userInfo.value = res.data
} catch (error) {
console.error('获取用户信息失败', error)
}
}
return {
// State
token,
userInfo,
// Getters
isLoggedIn,
isAdmin,
userName,
// Actions
loginAction,
logout,
fetchUserInfo
}
})
2.5 Composable 规范
// src/composables/usePagination.ts
import { ref, computed } from 'vue'
interface PaginationOptions {
defaultPage?: number
defaultSize?: number
}
export function usePagination(options: PaginationOptions = {}) {
const { defaultPage = 1, defaultSize = 10 } = options
const currentPage = ref(defaultPage)
const pageSize = ref(defaultSize)
const total = ref(0)
const totalPages = computed(() => Math.ceil(total.value / pageSize.value))
const hasMore = computed(() => currentPage.value < totalPages.value)
function setTotal(value: number) {
total.value = value
}
function nextPage() {
if (hasMore.value) {
currentPage.value++
}
}
function prevPage() {
if (currentPage.value > 1) {
currentPage.value--
}
}
function reset() {
currentPage.value = defaultPage
total.value = 0
}
return {
currentPage,
pageSize,
total,
totalPages,
hasMore,
setTotal,
nextPage,
prevPage,
reset
}
}
3. 组件开发规范
3.1 组件分类
| 类型 | 位置 | 说明 |
|---|---|---|
| 基础组件 | components/common/ | 通用 UI 组件,如 NavBar、TabBar |
| 业务组件 | components/[module]/ | 特定业务模块组件 |
| 页面组件 | views/[module]/ | 路由对应的页面组件 |
| 布局组件 | layouts/ | 页面布局组件 |
3.2 组件设计原则
<!-- 1. 单一职责:一个组件只做一件事 -->
<!-- Bad:ActivityCard 既显示又能编辑 -->
<!-- Good:分成 ActivityCard(显示)和 ActivityEditForm(编辑)-->
<!-- 2. Props 向下,Events 向上 -->
<template>
<ActivityCard
:activity="activity"
@click="handleClick"
@register="handleRegister"
/>
</template>
<!-- 3. 使用 v-model 实现双向绑定 -->
<template>
<SearchInput v-model="keyword" />
</template>
<!-- SearchInput.vue -->
<script setup lang="ts">
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
function handleInput(e: Event) {
emit('update:modelValue', (e.target as HTMLInputElement).value)
}
</script>
<!-- 4. 使用插槽提供扩展性 -->
<template>
<Card>
<template #header>
<slot name="header" />
</template>
<slot />
<template #footer>
<slot name="footer" />
</template>
</Card>
</template>
3.3 组件 Props 规范
// 1. 使用 TypeScript 定义 Props
interface Props {
// 必填属性
activityId: number
// 可选属性,带默认值
showImage?: boolean
// 复杂类型
activity: Activity
}
const props = withDefaults(defineProps<Props>(), {
showImage: true
})
// 2. Props 校验(可选,TS 已提供类型检查)
const props = defineProps({
activityId: {
type: Number,
required: true
},
status: {
type: Number,
default: 0,
validator: (value: number) => [0, 1, 2, 3].includes(value)
}
})
3.4 组件样式规范
<style lang="scss" scoped>
// 1. 使用 scoped 避免样式污染
// 2. 使用 SCSS 变量
@import '@/assets/styles/variables.scss';
.activity-card {
background-color: $white;
border-radius: $border-radius-lg;
// 3. 使用 BEM 命名
&__title {
font-size: $font-size-lg;
color: $text-primary;
}
// 4. 响应式样式使用 mixin
@include respond-to('mobile') {
padding: $spacing-sm;
}
}
// 5. 深度选择器修改子组件样式
:deep(.van-button) {
border-radius: $border-radius-md;
}
</style>
4. 类型定义规范
4.1 API 响应类型
// src/types/api.d.ts
// 统一响应格式
interface ApiResponse<T = any> {
code: number
message: string
data: T
timestamp: number
}
// 分页响应
interface PageResult<T> {
records: T[]
total: number
pages: number
current: number
size: number
}
// 分页请求参数
interface PageParams {
page?: number
size?: number
}
4.2 业务类型定义
// src/types/user.d.ts
interface UserInfo {
id: number
username: string
name: string
studentId: string | null
email: string | null
phone: string | null
avatar: string | null
role: 0 | 1 // 0-学生,1-管理员
}
interface LoginRequest {
username: string
password: string
}
interface LoginResponse {
accessToken: string
refreshToken: string
expiresIn: number
tokenType: string
userInfo: UserInfo
}
// src/types/activity.d.ts
interface Activity {
id: number
title: string
description: string
coverImage: string | null
startTime: string
endTime: string
registrationDeadline: string | null
location: string
maxParticipants: number
currentParticipants: number
status: ActivityStatus
category: string | null
adminId: number
adminName?: string
averageRating?: number
reviewCount?: number
isRegistered?: boolean
createdAt: string
}
type ActivityStatus = 0 | 1 | 2 | 3
interface ActivityListParams extends PageParams {
status?: ActivityStatus
keyword?: string
category?: string
startDate?: string
endDate?: string
}
interface CreateActivityParams {
title: string
description?: string
coverImage?: string
startTime: string
endTime: string
registrationDeadline?: string
location: string
maxParticipants: number
category?: string
}
// src/types/registration.d.ts
interface Registration {
id: number
activityId: number
activityTitle: string
activityStartTime: string
activityLocation: string
ticketCode: string
ticketPdfUrl: string
status: RegistrationStatus
createdAt: string
}
type RegistrationStatus = 0 | 1 | 2 // 0-已取消,1-已报名,2-已签到
// src/types/review.d.ts
interface Review {
id: number
userId: number
userName: string
userAvatar: string | null
activityId: number
rating: 1 | 2 | 3 | 4 | 5
content: string
createdAt: string
}
interface CreateReviewParams {
activityId: number
rating: number
content: string
}
5. 错误处理规范
5.1 API 错误处理
// src/utils/request.ts
import axios from 'axios'
import { showToast, showDialog } from 'vant'
import router from '@/router'
import { useUserStore } from '@/stores/user'
const instance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000
})
// 响应拦截器
instance.interceptors.response.use(
response => {
const { code, message, data } = response.data
// 业务成功
if (code === 200) {
return response.data
}
// 业务错误
showToast(message || '操作失败')
return Promise.reject(new Error(message))
},
error => {
const { response } = error
if (response) {
const { status, data } = response
switch (status) {
case 400:
showToast(data.message || '请求参数错误')
break
case 401:
// Token 过期,清除登录状态,跳转登录页
const userStore = useUserStore()
userStore.logout()
router.push('/login')
showToast('登录已过期,请重新登录')
break
case 403:
showToast('无权限访问')
break
case 404:
showToast('请求的资源不存在')
break
case 409:
// 业务冲突,显示具体原因
showToast(data.message || '操作冲突')
break
case 500:
showToast('服务器异常,请稍后重试')
break
default:
showToast('网络异常')
}
} else {
showToast('网络连接失败')
}
return Promise.reject(error)
}
)
5.2 组件错误处理
<script setup lang="ts">
import { ref } from 'vue'
import { showToast, showLoadingToast, closeToast } from 'vant'
import { registerActivity } from '@/api/registration'
const loading = ref(false)
async function handleRegister() {
if (loading.value) return
try {
loading.value = true
showLoadingToast({ message: '报名中...', forbidClick: true })
await registerActivity({ activityId: props.activityId })
showToast('报名成功')
emit('success')
} catch (error: any) {
// 错误已在拦截器处理,这里可以做额外处理
console.error('报名失败:', error)
} finally {
loading.value = false
closeToast()
}
}
</script>
5.3 表单校验
<template>
<van-form @submit="handleSubmit" @failed="handleFailed">
<van-cell-group inset>
<van-field
v-model="form.username"
name="username"
label="用户名"
placeholder="请输入用户名"
:rules="[
{ required: true, message: '请输入用户名' },
{ pattern: /^[a-zA-Z0-9_]{3,20}$/, message: '用户名格式不正确' }
]"
/>
<van-field
v-model="form.password"
type="password"
name="password"
label="密码"
placeholder="请输入密码"
:rules="[
{ required: true, message: '请输入密码' },
{ validator: validatePassword, message: '密码至少6位' }
]"
/>
</van-cell-group>
<div class="submit-btn">
<van-button round block type="primary" native-type="submit">
提交
</van-button>
</div>
</van-form>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import { showToast } from 'vant'
const form = reactive({
username: '',
password: ''
})
// 自定义校验函数
function validatePassword(value: string) {
return value.length >= 6
}
function handleSubmit() {
// 校验通过
console.log('提交:', form)
}
function handleFailed(errorInfo: any) {
// 校验失败
showToast(errorInfo.errors[0].message)
}
</script>
6. 注释规范
6.1 文件头注释
/**
* @file 活动相关 API 接口
* @author Your Name
* @description 包含活动的增删改查、日历视图、冲突检测等接口
*/
6.2 函数注释
/**
* 格式化日期时间
* @param date - 日期字符串或 Date 对象
* @param format - 格式化模板,默认 'YYYY-MM-DD HH:mm'
* @returns 格式化后的日期字符串
* @example
* formatDateTime('2025-06-01T09:00:00') // '2025-06-01 09:00'
* formatDateTime(new Date(), 'YYYY年MM月DD日') // '2025年06月01日'
*/
export function formatDateTime(
date: string | Date,
format: string = 'YYYY-MM-DD HH:mm'
): string {
return dayjs(date).format(format)
}
6.3 组件注释
<!--
ActivityCard 活动卡片组件
用于展示活动的基本信息,包括封面、标题、时间、地点、状态等
@prop {Activity} activity - 活动数据
@prop {boolean} showImage - 是否显示封面图,默认 true
@emit click - 点击卡片时触发
@emit register - 点击报名按钮时触发
@example
<ActivityCard
:activity="activity"
@click="handleClick"
@register="handleRegister"
/>
-->
6.4 复杂逻辑注释
async function handleRegister() {
// 1. 检查是否已登录
if (!userStore.isLoggedIn) {
router.push('/login')
return
}
// 2. 检查报名状态
// - 已报名:提示已报名
// - 已满员:提示名额已满
// - 已截止:提示报名已截止
if (activity.value.isRegistered) {
showToast('您已报名该活动')
return
}
// 3. 发起报名请求
// ...
}
7. Git 提交规范
7.1 提交信息格式
<type>(<scope>): <subject>
<body>
<footer>
7.2 Type 类型
| Type | 说明 |
|---|---|
| feat | 新功能 |
| fix | 修复 Bug |
| docs | 文档更新 |
| style | 代码格式(不影响功能) |
| refactor | 重构 |
| perf | 性能优化 |
| test | 测试相关 |
| build | 构建相关 |
| ci | CI 配置 |
| chore | 其他杂项 |
7.3 提交示例
# 新功能
feat(activity): 添加活动日历视图
# Bug 修复
fix(auth): 修复 Token 刷新失败的问题
# 文档
docs(readme): 更新项目启动说明
# 代码格式
style(components): 统一组件代码缩进
# 重构
refactor(api): 重构请求拦截器逻辑
7.4 分支命名
| 分支类型 | 命名格式 | 示例 |
|---|---|---|
| 主分支 | main | main |
| 开发分支 | develop | develop |
| 功能分支 | feature/xxx | feature/activity-calendar |
| 修复分支 | fix/xxx | fix/login-bug |
| 发布分支 | release/x.x.x | release/1.0.0 |
8. 性能优化规范
8.1 图片优化
<!-- 使用 Vant 的图片懒加载 -->
<van-image
lazy-load
:src="activity.coverImage"
fit="cover"
width="100%"
height="150"
/>
8.2 路由懒加载
// src/router/routes.ts
export const routes = [
{
path: '/activities',
name: 'ActivityList',
component: () => import('@/views/activity/List.vue')
},
{
path: '/activities/:id',
name: 'ActivityDetail',
component: () => import('@/views/activity/Detail.vue')
}
]
8.3 组件按需导入
// src/main.ts
// Vant 按需导入
import {
Button,
Cell,
CellGroup,
Form,
Field,
NavBar,
Tabbar,
TabbarItem
} from 'vant'
app.use(Button)
app.use(Cell)
app.use(CellGroup)
// ...
// 或使用自动导入插件
// vite.config.ts
import Components from 'unplugin-vue-components/vite'
import { VantResolver } from 'unplugin-vue-components/resolvers'
export default {
plugins: [
Components({
resolvers: [VantResolver()]
})
]
}
8.4 列表性能优化
<template>
<!-- 使用 v-for 的 key -->
<div v-for="item in list" :key="item.id">
{{ item.title }}
</div>
<!-- 长列表使用虚拟滚动 -->
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<ActivityCard
v-for="activity in activities"
:key="activity.id"
:activity="activity"
/>
</van-list>
</template>
8.5 缓存优化
// 使用 keep-alive 缓存页面
<router-view v-slot="{ Component }">
<keep-alive :include="['ActivityList', 'MyRegistrations']">
<component :is="Component" />
</keep-alive>
</router-view>
9. 安全规范
9.1 XSS 防护
<!-- Bad: 直接渲染 HTML -->
<div v-html="userInput"></div>
<!-- Good: 使用文本插值,Vue 会自动转义 -->
<div>{{ userInput }}</div>
<!-- 如必须渲染 HTML,先进行转义 -->
<div v-html="sanitizedHtml"></div>
<script setup>
import DOMPurify from 'dompurify'
const sanitizedHtml = computed(() => {
return DOMPurify.sanitize(rawHtml.value)
})
</script>
9.2 敏感信息处理
// 不要在前端存储敏感信息明文
// Bad
localStorage.setItem('password', password)
// 只存储 Token
localStorage.setItem('accessToken', token)
// Token 过期时间较短(2小时)
// 使用 Refresh Token 刷新
9.3 请求安全
// 1. 所有 API 请求都要携带 Token
// 2. 敏感操作需要二次确认
showDialog({
title: '确认删除',
message: '删除后无法恢复,确定要删除吗?',
showCancelButton: true
}).then(() => {
// 用户确认后执行删除
deleteActivity(id)
})
// 3. 防止重复提交
const submitting = ref(false)
async function handleSubmit() {
if (submitting.value) return
submitting.value = true
try {
await submitData()
} finally {
submitting.value = false
}
}
10. 测试规范
10.1 单元测试
// src/utils/__tests__/format.test.ts
import { describe, it, expect } from 'vitest'
import { formatDateTime, formatActivityStatus } from '../format'
describe('formatDateTime', () => {
it('should format date string correctly', () => {
const result = formatDateTime('2025-06-01T09:00:00')
expect(result).toBe('2025-06-01 09:00')
})
it('should support custom format', () => {
const result = formatDateTime('2025-06-01', 'YYYY年MM月DD日')
expect(result).toBe('2025年06月01日')
})
})
describe('formatActivityStatus', () => {
it('should return correct status text', () => {
expect(formatActivityStatus(0)).toBe('未开始')
expect(formatActivityStatus(1)).toBe('报名中')
expect(formatActivityStatus(2)).toBe('进行中')
expect(formatActivityStatus(3)).toBe('已结束')
})
})
10.2 组件测试
// src/components/__tests__/ActivityCard.test.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import ActivityCard from '../activity/ActivityCard.vue'
const mockActivity = {
id: 1,
title: '测试活动',
startTime: '2025-06-01 09:00:00',
location: '体育馆',
status: 1
}
describe('ActivityCard', () => {
it('should render activity title', () => {
const wrapper = mount(ActivityCard, {
props: { activity: mockActivity }
})
expect(wrapper.text()).toContain('测试活动')
})
it('should emit click event', async () => {
const wrapper = mount(ActivityCard, {
props: { activity: mockActivity }
})
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toBeTruthy()
})
})
附录:常用代码片段
A. Vant 表单模板
<template>
<van-form @submit="handleSubmit">
<van-cell-group inset>
<van-field
v-model="form.title"
name="title"
label="标题"
placeholder="请输入标题"
:rules="[{ required: true, message: '请输入标题' }]"
/>
<van-field
v-model="form.description"
name="description"
label="描述"
type="textarea"
placeholder="请输入描述"
rows="3"
autosize
/>
</van-cell-group>
<div style="margin: 16px">
<van-button round block type="primary" native-type="submit">
提交
</van-button>
</div>
</van-form>
</template>
B. 下拉刷新 + 加载更多
<template>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<div v-for="item in list" :key="item.id">
{{ item.title }}
</div>
</van-list>
</van-pull-refresh>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const list = ref([])
const loading = ref(false)
const finished = ref(false)
const refreshing = ref(false)
const page = ref(1)
async function onLoad() {
const res = await fetchList({ page: page.value })
list.value.push(...res.data.records)
loading.value = false
if (list.value.length >= res.data.total) {
finished.value = true
} else {
page.value++
}
}
async function onRefresh() {
page.value = 1
list.value = []
finished.value = false
await onLoad()
refreshing.value = false
}
</script>
C. 弹出确认框
import { showConfirmDialog, showToast } from 'vant'
async function handleDelete(id: number) {
try {
await showConfirmDialog({
title: '确认删除',
message: '删除后无法恢复,确定要删除吗?'
})
await deleteItem(id)
showToast('删除成功')
fetchList()
} catch {
// 用户取消
}
}