Vue - vue 前端项目基础框架搭建流程

1. 初始化项目

// 创建vue基础代码框架, 可选择支持typescript,vue-router, pinia, jsx, Eslint, prettier
pnpm create vue

// 添加路由支持 (创建基础代码框架时可选)
pnpm add vue-router

// 添加数据状态管理支持 (创建基础代码框架时可选)
pnpm add pinia

// 添加UI模块支持
pnpm add element-plus
// 添加图标支持
pnpm add @element-plus/icons-vue

// 增加网络请求模块支持
pnpm add axios

// 添加国际化支持
pnpm add vue-i18n

// 添加css预处理器支持
pnpm add -D sass

// 引入数据处理支持
pnpm add lodash-es

// 引入日期处理支持
pnpm add dayjs

// 增加数据模拟模块支持
pnpm add -D mockjs

2. 编译配置 (vite.config.js)

// 安装自动引入组件开发插件
pnpm add -D unplugin-vue-components
pnpm add -D unplugin-auto-import

// 安装插件支持jsx语法 (创建基础代码框架时可选)
pnpm add -D @vitejs/plugin-vue-jsx

vite.config.js

import { defineConfig } from 'vite';
import vueJsx from '@vitejs/plugin-vue-jsx';
import vue from '@vitejs/plugin-vue';

import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';

import vueDevTools from 'vite-plugin-vue-devtools'
import { fileURLToPath, URL } from 'node:url'

export default defineConfig({
  plugins: [
    vueJsx(),   // 使vue项目支持jsx语法
    vue(),  // 使vite支持vue组件的编译和解析
    vueDevTools(),  // vue开发者工具
    /**支持自动导入element-plus组件,vue-router, vue-i18n, pinia */
    AutoImport({
      imports: ['vue', 'vue-router', 'pinia', 'vue-i18n'],
      resolvers: [ElementPlusResolver()]
    }),
    Components({
      resolvers: [ElementPlusResolver()]
    })
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
});

3. 引入normal.css 进行样式重置

  • 安装normalize.css
    pnpm add normalize.css

  • 项目入口引入样式重置文件
    src/main.js

// 使用normalize.css 重置样式
import 'normalize.css';

4. 国际化配置

  • 创建语言包目录及语言文件 src/i18n/language, src/i18n/language/en.js src/i18n/language/zh-cn.js
    language/en.js
export default {
  logout: "Exit",
  login: "Login",
  nodata: "No results.",
}

language/zh-cn.js

export default {
  logout: '退出',
  login: '登录',
  nodata: '暂无数据',
}
  • 创建语言包入口文件 src/i18n/index.js
import { createI18n } from 'vue-i18n';
// 项目语言包文件
import localEn from './language/en';
import localZhCn from './language/zh-cn';

// element-plus 语言包文件
import en from 'element-plus/es/locale/lang/en';
import zhCn from 'element-plus/es/locale/lang/zh-cn';

// project i18n
const i18n = createI18n({
  legacy: false,  // vue组合式api设为false
  locale: 'zh-cn',  // 默认语言
  fallbackLocale: 'en', // 回退语言
  messages: {
    'zh-cn': localZhCn,
    'en': localEn
  }
});

// element i18n
export const elementLocales = {
  'zh-cn': zhCn,
  en
};

export default i18n;
  • 项目入口引入i18n
    src/main.js
import { createApp } from 'vue';
import i18n from './i18n';

const app = createApp(App);
app.use(i18n).mount('#app');
  • 项目中设置element-plus组件国际化
    src/App.vue
<script setup>
// 导入elementUI 的国际化内容
import { elementLocales } from '@/i18n';
const { locale } = useI18n();
locale.value = localStorage.getItem('locale') || 'zh-cn';
</script>
<template>
  <el-config-provider :locale="elementLocales[locale]">
    <router-view></router-view>
  </el-config-provider>
</template>
  • 在项目中使用国际化相关方法
<template>
  <div class="font-sm">{{ t('tips404') }}</div>
</template>
<script setup>
const { locale, t } = useI18n();
function changeLanguage(lang){
  locale.value = lang;
  localStorage.setItem('locale', lang);
}
</script>

5. 配置路由

  • 创建路由配置文件
    src/router/router.js
// 组件采用懒加载形式引入,打包时会进行分块
export default [
  {
    path: '/datacenter',
    name: 'Datacenter',
    component: () => import('@/views/datacenter/index.vue'),
  },
  {
    path: '/404',
    name: 'NotFound',
    component: () => import('@/views/404.vue'),
  },
]
  • 创建路由入口文件
    src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
// 布局文件
import Layout from '@/layout/index.vue'
// 路由配置文件
import subRoutes from './router';

/**
 * 配置路由
 */
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Layout,
    children: subRoutes,
  }
];

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router
  • 创建布局文件
    src/layout/components/PageHeader.vue
    src/layout/components/PageSidebar.vue
    src/layout/index.vue
