👨💻 开发指南
l-pc-front 项目完整开发指南与最佳实践
📋 开发环境搭建
1. 环境准备
bash
# 1. 安装 Node.js (推荐使用 Volta 管理)
# 下载地址: https://volta.sh/
volta install node@18.20.8
# 2. 安装 Yarn
npm install -g yarn@1.22.22
# 3. 验证安装
node -v # v18.20.8
yarn -v # 1.22.22
# 4. 配置淘宝镜像(加速下载)
yarn config set registry https://registry.npmmirror.com/
npm config set registry https://registry.npmmirror.com/2. 项目初始化
bash
# 1. 克隆项目
git clone https://gitee.com/njy_3/l-pc-front.git
cd l-pc-front
# 2. 安装依赖
yarn install
# 或使用 npm
npm install
# 3. 验证安装
ls node_modules # 检查依赖是否完整
# 4. 启动开发服务器
yarn dev
# 或
npm run dev
# 5. 访问应用
# 打开浏览器: http://localhost:80803. 开发环境配置
bash
# 环境变量配置
cp env.example .env
# 根据需要修改 .env 文件
# 特定环境启动
yarn dev:park # 园区环境
yarn dev:audit # 稽核环境🎯 项目结构详解
1. 核心目录结构
l-pc-front/
├── public/ # 静态资源
│ ├── index.html # HTML入口
│ ├── config/ # 配置文件
│ └── ppt/ # PPT演示
├── src/
│ ├── assets/ # 静态资源 (图片、样式)
│ ├── common/ # 公共工具
│ │ ├── http.js # HTTP客户端
│ │ ├── config.js # 配置管理
│ │ ├── updateChecker.js # 更新检查
│ │ └── vueApi.js # Vue API封装
│ ├── components/ # 组件库
│ │ ├── common/ # 通用业务组件
│ │ ├── ui/ # UI组件
│ │ ├── model/ # 数据模型
│ │ └── mixin/ # 混合逻辑
│ ├── directives/ # 自定义指令
│ ├── pages/ # 页面视图
│ │ ├── lowcode/ # 低代码平台
│ │ ├── audit/ # 稽核模块
│ │ ├── health/ # 健康模块
│ │ ├── datav/ # 数据可视化
│ │ └── ... # 其他业务模块
│ ├── plugin/ # 插件系统
│ ├── router/ # 路由配置
│ │ ├── modules/ # 模块化路由
│ │ └── index.js # 路由入口
│ ├── store/ # Vuex状态管理
│ │ ├── modules/ # 状态模块
│ │ └── index.js # Store入口
│ ├── util/ # 工具函数
│ ├── vxhr/ # 个人信息管理
│ ├── App.vue # 根组件
│ └── main.js # 应用入口
├── theme/ # 主题配置
│ ├── scss/ # SCSS主题
│ └── less/ # LESS主题
├── scripts/ # 构建脚本
├── .env* # 环境变量
├── vue.config.js # Vue CLI配置
├── babel.config.js # Babel配置
├── package.json # 项目配置
└── README.md # 项目说明2. 关键文件说明
应用入口 (src/main.js)
javascript
import Vue from "vue";
import App from "./App.vue";
import router from "./router/index.js";
import store from "./store";
import ElementUI from "element-ui";
import bxPlugin from "./plugin/bx-plugin.js";
// 核心初始化
Vue.use(bxPlugin);
Vue.use(ElementUI);
VueInit();
VueUtil();
// 路由栈全局方法
Vue.prototype.$routeStack = {
goBack() { return store.dispatch('routeStack/goBack', router); },
goForward() { return store.dispatch('routeStack/goForward', router); },
// ... 其他方法
};
// 启动应用
window.app = new Vue({
el: "#app",
store,
router,
render: h => h(App),
});路由配置 (src/router/index.js)
javascript
import Vue from "vue";
import VueRouter from "vue-router";
// 导入模块化路由
import auditRoutes from "./modules/audit";
import healthRoutes from "./modules/health";
import lowcodeRoutes from "./modules/lowcode";
// 通用CRUD路由
const publicCrudRoutes = [
{ path: "/list/:service_name", name: "list", component: TabList },
{ path: "/detail/:service_name/:id", name: "detail", component: Detail },
// ... 更多路由
];
// 合并所有路由
const routes = [
...publicCrudRoutes,
...lowcodeRoutes,
...auditRoutes,
...healthRoutes,
];
Vue.use(VueRouter);
const router = new VueRouter({ routes });
// 路由守卫
router.beforeEach((to, from, next) => {
// 路由栈管理
if (store.getters['routeStack/isEnabled']) {
store.dispatch('routeStack/pushRoute', to);
}
next();
});
export default router;状态管理 (src/store/index.js)
javascript
import Vue from 'vue';
import Vuex from 'vuex';
import SrvColData from './modules/srvcol-data';
import HotTableData from './modules/hot-table-data';
import routeStack from './modules/route-stack';
Vue.use(Vuex);
export new Vuex.Store({
modules: {
SrvColData,
HotTableData,
routeStack,
// ... 其他模块
}
});💻 编码规范
1. JavaScript/TypeScript 规范
javascript
// ✅ 推荐:使用ES6+语法
const fetchData = async () => {
try {
const res = await $http.post('/api/data', req);
return res.data;
} catch (error) {
console.error('获取数据失败:', error);
throw error;
}
};
// ❌ 避免:使用回调地狱
function fetchData(callback) {
$http.post('/api/data', req, function(res) {
if (res.data.state === 'SUCCESS') {
callback(res.data.data);
}
});
}
// ✅ 推荐:解构赋值
const { data, ok, msg } = await $selectList(url, req);
// ✅ 推荐:可选链操作符
const userName = user?.profile?.name || '匿名';
// ✅ 推荐:空值合并运算符
const pageSize = options?.rownumber ?? 20;2. Vue 组件规范
vue
<template>
<div class="user-list">
<!-- 使用有意义的类名 -->
<div class="toolbar">
<el-button @click="handleAdd">新增</el-button>
</div>
<!-- 表格 -->
<el-table :data="tableData" v-loading="loading">
<el-table-column label="姓名" prop="name" />
<el-table-column label="操作">
<template #default="{ row }">
<el-button @click="handleEdit(row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
name: 'UserList', // 组件名使用大驼峰
// Props 定义
props: {
service: {
type: String,
required: true,
default: 'srvuser_list_select'
}
},
// 响应式数据
data() {
return {
tableData: [],
loading: false,
pagination: {
page: 1,
rownumber: 20,
total: 0
}
};
},
// 计算属性
computed: {
hasPermission() {
return this.$store.getters['user/hasPermission'];
}
},
// 监听器
watch: {
'pagination.page'() {
this.fetchData();
}
},
// 方法
methods: {
async fetchData() {
this.loading = true;
try {
const { data, page, ok } = await $selectList(
`/business/select/${this.service}`,
{
page: this.pagination
}
);
if (ok) {
this.tableData = data;
this.pagination.total = page.total;
}
} catch (error) {
this.$message.error('获取数据失败');
} finally {
this.loading = false;
}
},
handleAdd() {
this.$router.push({
path: `/add/${this.service}`
});
},
handleEdit(row) {
this.$router.push({
path: `/update/${this.service}/${row.id}`
});
}
},
// 生命周期
mounted() {
this.fetchData();
}
};
</script>
<style scoped lang="scss">
.user-list {
padding: 20px;
.toolbar {
margin-bottom: 16px;
display: flex;
justify-content: space-between;
}
.el-table {
border-radius: 4px;
overflow: hidden;
}
}
</style>3. 样式规范
scss
// ✅ 推荐:使用 SCSS,模块化样式
.container {
// 变量定义
$primary-color: #409eff;
// 嵌套规则
.header {
background: $primary-color;
padding: 16px;
.title {
font-size: 20px;
font-weight: bold;
}
}
// 混合宏
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.content {
@include flex-center;
min-height: 400px;
}
}
// ✅ 推荐:使用 CSS 变量
:root {
--primary-color: #409eff;
--bg-color: #f5f7fa;
}
.dark-mode {
--primary-color: #66b1ff;
--bg-color: #1a1a1a;
}
// ❌ 避免:过深的嵌套
.container {
.header {
.nav {
.menu {
.item { // 4层嵌套,太深
// ...
}
}
}
}
}🔄 开发流程
1. 功能开发流程
bash
# 1. 创建功能分支
git checkout -b feature/user-management
# 2. 开发功能
# - 创建组件
# - 添加路由
# - 实现业务逻辑
# - 编写测试
# 3. 代码检查
yarn lint
# 修复所有 ESLint 错误
# 4. 本地测试
yarn dev
# 测试所有功能点
# 5. 提交代码
git add .
git commit -m "feat: 添加用户管理功能"
# 6. 推送分支
git push origin feature/user-management
# 7. 创建 Pull Request
# 在 Gitee/GitHub 上创建 PR2. 组件开发流程
javascript
// 步骤1: 确定组件职责
// 组件名称: UserSelector
// 功能: 用户选择器,支持搜索和多选
// 步骤2: 定义 Props
const props = {
// 是否多选
multiple: {
type: Boolean,
default: false
},
// 已选用户ID
value: {
type: [String, Array],
default: ''
},
// 过滤条件
filter: {
type: Object,
default: () => ({})
}
};
// 步骤3: 实现核心逻辑
methods: {
async fetchUsers() {
const { data, ok } = await $selectList('/api/users', {
condition: this.filterCondition,
page: { pageNo: 1, rownumber: 50 }
});
if (ok) {
this.userList = data;
}
},
handleSelect(selected) {
this.$emit('input', selected);
this.$emit('change', selected);
}
}
// 步骤4: 导出组件
export default {
name: 'UserSelector',
props,
methods
}3. 接口对接流程
javascript
// 步骤1: 查看API文档
// 目标: 获取用户列表
// 接口: /business/select/srvuser_list_select
// 步骤2: 定义请求参数
const req = {
serviceName: "srvuser_list_select",
colNames: ["*"],
condition: [
{
colName: "status",
ruleType: "eq",
value: "active"
}
],
page: {
pageNo: 1,
rownumber: 20
}
};
// 步骤3: 调用接口
const fetchData = async () => {
const { data, page, ok } = await $selectList(
"/business/select/srvuser_list_select",
req
);
if (ok) {
// 处理数据
this.tableData = data;
this.pagination.total = page.total;
}
};
// 步骤4: 处理错误
try {
await fetchData();
} catch (error) {
this.$message.error("获取用户列表失败");
console.error(error);
}🎨 UI开发指南
1. Element UI 使用规范
vue
<template>
<div>
<!-- 表单 -->
<el-form
:model="form"
:rules="rules"
ref="form"
label-width="100px"
>
<el-form-item label="姓名" prop="name">
<el-input
v-model="form.name"
placeholder="请输入姓名"
clearable
/>
</el-form-item>
<el-form-item label="部门" prop="dept">
<el-select
v-model="form.dept"
placeholder="请选择部门"
filterable
>
<el-option
v-for="item in deptOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="reset">重置</el-button>
<el-button type="primary" @click="submit">提交</el-button>
</el-form-item>
</el-form>
<!-- 表格 -->
<el-table
:data="tableData"
v-loading="loading"
stripe
border
style="width: 100%"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="name" label="姓名" />
<el-table-column prop="age" label="年龄" />
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button
type="text"
size="small"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
type="text"
size="small"
@click="handleDelete(row)"
style="color: #f56c6c"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
style="margin-top: 16px"
:current-page="pagination.page"
:page-size="pagination.rownumber"
:total="pagination.total"
@current-change="handlePageChange"
layout="total, prev, pager, next"
/>
</div>
</template>2. 响应式设计
scss
// 移动端适配
@media (max-width: 768px) {
.container {
padding: 12px;
.toolbar {
flex-direction: column;
gap: 8px;
.el-button {
width: 100%;
}
}
.el-table {
font-size: 12px;
::v-deep .cell {
padding: 4px 0;
}
}
}
}
// 平板适配
@media (min-width: 769px) and (max-width: 1024px) {
.container {
padding: 16px;
.toolbar {
gap: 12px;
}
}
}🔧 调试技巧
1. 浏览器开发者工具
javascript
// 1. 调试Vuex状态
console.log(this.$store.state.routeStack.stack);
// 2. 监听状态变化
this.$store.subscribe((mutation, state) => {
console.log('Mutation:', mutation.type, mutation.payload);
});
// 3. 调试路由
console.log(this.$route); // 当前路由信息
console.log(this.$router); // 路由实例
// 4. 调试组件
console.log(this.$refs); // 所有ref引用
console.log(this.$el); // 组件DOM元素2. 网络请求调试
javascript
// 在 http.js 中启用详细日志
instance.interceptors.request.use(config => {
console.log(`[Request] ${config.method} ${config.url}`, {
data: config.data,
headers: config.headers
});
return config;
});
instance.interceptors.response.use(response => {
console.log(`[Response] ${response.status}`, {
data: response.data,
duration: Date.now() - response.config.metadata.startTime
});
return response;
});3. 性能分析
javascript
// 测量组件渲染时间
export default {
mounted() {
this.$nextTick(() => {
const duration = performance.now() - performance.timing.navigationStart;
console.log(`组件渲染耗时: ${duration.toFixed(2)}ms`);
});
}
}
// 分析包大小
// vue.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
configureWebpack: {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false
})
]
}🧪 测试指南
1. 单元测试示例
javascript
// test/unit/components/user-list.spec.js
import { shallowMount } from '@vue/test-utils';
import UserList from '@/components/UserList.vue';
describe('UserList.vue', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(UserList, {
propsData: {
service: 'srvuser_list_select'
},
mocks: {
$http: {
post: jest.fn().mockResolvedValue({
data: {
state: 'SUCCESS',
data: [{ id: 1, name: '张三' }],
page: { total: 1 }
}
})
},
$message: {
success: jest.fn(),
error: jest.fn()
}
}
});
});
afterEach(() => {
wrapper.destroy();
});
test('组件正确渲染', () => {
expect(wrapper.exists()).toBe(true);
});
test('数据加载功能', async () => {
await wrapper.vm.fetchData();
expect(wrapper.vm.tableData).toHaveLength(1);
expect(wrapper.vm.tableData[0].name).toBe('张三');
});
test('新增按钮跳转', async () => {
const pushSpy = jest.fn();
wrapper.vm.$router = { push: pushSpy };
await wrapper.vm.handleAdd();
expect(pushSpy).toHaveBeenCalledWith({
path: '/add/srvuser_list_select'
});
});
});2. E2E测试示例
javascript
// test/e2e/specs/user-management.spec.js
describe('用户管理功能', () => {
beforeEach(() => {
cy.visit('/list/srvuser_list_select');
});
it('应该能正确加载用户列表', () => {
cy.get('.el-table').should('exist');
cy.get('.el-table__body tr').should('have.length.at.least', 1);
});
it('应该能新增用户', () => {
cy.get('.el-button').contains('新增').click();
cy.url().should('include', '/add/srvuser_list_select');
cy.get('input[name="name"]').type('测试用户');
cy.get('input[name="age"]').type('25');
cy.get('.el-button').contains('提交').click();
cy.get('.el-message').should('contain', '成功');
});
it('应该能搜索用户', () => {
cy.get('input[placeholder="搜索"]').type('张');
cy.get('.el-button').contains('搜索').click();
cy.get('.el-table__body tr').each(($el) => {
cy.wrap($el).should('contain', '张');
});
});
});🚀 性能优化
1. 代码分割
javascript
// 路由懒加载
const routes = [
{
path: '/lowcode/editor/:pageNo',
name: 'lowcode-editor',
component: () => import(/* webpackChunkName: "lowcode" */ '@/pages/lowcode/index.vue')
},
{
path: '/list/:service_name',
name: 'list',
component: () => import(/* webpackChunkName: "common-crud" */ '@/components/common/list.vue')
}
];
// 组件懒加载
export default {
components: {
HeavyComponent: () => import('./HeavyComponent.vue')
}
}2. 数据缓存
javascript
// 使用Vuex缓存
const actions = {
async fetchUserList({ commit, state }, force = false) {
// 检查缓存
if (!force && state.userList.length > 0) {
return state.userList;
}
const { data, ok } = await $selectList('/api/users', req);
if (ok) {
commit('SET_USER_LIST', data);
return data;
}
}
};
// 使用localStorage持久化
const mutations = {
SET_USER_LIST(state, list) {
state.userList = list;
localStorage.setItem('userList', JSON.stringify(list));
}
};3. 图片优化
javascript
// 使用图片懒加载
import Vue from 'vue';
import VueLazyload from 'vue-lazyload';
Vue.use(VueLazyload, {
loading: '/assets/loading.gif',
error: '/assets/error.png'
});
// 在组件中使用
<img v-lazy="imageUrl" alt="用户头像" />🔒 安全最佳实践
1. 输入验证
javascript
// 前端验证
const rules = {
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' },
{ pattern: /^[a-zA-Z\u4e00-\u9fa5]+$/, message: '只允许中文和英文', trigger: 'blur' }
],
email: [
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
]
};
// 后端验证(前端配合)
const submitData = async () => {
try {
// 清理输入
const cleanData = {
name: form.name.trim(),
email: form.email.trim(),
age: parseInt(form.age, 10)
};
// 验证
await this.validateData(cleanData);
// 提交
const res = await $http.post('/api/user/add', {
serviceName: 'srvuser_list_add',
data: [cleanData]
});
return res;
} catch (error) {
this.$message.error('提交失败');
throw error;
}
};2. 敏感信息处理
javascript
// ❌ 永远不要这样做
const config = {
apiKey: 'sk-1234567890abcdef', // 硬编码密钥
password: 'admin123' // 硬编码密码
};
// ✅ 正确做法:使用环境变量
const config = {
apiKey: process.env.VUE_APP_API_KEY,
apiSecret: process.env.VUE_APP_API_SECRET
};
// 注意:即使使用环境变量,也不要将敏感信息提交到代码仓库
// 在 .env.example 中提供模板,实际值在 .env 中配置3. XSS防护
javascript
// 自动转义(Vue默认行为)
<template>
<!-- 安全 -->
<div>{{ userInput }}</div>
<!-- 危险:避免使用 v-html -->
<!-- <div v-html="userInput"></div> -->
</template>
// 如果必须使用 v-html,使用 DOMPurify
import DOMPurify from 'dompurify';
export default {
methods: {
safeHtml(html) {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'u', 'p', 'br'],
ALLOWED_ATTR: ['class', 'style']
});
}
}
}📊 监控与日志
1. 错误监控
javascript
// 全局错误捕获
Vue.config.errorHandler = function (err, vm, info) {
console.error('Vue错误:', err);
console.error('组件:', vm.$options.name);
console.error('信息:', info);
// 发送到监控平台
if (process.env.NODE_ENV === 'production') {
fetch('/api/error-collect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'vue-error',
message: err.message,
stack: err.stack,
component: vm.$options.name,
info: info,
url: window.location.href,
timestamp: new Date().toISOString()
})
}).catch(() => {});
}
};
// Promise错误
window.addEventListener('unhandledrejection', (event) => {
console.error('未处理的Promise:', event.reason);
if (process.env.NODE_ENV === 'production') {
fetch('/api/error-collect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'promise-rejection',
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
url: window.location.href,
timestamp: new Date().toISOString()
})
});
}
});2. 性能监控
javascript
// 页面加载性能
window.addEventListener('load', () => {
const perfData = performance.timing;
const metrics = {
dns: perfData.domainLookupEnd - perfData.domainLookupStart,
tcp: perfData.connectEnd - perfData.connectStart,
ttfb: perfData.responseStart - perfData.requestStart, // 首字节时间
download: perfData.responseEnd - perfData.responseStart,
total: perfData.loadEventEnd - perfData.navigationStart
};
console.log('页面性能:', metrics);
// 上报慢页面
if (metrics.total > 3000) {
fetch('/api/perf-collect', {
method: 'POST',
body: JSON.stringify({ metrics, url: window.location.href })
});
}
});
// API性能
instance.interceptors.response.use(response => {
const duration = Date.now() - response.config.metadata.startTime;
if (duration > 1000) {
console.warn(`慢API: ${response.config.url} (${duration}ms)`);
if (process.env.NODE_ENV === 'production') {
fetch('/api/perf-collect', {
method: 'POST',
body: JSON.stringify({
type: 'slow-api',
url: response.config.url,
duration,
timestamp: new Date().toISOString()
})
});
}
}
return response;
});🤝 团队协作
1. Git工作流
bash
# 主分支保护
# main 分支只接受 PR,不允许直接提交
# 功能开发流程
git checkout main
git pull origin main
git checkout -b feature/user-auth
# 开发完成后
git add .
git commit -m "feat: 添加用户认证功能
- 实现登录/登出
- 添加JWT验证
- 支持记住登录状态"
git push origin feature/user-auth
# 在 Gitee 上创建 PR,等待 Code Review
# Code Review 通过后合并
# 删除功能分支
git branch -d feature/user-auth2. 提交信息规范
bash
# 格式: <type>(<scope>): <subject>
# 示例
feat(auth): 添加用户登录功能
fix(list): 修复表格分页bug
docs(api): 更新API文档
refactor(router): 重构路由配置
style(ui): 优化按钮样式
# 类型列表
feat: 新功能
fix: Bug修复
docs: 文档更新
style: 代码格式(不影响功能)
refactor: 代码重构
test: 测试相关
chore: 构建/工具相关3. 代码审查清单
markdown
- [ ] 功能是否完整实现?
- [ ] 代码是否符合规范?
- [ ] 是否有性能问题?
- [ ] 是否有安全漏洞?
- [ ] 是否有重复代码?
- [ ] 是否有充分的注释?
- [ ] 是否更新了相关文档?
- [ ] 是否通过了测试?
- [ ] 是否考虑了边界情况?
- [ ] 是否考虑了移动端适配?🐛 常见问题解决
1. 依赖安装失败
bash
# 问题: 网络超时
# 解决: 使用国内镜像
yarn config set registry https://registry.npmmirror.com/
# 清理缓存
yarn cache clean
# 重新安装
rm -rf node_modules yarn.lock
yarn install2. 热更新失效
bash
# 重启开发服务器
yarn dev
# 检查端口占用
lsof -i :8080
kill -9 <PID>
# 清理浏览器缓存
# Chrome: Ctrl+Shift+R (硬刷新)3. 路由跳转报错
javascript
// 问题: NavigationDuplicated
// 解决: 使用异步跳转
this.$router.push('/path').catch(err => {
if (err.name !== 'NavigationDuplicated') {
throw err;
}
});
// 或使用 Promise
await this.$router.push('/path');4. 组件不更新
javascript
// 问题: 数据变化但视图不更新
// 解决: 使用 $set 或 重新赋值
// ❌ 错误
this.data.push(item);
// ✅ 正确
this.$set(this.data, this.data.length, item);
// 或
this.data = [...this.data, item];文档维护: l-pc-front 开发组
最后更新: 2025-12-19