前端开发指南
前端开发指南
本文介绍如何参与 ModelGate 前端开发。
开发环境准备
环境要求
- Node.js >= 18.12.0
- pnpm >= 8.7.0
安装依赖
cd modelgate-web
pnpm install配置环境变量
创建 .env.development 文件:
# 后端 API 地址
VITE_API_URL=http://localhost:8889
# 其他配置...启动开发服务器
pnpm dev访问 http://localhost:5173
开发命令
# 启动开发服务器
pnpm dev
# 构建生产版本
pnpm build
# 预览生产构建
pnpm preview
# 代码检查
pnpm lint
# 代码格式化
pnpm format
# 类型检查
pnpm typecheck添加新页面
1. 创建页面组件
在 src/views/ 下创建页面:
<!-- src/views/new-page/index.vue -->
<template>
<div class="new-page">
<h1>{{ t('newPage.title') }}</h1>
<!-- 页面内容 -->
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<style scoped>
.new-page {
padding: 20px;
}
</style>2. 添加路由配置
在 src/router/routes/ 下添加路由:
// src/router/routes/modules/new-page.ts
import type { RouteRecordRaw } from 'vue-router'
const route: RouteRecordRaw = {
path: '/new-page',
name: 'new_page',
component: () => import('@/views/new-page/index.vue'),
meta: {
title: '新页面',
i18nKey: 'route.newPage'
}
}
export default route3. 添加国际化文本
// src/locales/zh-cn.ts
export default {
route: {
newPage: '新页面'
},
newPage: {
title: '新页面标题'
}
}添加新组件
1. 创建组件
<!-- src/components/custom/MyComponent.vue -->
<template>
<div class="my-component">
<slot />
</div>
</template>
<script setup lang="ts">
interface Props {
title?: string
}
withDefaults(defineProps<Props>(), {
title: 'Default Title'
})
</script>
<style scoped>
.my-component {
/* 样式 */
}
</style>2. 使用组件
<template>
<MyComponent title="Custom Title">
内容
</MyComponent>
</template>
<script setup lang="ts">
import MyComponent from '@/components/custom/MyComponent.vue'
</script>API 调用
定义 API 接口
// src/api/user.ts
import { client } from '@/service/connect'
import type { User } from '@/types/user'
export async function fetchUsers(): Promise<User[]> {
const response = await client.getUsers({})
return response.users || []
}
export async function createUser(data: CreateUserRequest): Promise<User> {
const response = await client.createUser(data)
return response.user || null
}
export async function updateUser(id: string, data: UpdateUserRequest): Promise<User> {
const response = await client.updateUser({ id, ...data })
return response.user || null
}
export async function deleteUser(id: string): Promise<void> {
await client.deleteUser({ id })
}在组件中使用
<template>
<div>
<n-button @click="loadUsers">加载用户</n-button>
<n-list v-if="users.length">
<n-list-item v-for="user in users" :key="user.id">
{{ user.username }}
</n-list-item>
</n-list>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { fetchUsers } from '@/api/user'
import type { User } from '@/types/user'
const users = ref<User[]>([])
const loading = ref(false)
async function loadUsers() {
loading.value = true
try {
users.value = await fetchUsers()
} catch (error) {
console.error('Failed to fetch users:', error)
} finally {
loading.value = false
}
}
</script>状态管理
创建 Store
// src/stores/my-store.ts
import { defineStore } from 'pinia'
export const useMyStore = defineStore('myStore', {
state: () => ({
data: [],
loading: false
}),
getters: {
dataCount: (state) => state.data.length
},
actions: {
async fetchData() {
this.loading = true
try {
const response = await api.fetchData()
this.data = response
} finally {
this.loading = false
}
}
}
})使用 Store
<script setup lang="ts">
import { useMyStore } from '@/stores/my-store'
const myStore = useMyStore()
// 访问 state
console.log(myStore.data)
// 访问 getters
console.log(myStore.dataCount)
// 调用 actions
myStore.fetchData()
</script>表格组件使用
ModelGate 使用 NaiveUI 的表格组件:
<template>
<n-data-table
:columns="columns"
:data="dataSource"
:loading="loading"
:pagination="pagination"
@update:page="handlePageChange"
/>
</template>
<script setup lang="ts">
import { ref, h } from 'vue'
import { NButton, NSpace, NTag } from 'naive-ui'
import type { DataTableColumns } from 'naive-ui'
interface User {
id: number
username: string
email: string
status: 'active' | 'inactive'
}
const dataSource = ref<User[]>([])
const loading = ref(false)
const columns: DataTableColumns<User> = [
{ title: 'ID', key: 'id' },
{ title: '用户名', key: 'username' },
{ title: '邮箱', key: 'email' },
{
title: '状态',
key: 'status',
render: (row) => {
return h(NTag, {
type: row.status === 'active' ? 'success' : 'default'
}, { default: () => row.status })
}
},
{
title: '操作',
key: 'actions',
render: (row) => {
return h(NSpace, {}, {
default: () => [
h(NButton, {
size: 'small',
onClick: () => handleEdit(row)
}, { default: () => '编辑' }),
h(NButton, {
size: 'small',
type: 'error',
onClick: () => handleDelete(row)
}, { default: () => '删除' })
]
})
}
}
]
function handleEdit(row: User) {
// 编辑逻辑
}
function handleDelete(row: User) {
// 删除逻辑
}
const pagination = ref({
page: 1,
pageSize: 10,
itemCount: 0
})
function handlePageChange(page: number) {
pagination.value.page = page
// 加载数据
}
</script>表单组件
表单验证
<template>
<n-form
ref="formRef"
:model="formData"
:rules="rules"
label-placement="left"
label-width="100"
>
<n-form-item label="用户名" path="username">
<n-input v-model:value="formData.username" placeholder="请输入用户名" />
</n-form-item>
<n-form-item label="邮箱" path="email">
<n-input v-model:value="formData.email" placeholder="请输入邮箱" />
</n-form-item>
<n-form-item>
<n-space>
<n-button type="primary" @click="handleSubmit">提交</n-button>
<n-button @click="handleReset">重置</n-button>
</n-space>
</n-form-item>
</n-form>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { FormInst, FormRules } from 'naive-ui'
interface FormData {
username: string
email: string
}
const formRef = ref<FormInst | null>(null)
const formData = ref<FormData>({
username: '',
email: ''
})
const rules: FormRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '用户名长度为 3-20 个字符', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' }
]
}
async function handleSubmit() {
try {
await formRef.value?.validate()
// 提交逻辑
} catch (error) {
console.log('Validation failed:', error)
}
}
function handleReset() {
formRef.value?.restoreValidation()
formData.value = {
username: '',
email: ''
}
}
</script>构建和部署
构建
pnpm build构建产物在 dist/ 目录。
Docker 构建
# Dockerfile
FROM node:18-alpine as builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install
COPY . .
RUN pnpm build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]环境变量
生产环境变量在 .env.production 中配置:
VITE_API_URL=https://api.modelgate.com代码规范
TypeScript 规范
- 使用
interface定义对象类型 - 使用
type定义联合类型 - 避免使用
any,使用unknown代替
Vue 规范
- 使用
<script setup>语法 - 组件名使用 PascalCase
- Props 定义使用 TypeScript 类型
样式规范
- 优先使用 UnoCSS 原子类
- 复杂样式使用
<style scoped> - 全局样式放在
src/assets/styles/
常见问题
开发服务器启动失败
检查 Node.js 版本是否符合要求,清理缓存:
rm -rf node_modules .vite
pnpm installAPI 调用失败
检查 .env.development 中的 VITE_API_URL 是否正确。
类型检查失败
运行 pnpm typecheck 查看具体的类型错误。