<template>
  <div class="page-container">
    <main>
      <page-sidebar></page-sidebar>
      <div class="right">
        <page-header />
        <router-view></router-view>
      </div>
    </main>
  </div>
</template>

<script setup>
import { computed } from 'vue';
import PageHeader from './components/PageHeader.vue';
import PageSidebar from './components/PageSidebar.vue';
const route = useRoute();
</script>

<style lang="scss">
.page-container {
  display: flex;
  flex-direction: column;
  height: 100%;
  overflow: hidden;
}
</style>
  • 在项目入口引入路由
    src/main.js
import { createApp } from 'vue';
import router from './router';
const app = createApp(App);
app.use(router).mount('#app');

6. 创建路由对应的视图文件

  • 404 页面视图
    src/views/404.vue
  • 数据中心页面视图
    src/views/datacenter/index.vue

7. 创建布局文件

  • 页头
    src/layout/components/PageHeader.vue
  • 侧边栏
    src/layout/components/PageSidebar.vue
  • 布局入口文件
    src/layout/index.vue

8. 创建store

  • 在项目入口引入pinia
    src/main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';

const pinia = createPinia();
const app = createApp(App);
app.use(pinia).mount('#app');
  • 创建store
    src/store/dict.js // 字典数据
import { defineStore } from 'pinia';
import { getDict } from '@/apis/dict';
export const DICT = {
  vehicleType: '1819698002348158977', 
  plateColor: '1819698002348158978', 
}

export const useDictStore = defineStore('dict', {
  // 类似于data
  state: () => ({
    data: {}
  }),
  // 类似于computed计算属性
  getters: {
    dict(state, type) {
      return state.data[type] || [];
    }
  },
  // 类似于vue2中的methods
  actions: {
    setDict(type, data) {
      this.data[type] = data;
    },
    format(type, value) {
      const data = this.data[type];
      if (data) {
        const obj = data.find((item) => value == item.value);
        return obj?.label || '';
      }
    },
    async getDict(type, callback) {
      const id = DICT[type];
      if (!id) throw new Error('字典类型不存在');
      // 如果store中存在,则从store中直接获取
      if(this.data[type]) {
        // 执行回调
        callback && callback();
        return this.data[type];
      } 
      // 获取该类型字典数据
      const res = await getDict(id);
      const resData = res?.data || [];
      // 提取关键数据转换为{label: '', value: ''}
      const data = resData.map((item) => ({ label: item.name, value: item.value }));
      // 存储数据
      this.setDict(type, data);
      // 执行回调
      callback && callback();
      // 返回数据
      return data;
    }
  }
});
  • 在项目中使用创建的store
<script setup>
import { useDictStore } from '@/store/dict';
const dictStore = useDictStore();

onMounted(() => {
  dictStore.getDict('vehicleType');
});
</script>

9. 封装mock模拟数据模块

  • 创建mock入口文件
    src/mock/index.js
import Mock from 'mockjs';
import config from './config';

// 引入模块模拟数据
import * as login from './modules/login';
import * as app from './modules/app';

const { baseURL, timeout } = config;

Mock.setup({ timeout });

// 1. 开启/关闭所有模块拦截,通过openMock开关设置
// 2. 开启/关闭单个模块拦截,通过调用mock方法isOpen参数设置
// 3. 开启/关闭模块中某个请求拦截,通过函数返回对象中的isOpen属性设置

const openMock = false;
mockAll([
  login, 
  app, 
], openMock);

