yarn add -g @haha-cli/core
haha-cli --version
haha-cli init hahaya //备注 上线仍有问题 暂不处理 先主要处理项目
Git Flow标准
根据需求,从 master 拉出分支
开发阶段,提交 commit
开发完毕,发起 PR(pull request)
代码评审
部署,测试
merge 到 master
分支命名
feature 开头代表新功能开发
hotfix 开头代表 bug 修复
安装第三方组件库 ant-design-vue
yarn add ant-design-vue
import { createApp } from 'vue'
import App from './App.vue'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/antd.css'
createApp(App).use(Antd).mount('#app')
增加ts配置:https://juejin.cn/post/6982444129271676942
项目搭建
<template>
<div class="editor-container">
<a-layout>
<a-layout-sider width="300" style="background: #fff">
<div class="sidebar-container">组件列表</div>
</a-layout-sider>
<a-layout style="padding: 0 24px 24px">
<a-layout-content class="preview-container">
<p>画布区域</p>
<div class="preview-list" id="canvas-area"></div>
</a-layout-content>
</a-layout>
<a-layout-sider
width="300"
style="background: #fff"
class="settings-panel"
>
组件属性
</a-layout-sider>
</a-layout>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue'
export default defineComponent({
components: {},
setup() {},
})
</script>
<style>
.editor-container .preview-container {
padding: 24px;
margin: 0;
min-height: 85vh;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.editor-container .preview-list {
padding: 0;
margin: 0;
min-width: 375px;
min-height: 200px;
border: 1px solid #efefef;
background: #fff;
overflow-x: hidden;
overflow-y: auto;
position: fixed;
margin-top: 50px;
max-height: 80vh;
}
</style>
<template>
<a-layout>
<a-layout-header>乐高</a-layout-header>
<a-layout-content>
<div class="home-container"><template-list></template-list></div
></a-layout-content>
<a-layout-footer>© 慕课网(imooc.com)版权所有</a-layout-footer>
</a-layout>
</template>
<script>
import TemplateList from '@/components/TemplateList.vue'
import { defineComponent } from 'vue'
export default defineComponent({
components: { TemplateList },
setup() {
},
})
</script>
<style>
.ant-layout-header {
color: white;
}
.page-title {
color: #fff;
}
.home-container {
background: #fff;
padding: 0 24px 24px 30px;
min-height: 85vh;
max-width: 1200px;
/* margin: 50px auto; */
width: 100%;
}
</style>
<template>
<div class="homepage-container">
<a-layout :style="{ background: '#fff' }">
<a-layout-header class="header">
<div class="page-title">
<router-link to="/">慕课乐高</router-link>
</div>
<user-profile :user="user"></user-profile>
</a-layout-header>
<a-layout-content class="home-layout">
<router-view></router-view>
</a-layout-content>
</a-layout>
<a-layout-footer>
© 慕课网(imooc.com)版权所有 | 津ICP备20000929号-2
</a-layout-footer>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
export default defineComponent({
name: 'Index',
components: {},
setup() {},
})
</script>
<style>
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
.page-title {
color: #fff;
}
</style>
<template>
<div class="work-detail-container">
<a-row type="flex" justify="center">
<a-col :span="8" class="cover-img">
<img
src="http://typescript-vue.oss-cn-beijing.aliyuncs.com/vue-marker/5f81cca3f3bf7a0e1ebaf885.png"
alt=""
/>
</a-col>
<a-col :span="8">
<h2>11</h2>
<p>11</p>
<div class="author">
<a-avatar>V</a-avatar>
该模版由 <b>11</b> 创作
</div>
<div class="bar-code-area">
<span>扫一扫,手机预览</span>
<div ref="container"></div>
</div>
<div class="use-button">
<a-button type="primary" size="large"> 使用模版 </a-button>
<a-button size="large"> 下载图片海报 </a-button>
</div>
</a-col>
</a-row>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue'
export default defineComponent({
setup() {},
})
</script>
<style scoped>
.work-detail-container {
margin-top: 50px;
}
.cover-img {
margin-right: 30px;
}
.cover-img img {
width: 100%;
}
.use-button {
margin: 30px 0;
}
.ant-avatar {
margin-right: 10px;
}
.bar-code-area {
margin: 20px 0;
}
</style>
<template>
<div class="template-list-component">
<a-row :gutter="16">
<a-col :span="6" class="poster-item">
<a-card hoverable>
<template #cover>
<!-- <img :src="item.coverImg" v-if="item.coverImg" /> -->
<img
src="http://typescript-vue.oss-cn-beijing.aliyuncs.com/vue-marker/5f81cca3f3bf7a0e1ebaf885.png"
/>
<div class="hover-item">
<a-button size="large" type="primary">使用该模版创建</a-button>
</div>
</template>
<a-card-meta :title="11">
<template v-slot:description>
<div class="description-detail">
<span>作者:11</span>
<span class="user-number">11</span>
</div>
</template>
</a-card-meta>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'template-list',
})
</script>
<style>
.poster-item {
position: relative;
margin-bottom: 20px;
}
.poster-item .ant-card {
border-radius: 12px;
}
.poster-item .ant-card-cover {
height: 390px;
}
.poster-item .ant-card-cover > img {
width: 100%;
}
.poster-item .ant-card-hoverable {
box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.1);
}
.poster-item .ant-card-body {
padding: 0;
}
.poster-item .ant-card-meta {
margin: 0;
}
.poster-item .ant-card-meta-title {
color: #333;
padding: 10px 12px;
border-bottom: 1px solid #f2f2f2;
margin-bottom: 0 !important;
}
.description-detail {
display: flex;
justify-content: space-between;
padding: 13px 12px;
color: #999;
}
.user-number {
font-weight: bold;
}
.poster-title {
height: 70px;
}
.poster-title h2 {
margin-bottom: 0px;
}
.poster-item .ant-card-cover {
position: relative;
overflow: hidden;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}
.poster-item .ant-card-cover img {
transition: all ease-in 0.2s;
}
.poster-item .ant-card-cover .hover-item {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: none;
background: rgba(0, 0, 0, 0.8);
align-items: center;
justify-content: center;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}
.poster-item:hover .hover-item {
display: flex;
}
.poster-item:hover img {
transform: scale(1.25);
}
.barcode-container img {
border-radius: 0;
}
</style>
安装 vue-router 路由
import { createRouter, createWebHashHistory } from 'vue-router'
const Home = () => import('@/views/Home.vue')
const Editor = () => import('../views/Editor.vue')
const TemplateDetail = () => import('../views/TemplateDetail.vue')
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
name: 'home',
component: Home,
meta: {
withFooter: true,
},
children: [
//嵌套路由
// {
// path: '/',
// name: 'home',
// component: Home,
// },
],
},
{
path: '/editor',
name: 'editor',
component: Editor,
},
{
path: '/template/:id',
name: 'template',
component: TemplateDetail,
meta: {
withFooter: true,
},
},
],
})
export default router
import { createApp } from 'vue'
import App from './App.vue'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/antd.css'
import router from '@/routes/index'
//将路由挂载到app实例上
createApp(App).use(Antd).use(router).mount('#app')
在App.vue增加路由视图
<template>
<router-view />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'App',
components: {},
})
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
增加路由函数实现跳转
...
//详情页点击详情的使用模板,跳转到编辑页面
<router-link to="/editor">
<a-button type="primary" size="large"> 使用模版 </a-button>
</router-link>
...
...
//首页点击使用该模版创建,跳转到详情页面
<router-link :to="`/template/${1}`">
<a-button size="large" type="primary">使用该模版创建</a-button>
</router-link>
...
<template>
<div class="template-list-component">
<pre>{{ route }}</pre>
...
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
// useRoute(获取路由参数)
import { useRoute } from 'vue-router'
export default defineComponent({
name: 'template-list',
setup() {
const route = useRoute()
return {
route,
}
},
})
</script>
...
...
<script>
import { useRouter } from 'vue-router'
import TemplateList from '@/components/TemplateList.vue'
import { defineComponent } from 'vue'
export default defineComponent({
components: { TemplateList },
setup() {
//useRouter 跳转
const router = useRouter()
setTimeout(() => {
router.push(`/template/${1}`)
}, 2000)
},
})
</script>
...
增加header是否根据不同页面显示显示
<template>
<a-layout>
<a-layout-header><span class="header">乐高</span></a-layout-header>
<a-layout-content>
<div class="content-container">
<router-view />
</div>
</a-layout-content>
<a-layout-footer v-if="withFooter"
>© 慕课网(imooc.com)版权所有</a-layout-footer
>
</a-layout>
</template>
<script lang="ts">
import { defineComponent, toRaw, computed } from 'vue'
import { useRoute } from 'vue-router'
export default defineComponent({
name: 'app',
setup() {
const route = useRoute()
const withFooter = computed(() => route.meta.withFooter)
return {
withFooter,
}
},
})
</script>
<style>
.page-title {
color: #fff;
}
.content-container {
background: #fff;
width: 100%;
}
.header {
color: white;
}
</style>
安装vuex
路由
import { createRouter, createWebHashHistory } from 'vue-router'
const Index = () => import('@/views/Index.vue')
const Home = () => import('@/views/Home.vue')
const Editor = () => import('../views/Editor.vue')
const TemplateDetail = () => import('../views/TemplateDetail.vue')
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
name: 'home',
component: Index,
children: [
//嵌套路由
{
path: '',
name: 'home',
component: Home,
},
{
path: '/template/:id',
name: 'template',
component: TemplateDetail,
},
],
},
{
path: '/editor',
name: 'editor',
component: Editor,
},
],
})
export default router
store
export const LOGIN = 'login'
export const LOGOUT = 'logout'
export const GETTEMPLATEBYID = 'getTemplateById'
import { Module } from 'vuex'
import { GloabalDataProps } from '.'
import { GETTEMPLATEBYID } from './mutation-types'
export interface TemplateProps {
id: number
title: string
coverImg: string
author: string
copiedCount: number
}
export const testData: TemplateProps[] = [
{
id: 1,
coverImg:
'https://static.imooc-lego.com/upload-files/screenshot-889755.png',
title: 'test title 1',
author: 'viking',
copiedCount: 1,
},
{
id: 2,
coverImg:
'https://static.imooc-lego.com/upload-files/screenshot-677311.png',
title: '前端架构师直播海报',
author: 'viking',
copiedCount: 1,
},
{
id: 3,
coverImg:
'https://static.imooc-lego.com/upload-files/screenshot-682056.png',
title: '前端架构师直播海报',
author: 'viking',
copiedCount: 1,
},
{
id: 4,
coverImg:
'https://static.imooc-lego.com/upload-files/screenshot-677311.png',
title: '前端架构师直播海报',
author: 'viking',
copiedCount: 1,
},
{
id: 5,
coverImg:
'https://static.imooc-lego.com/upload-files/screenshot-889755.png',
title: '前端架构师直播海报',
author: 'viking',
copiedCount: 1,
},
{
id: 6,
coverImg:
'https://static.imooc-lego.com/upload-files/screenshot-677311.png',
title: '前端架构师直播海报',
author: 'viking',
copiedCount: 1,
},
]
export interface TemplatesProps {
templateList: TemplateProps[]
}
//两个参数 第一个本地的interface,第二个是全局的interface
const templates: Module<TemplatesProps, GloabalDataProps> = {
state: {
templateList: testData,
},
getters: {
[GETTEMPLATEBYID](state, getters, rootState) {
return (templateId: number) =>
state.templateList.find((item) => item.id === templateId)
},
},
}
export default templates
import { Module } from 'vuex'
import { GloabalDataProps } from '.'
import { LOGIN, LOGOUT } from './mutation-types'
export interface UserProps {
isLogin: boolean
userName?: string
}
const testUser: UserProps = { isLogin: false }
//两个参数 第一个本地的interface,第二个是全局的interface
const user: Module<UserProps, GloabalDataProps> = {
mutations: {
[LOGIN](state) {
state.isLogin = true
state.userName = 'hahaya'
},
[LOGOUT](state) {
state.isLogin = false
},
},
}
export default user
import { createStore } from 'vuex'
import user, { UserProps } from './user'
import templates, { TemplatesProps } from './templates'
export interface GloabalDataProps {
user: UserProps
templates: TemplatesProps
}
const store = createStore<GloabalDataProps>({
modules: {
user,
templates,
},
})
export default store
View
<template>
<div class="home-container">
<template-list :templates="{ templates }"></template-list>
</div>
</template>
<script lang="ts">
import { useRouter } from 'vue-router'
import TemplateList from '@/components/TemplateList.vue'
import { computed, defineComponent, toRaw } from 'vue'
import { GloabalDataProps } from '@/store'
import { useStore } from 'vuex'
import { TemplateProps } from '@/store/templates'
export default defineComponent({
components: { TemplateList },
setup() {
const store = useStore<GloabalDataProps>()
const templates = computed(() => store.state.templates.templateList)
return {
templates,
}
},
})
</script>
<style lang="less" scoped>
.page-title {
color: #fff;
}
.home-container {
background: #fff;
padding: 0 24px 24px 30px;
min-height: 85vh;
max-width: 1200px;
/* margin: 50px auto; */
width: 100%;
}
</style>
<template>
<a-layout>
<a-layout-header>
<router-link to="/">慕课乐高</router-link>
<user-profile :user="{ user }" />
</a-layout-header>
<a-layout-content>
<div class="content-container">
<router-view />
</div>
</a-layout-content>
<a-layout-footer> © 慕课网(imooc.com)版权所有 </a-layout-footer>
</a-layout>
</template>
<script lang="ts">
import { defineComponent, toRaw, computed } from 'vue'
import { useRoute } from 'vue-router'
import UserProfile from '@/components/UserProfile.vue'
import { GloabalDataProps } from '@/store'
import { useStore } from 'vuex'
export default defineComponent({
name: 'index',
components: { UserProfile },
setup() {
const route = useRoute()
const withHeader = computed(() => route.meta.withHeader)
const store = useStore<GloabalDataProps>()
const user = computed(() => store.state.user)
return {
withHeader,
user: user,
}
},
})
</script>
<style lang="less" scoped>
.content-container {
width: 100%;
padding: 20px;
background: #fff;
}
.ant-layout-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
<template>
<div class="work-detail-container">
<a-row type="flex" justify="center">
<a-col :span="8" class="cover-img">
<img :src="template.coverImg" alt="" />
</a-col>
<a-col :span="8">
<h2>{{ template.title }}</h2>
<div class="author">
<a-avatar>V</a-avatar>
该模版由 <b>{{ template.author }}</b> 创作
</div>
<div class="bar-code-area">
<span>扫一扫,手机预览</span>
<div ref="container"></div>
</div>
<div class="use-button">
<router-link to="/editor">
<a-button type="primary" size="large"> 使用模版 </a-button>
</router-link>
<a-button size="large"> 下载图片海报 </a-button>
</div>
</a-col>
</a-row>
</div>
</template>
<script lang="ts">
import { GloabalDataProps } from '@/store'
import { GETTEMPLATEBYID } from '@/store/mutation-types'
import { TemplateProps } from '@/store/templates'
import { defineComponent, computed } from 'vue'
import { useStore } from 'vuex'
import { useRoute } from 'vue-router'
export default defineComponent({
setup() {
const store = useStore<GloabalDataProps>()
const route = useRoute()
const template = computed<TemplateProps>(() =>
store.getters[GETTEMPLATEBYID](Number(route.params.id)),
)
return {
template,
}
},
})
</script>
<style lang="less" scoped>
.work-detail-container {
margin-top: 50px;
}
.cover-img {
margin-right: 30px;
}
.cover-img img {
width: 100%;
}
.use-button {
margin: 30px 0;
}
.ant-avatar {
margin-right: 10px;
}
.bar-code-area {
margin: 20px 0;
}
</style>
components
<template>
<div class="template-list-component">
<a-row :gutter="16">
<a-col
:span="6"
class="poster-item"
v-for="item in templates"
:key="item.id"
>
<a-card hoverable>
<template #cover>
<img :src="item.coverImg" v-if="item.coverImg" />
<img
v-else
src="http://typescript-vue.oss-cn-beijing.aliyuncs.com/vue-marker/5f81cca3f3bf7a0e1ebaf885.png"
/>
<div class="hover-item">
<router-link :to="`/template/${item.id}`">
<a-button size="large" type="primary">使用该模版创建</a-button>
</router-link>
</div>
</template>
<a-card-meta :title="item.title">
<template v-slot:description>
<div class="description-detail">
<span>作者:{{ item.author }}</span>
<span class="user-number">{{ item.copiedCount }}</span>
</div>
</template>
</a-card-meta>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, reactive } from 'vue'
// useRoute(获取路由参数)
import { useRoute } from 'vue-router'
import { TemplateProps, TemplatesProps } from '@/store/templates'
export default defineComponent({
name: 'template-list',
props: {
templates: {
type: Object,
required: true,
},
},
setup(props) {
const route = useRoute()
const templates = computed<TemplateProps[]>(() => props.templates.templates)
return {
route,
templates,
}
},
})
</script>
<style lang="less" scoped>
.poster-item {
position: relative;
margin-bottom: 20px;
}
.poster-item .ant-card {
border-radius: 12px;
}
.poster-item .ant-card-cover {
height: 390px;
}
.poster-item .ant-card-cover > img {
width: 100%;
}
.poster-item .ant-card-hoverable {
box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.1);
}
.poster-item .ant-card-body {
padding: 0;
}
.poster-item .ant-card-meta {
margin: 0;
}
.poster-item .ant-card-meta-title {
color: #333;
padding: 10px 12px;
border-bottom: 1px solid #f2f2f2;
margin-bottom: 0 !important;
}
.description-detail {
display: flex;
justify-content: space-between;
padding: 13px 12px;
color: #999;
}
.user-number {
font-weight: bold;
}
.poster-title {
height: 70px;
}
.poster-title h2 {
margin-bottom: 0px;
}
.poster-item .ant-card-cover {
position: relative;
overflow: hidden;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}
.poster-item .ant-card-cover img {
transition: all ease-in 0.2s;
}
.poster-item .ant-card-cover .hover-item {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: none;
background: rgba(0, 0, 0, 0.8);
align-items: center;
justify-content: center;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}
.poster-item:hover .hover-item {
display: flex;
}
.poster-item:hover img {
transform: scale(1.25);
}
.barcode-container img {
border-radius: 0;
}
</style>
<template>
<a-button
type="primary"
class="user-profile-component"
v-if="!user.isLogin"
@click="login"
>
登录
</a-button>
<div v-else>
<a-dropdown-button class="user-profile-component" type="primary">
{{ user.userName }}
<template #overlay>
<a-menu class="user-profile-dropdown">
<a-menu-item key="1" @click="logout">
<user-outlined />
退出登录
</a-menu-item>
</a-menu>
</template>
</a-dropdown-button>
</div>
</template>
<script lang="ts">
import { useStore } from 'vuex'
import { computed, defineComponent, PropType } from 'vue'
import { useRouter } from 'vue-router'
import { UserProps } from '@/store/user'
import { UserOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { GloabalDataProps } from '@/store'
export default defineComponent({
name: 'user-profile',
components: {
UserOutlined,
},
props: {
user: {
type: Object,
required: true,
},
},
setup(props) {
const store = useStore<GloabalDataProps>()
const router = useRouter()
//登录
const login = () => {
console.log()
store.commit('login')
message.success('登录成功')
}
//登出
const logout = () => {
store.commit('logout')
message.success('退出登录成功,2秒后跳转到首页', 2)
setTimeout(() => {
router.replace('/')
}, 2000)
}
return {
user: props.user.user as UserProps,
login,
logout,
}
},
})
</script>
<style lang="less" scoped>
.user-profile-dropdown {
border-radius: 2px !important;
}
.user-operation > * {
margin-left: 30px !important;
}
</style>