function mockAll(modules, isOpen=true){
  for(const k in modules){
    mock(modules[k], isOpen);
  }
}

// 模拟单个模块
// mock(login, true);
// mock(app, true);

function mock(mod, isOpen = true) {
  if(isOpen){
    for (var key in mod){
      ((res) => {
        if(res.isOpen !== false){
          let url = baseURL;
          if(!url.endsWith('/')){
            url = url + '/';
          }
          url = url + res.url;
          Mock.mock(new RegExp(url), res.method, (opts) =>{
            opts.data = opts.body ? JSON.parse(opts.body) : null;
            const resData = Mock.mock(typeof res.response === 'function' ? res.response(opts):res.response);
            console.log('%cmock拦截, 请求:', 'color: blue', opts);
            console.log('%cmock拦截,响应:',  'color: blue', resData);
            return resData;
          });
        }
      })(mod[key]() || {});
    }
  }
}
  • 创建数据模块文件
    src/mock/modules/app.js
// 应用管理模块 分页查询
export function listPage() {
  return {
    url: "app/listPage",
    method: "get",
    response: (opts) => {
      const { pageNum, pageSize } = opts.data;
      const totalSize = 24;
      const content =
        pageNum * pageSize < totalSize
          ? `content|${pageSize}`
          : `content|${totalSize % pageSize}`;
      return {
        code: 200,
        msg: null,
        data: {
          pageNum,
          pageSize,
          totalSize,
          [content]: [
            {
              appCode: "@increment",
              appName: "@word",
              "versionName": "15",
              versionNumber: "@float(1, 100, 2)",
              "isForce|1": ["是", "否"],
              buildTime: "@date @time",
              commitTime: "@date @time",
              "versionStatus|1": [0, 1],
            },
          ],
        },
      };
    },
  };
}
  • 项目入口引入mock
    src/main.js
// 使用mockjs进行数据模拟
import './mock';

10. 封装网络请求模块

  • 配置请求参数设置
    src/request/config.js
export default {
  // 默认请求方法
  method: 'get',
  // 请求url前缀
  baseURL: 'http://your.domain.com/api',
  // 请求头设置
  headers: {
    'Content-type': 'application/json;charset=UTF-8'
  },
  // 设置超时时间
  timeout: 10000,
  // 携带凭证
  withCredentials: true,
  // 返回数据类型
  responseType: 'json'
};
  • 封装统一的api请求
    src/request/index.js
import axios from 'axios';
import config from './config';
import router from '../router';
import { ElMessage } from 'element-plus';
import { useUserStore } from '@/store/user';
import i18n from '@/i18n';
const { t } = i18n.global;

export default function request(options) {

  return new Promise((resolve, reject) => {
    // 创建请求实例
    const instance = axios.create({ ...config });
    const userStore = useUserStore();

    // 增加请求拦截处理
    instance.interceptors.request.use(
      (config) => {
        let token = localStorage.getItem('access_token');
        if (token) {
          config.headers.Authorization = `bearer ${token}`;
        } else {
          router.push('/login');
        }
        return config;
      },
      (error) => {
        console.log('request:', error);
        if (error.code === 'ECONNABORTED' && error.message.indexOf('timeout') !== -1) {
          ElMessage({ message: '请求超时', type: 'error', showClose: true });
        }
        return Promise.reject(error);
      }
    );

    // 增加响应拦截处理
    instance.interceptors.response.use(
      (response) => {
        return response.data;
      },
      (err) => {
        const codeMap = {
          400: '请求错误',
          401: '未授权,请登录',
          403: '拒绝访问',
          404: '请求地址出错',
          408: '请求超时',
          500: '服务器内部错误',
          501: '服务未实现',
          502: '网关错误',
          503: '服务不可用',
          504: '网关超时',
          505: 'HTTP版本不受支持'
        };
        if (err && err.response) {
          const status = err.response.status;
          const errMsg = codeMap[status] || '';
          ElMessage({ type: 'error', message: errMsg, showClose: true });
          if (status === 401) {
            userStore.clearToken();
            router.push('/login');
          }
        }
        return Promise.reject(err);
      }
    );

    // 处理正常返回的数据
    instance(options)
      .then((res) => {
        if (res || res.code === 200) {
          resolve(res);
        } else {
          if (res.code === -2) {
            router.push('/login');
          }
          ElMessage({ type: 'error', message: res.msg || '操作失败', showClose: true });
          reject(res);
        }
      })
      .catch((error) => {
        reject(error);
      });
  });
}

  • 添加业务模块所需的请求接口
    src/apis/login.js
import request from '@/request';
import config from '@/request/config';

const baseURL = `${config.baseURL}/auth/oauth/token`

// 登录
export const login = (data) => {
  return request({
    headers: {
      'Content-type': 'application/x-www-form-urlencoded;charset=utf-8'
    },
    url: baseURL,
    method: 'post',
    data
  });
};

// 退出登录
export const logout = (data) => {
  return request({
    headers: {
      'Content-type': 'application/x-www-form-urlencoded;charset=utf-8'
    },
    url: `${config.baseURL}/auth/logout2`,
    method: 'post',
    data,
  });
};

src/apis/params.js

import request from '@/request';
import config from '@/request/config';

const baseURL = `${config.baseURL}/admin/baseparam`

// 基础参数查询
export const listPage = (data) => {
  const url = `${baseURL}/queryList`;
  return request({ url, method: 'post', data });
};

// 添加
export const save = (data) => {
  const url = `${baseURL}/add`;
  return request({ url, method: 'post', data });
};

// 更新
export const update = (data) => {
  const url = `${baseURL}/update`;
  return request({ url, method: 'post', data });
}

// 删除
export const remove = (id) => {
  const url = `${baseURL}/delete/${id}`;
  return request({ url, method: 'post'});
}

11. 定义项目中用到常量

src/consts/index.js

// 设置统计数据 5 min 刷新一次
export const STATS_DATA_FRESH_TIME = 5 * 60 * 1000;
// 设置实时检测记录数据 30s 刷新一次
export const RECORD_DATA_FRESH_TIME = 30 * 1000;

12. 封装自定义组件

  • 创建自定义组件
    src/components/CmVideo.vue
<template>
  <el-dialog v-model="dialogVisible" :draggable="true" :close-on-click-modal="false" @close="closeDialog"
    class="videoDialog">
    <video :src="videoUrl" controls autoplay width="100%"></video>
  </el-dialog>
</template>
<script setup>
const dialogVisible = ref(false);
const videoUrl = ref(null);

const props = defineProps(['url']);
defineExpose({
  display: openDialog,
  close: closeDialog,
});

function openDialog() {
  dialogVisible.value = true;
  videoUrl.value = props.url;
}

function closeDialog() {
  videoUrl.value = null;
  dialogVisible.value = false;
}
</script>

<style lang="scss" scoped>
</style>
  • 在项目中使用自定义组件
<template>
  <CmVideo ref="videoRef" :url="videoUrl" />
</template>
<script>
import { getFile } from '@/apis/file';
import CmVideo from '@/components/CmVideo.vue';
const videoRef = ref(null);
const videoUrl = ref(null);
const isVideoLoading = ref(false);

// 播放视频
function playVideo() {
  if (isVideoLoading.value || videoUrl.value == null) return;
  videoRef.value?.display();
}

// 获取视频资源
async function getVideo(truckVedio) {
  if (truckVedio == 'null' || truckVedio == '0') return;
  isVideoLoading.value = true;
  return getFile(truckVedio).then((res) => {
    const url = window.URL.createObjectURL(res);
    videoUrl.value = url;
  }).finally(() => {
    isVideoLoading.value = false;
  });
}

onMounted(async () => {
  const { detectImages, truckVedio } = await getData();
  getVideo(truckVedio);
});
</script>

13 封装组合式函数

src/hooks/use-table-handlers.js

  • 创建组合式函数
export default function useTableHandlers(form) {
  const { t } = useI18n();
  const tableRef = ref();
  const formRef = ref();
  const formLoading = ref(false);
  const dialogVisible = ref(false);
  const isEdit = ref(false);
  const _formOld = { ...form };

  // 查询
  const doSearch = () => {
    tableRef.value.reload();
  };

  // 增加
  const doAdd = () => {
    dialogVisible.value = true;
    isEdit.value = false;
    formRef.value && formRef.value.clearValidate();
  };

  // 编辑
  const doEdit = (row) => {
    if (!form) return;
    isEdit.value = true;
    dialogVisible.value = true;
    for (const key in form) {
      if (key in row) {
        form[key] = row[key];
      }
    }
  };

  // 删除
  const doRemove = (api, ids, callback) => {
    api(ids).then((res) => {
      callback && callback(res);
    });
  };

  // 获取参数
  function getParams() {
    const params = { ...form };
    if (!isEdit.value) {
      delete params.id;
    }
    return params;
  }

  // 提交数据
  function doSubmit(apis, callback) {
    if (!form || !apis) return;
    formRef.value.validate((valid) => {
      if (!valid) return;
      formLoading.value = true;
      let promise;
      const params = apis.getParams ? apis.getParams() : getParams();
      if (isEdit.value) {
        promise = apis.update(params);
      } else {
        promise = apis.save(params);
      }
      promise
        .then((res) => {
          if (callback) {
            callback(res);
          } else {
            ElMessage({
              message: t('tips.success'),
              type: 'success',
              showClose: true
            });
          }
          doClose();
          if (isEdit.value) {
            tableRef.value.refresh();
          } else {
            tableRef.value.reload();
          }
        })
        .finally(() => {
          formLoading.value = false;
        });
    });
  }

  //重置表单
  function resetForm() {
    if (!form) return;
    for (const key in _formOld) {
      form[key] = _formOld[key];
    }
  }

  // 关闭新增弹窗
  function doClose() {
    dialogVisible.value = false;
    resetForm();
  }

  return {
    t,
    tableRef,
    dialogVisible,
    isEdit,
    formLoading,
    formRef,
    doSearch,
    doAdd,
    doEdit,
    doRemove,
    doSubmit,
    doClose
  };
}


  • 在项目中使用组合式函数
<template>
<cm-table ref="tableRef" :header-cell-style="{ background: '#F6F7FB' }" :oprWidth="150" :get-page="listPage"
    :filters="filters" :columns="columns" :showBatchDelete="false" :showAdd="false" :operations="operations"
    :showPagination="true" @handleEdit="doEdit" @handleAdd="doAdd" @handleDelete="handleDelete"></cm-table>

  <el-form ref="formRef" :model="form" :rules="rules" label-width="120px" label-position="right" label-suffix=": ">
    <el-row>
      <el-col :span="24">
        <el-form-item prop="name" label="检测点名称">
          <el-input v-model="form.name" placeholder="检测点名称"></el-input>
        </el-form-item>
      </el-col>
    </el-row>
  </el-form>
</template>
<script setup>
import useTableHandlers from '@/hooks/use-table-handlers';

// form数据模型
const form = reactive({
  id: null,
  name: null,
  pointNo: null,
  deviceType: null,
  lat: null,
  lng: null,
  location: null,
});

// 使用表格处理器
const {
  t,
  tableRef,
  formRef,
  formLoading,
  dialogVisible,
  isEdit,
  doSearch,
  doAdd,
  doEdit,
  doRemove,
  doSubmit,
  doClose
} = useTableHandlers(form);

// 处理删除
function handleDelete(ids, callback) {
  doRemove(remove, ids, callback);
}
</script>

14. 封装自定义指令 [可选]

src/directives/

15. 封装自定义插件 [可选]

src/plugins/

16. 封装工具函数

src/utils/index.js

import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration'
dayjs.extend(duration);

// 转换时长
export function formatDuration(duration) {
  return dayjs.duration(duration, 'seconds').format('HH:mm:ss');
}

17. 进行业务开发

  • 开发api请求
    在src/apis/ 目录下建立以业务命名的接口请求文件
  • 开发数据状态业务
    在src/store/ 目录下建立以业务命名的进行数据请求或者数据转换的文件(如果有需要进行数据状态管理的话)
  • 构建业务相关UI
    在src/views/ 目录下建立以模块命名的视图目录,并建立相关单文件组件视图页面
posted @ 2024-12-26 15:00  箫笛  阅读(16)  评论(0编辑  收藏  举报