@ -0,0 +1,14 @@ |
|||||||
|
/* eslint-env node */ |
||||||
|
require('@rushstack/eslint-patch/modern-module-resolution') |
||||||
|
|
||||||
|
module.exports = { |
||||||
|
root: true, |
||||||
|
'extends': [ |
||||||
|
'plugin:vue/vue3-essential', |
||||||
|
'eslint:recommended', |
||||||
|
'@vue/eslint-config-prettier/skip-formatting' |
||||||
|
], |
||||||
|
parserOptions: { |
||||||
|
ecmaVersion: 'latest' |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
# Logs |
||||||
|
logs |
||||||
|
*.log |
||||||
|
npm-debug.log* |
||||||
|
yarn-debug.log* |
||||||
|
yarn-error.log* |
||||||
|
pnpm-debug.log* |
||||||
|
lerna-debug.log* |
||||||
|
|
||||||
|
node_modules |
||||||
|
.DS_Store |
||||||
|
dist |
||||||
|
dist-ssr |
||||||
|
coverage |
||||||
|
*.local |
||||||
|
|
||||||
|
/cypress/videos/ |
||||||
|
/cypress/screenshots/ |
||||||
|
|
||||||
|
# Editor directories and files |
||||||
|
.vscode/* |
||||||
|
!.vscode/extensions.json |
||||||
|
.idea |
||||||
|
*.suo |
||||||
|
*.ntvs* |
||||||
|
*.njsproj |
||||||
|
*.sln |
||||||
|
*.sw? |
||||||
|
|
||||||
|
*.tsbuildinfo |
@ -0,0 +1,8 @@ |
|||||||
|
{ |
||||||
|
"$schema": "https://json.schemastore.org/prettierrc", |
||||||
|
"semi": false, |
||||||
|
"tabWidth": 4, |
||||||
|
"singleQuote": true, |
||||||
|
"printWidth": 100, |
||||||
|
"trailingComma": "none" |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
{ |
||||||
|
"recommendations": [ |
||||||
|
"Vue.volar", |
||||||
|
"dbaeumer.vscode-eslint", |
||||||
|
"esbenp.prettier-vscode" |
||||||
|
] |
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
# vue3-test |
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite. |
||||||
|
|
||||||
|
## Recommended IDE Setup |
||||||
|
|
||||||
|
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). |
||||||
|
|
||||||
|
## Customize configuration |
||||||
|
|
||||||
|
See [Vite Configuration Reference](https://vitejs.dev/config/). |
||||||
|
|
||||||
|
## Project Setup |
||||||
|
|
||||||
|
```sh |
||||||
|
npm install |
||||||
|
``` |
||||||
|
|
||||||
|
### Compile and Hot-Reload for Development |
||||||
|
|
||||||
|
```sh |
||||||
|
npm run dev |
||||||
|
``` |
||||||
|
|
||||||
|
### Compile and Minify for Production |
||||||
|
|
||||||
|
```sh |
||||||
|
npm run build |
||||||
|
``` |
||||||
|
|
||||||
|
### Lint with [ESLint](https://eslint.org/) |
||||||
|
|
||||||
|
```sh |
||||||
|
npm run lint |
||||||
|
``` |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 88 KiB |
@ -0,0 +1,910 @@ |
|||||||
|
# 基础 |
||||||
|
|
||||||
|
```bash |
||||||
|
npm create vue@latest |
||||||
|
``` |
||||||
|
|
||||||
|
## 创建应用 |
||||||
|
|
||||||
|
### 应用配置 |
||||||
|
|
||||||
|
> **确保在挂载应用实例之前完成所有应用配置!** |
||||||
|
|
||||||
|
- 应用级的错误处理器,用来捕获所有子组件上的错误 |
||||||
|
|
||||||
|
```javascript |
||||||
|
app.config.errorHandler = (err) => { |
||||||
|
/* 处理错误 */ |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
- 注册全局资源,**注册一个组件** |
||||||
|
|
||||||
|
```javascript |
||||||
|
app.component('TodoDeleteButton', TodoDeleteButton) |
||||||
|
``` |
||||||
|
|
||||||
|
### 多实例 |
||||||
|
|
||||||
|
```javascript |
||||||
|
const app1 = createApp({ |
||||||
|
/* ... */ |
||||||
|
}) |
||||||
|
app1.mount('#container-1') |
||||||
|
|
||||||
|
const app2 = createApp({ |
||||||
|
/* ... */ |
||||||
|
}) |
||||||
|
app2.mount('#container-2') |
||||||
|
``` |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 模板语法 |
||||||
|
|
||||||
|
> 如果偏好使用JavaScript,JSX 支持直接**手写渲染函数**而不采用模板。但JSX不会享受到和模板同等级别的**编译时优化**。 |
||||||
|
|
||||||
|
### 文本插值 |
||||||
|
|
||||||
|
```vue |
||||||
|
<span>Message: {{ msg }}</span> |
||||||
|
``` |
||||||
|
|
||||||
|
### 使用原始HTML,不被vue编译 |
||||||
|
|
||||||
|
```vue |
||||||
|
<p>Using text interpolation: {{ rawHtml }}</p> |
||||||
|
<p>Using v-html directive: <span v-html="rawHtml"></span></p> |
||||||
|
``` |
||||||
|
|
||||||
|
![原始HTML](./vue3.assets/c0eed55cafc0418c813d4a6e77ed2835.png) |
||||||
|
|
||||||
|
![安全警告](./vue3.assets/3b3bd03e11464112b94ed3afd66e7fbf.png) |
||||||
|
|
||||||
|
### Attribute 绑定 |
||||||
|
|
||||||
|
```vue |
||||||
|
<div v-bind:id="dynamicId"></div> |
||||||
|
<!--简写--> |
||||||
|
<div :id="dynamicId"></div> |
||||||
|
|
||||||
|
<!--同名简写 版本>=3.4--> |
||||||
|
<!-- 与 :id="id" 相同 --> |
||||||
|
<div :id></div> |
||||||
|
|
||||||
|
<!-- 这也同样有效 --> |
||||||
|
<div v-bind:id></div> |
||||||
|
``` |
||||||
|
|
||||||
|
### _布尔型 Attribute ???_ |
||||||
|
|
||||||
|
当 isButtonDisabled 为真值或一个空字符串 (即 `<button disabled="">`) 时,元素会包含这个 disabled attribute。而当其为其他假值时 attribute 将被忽略。 |
||||||
|
|
||||||
|
### 动态绑定多个值 |
||||||
|
|
||||||
|
```vue |
||||||
|
const objectOfAttrs = { |
||||||
|
id: 'container', |
||||||
|
class: 'wrapper' |
||||||
|
} |
||||||
|
<div v-bind="objectOfAttrs"></div> |
||||||
|
``` |
||||||
|
|
||||||
|
### 使用 JavaScript 表达式 |
||||||
|
|
||||||
|
```vue |
||||||
|
{{ number + 1 }} |
||||||
|
|
||||||
|
{{ ok ? 'YES' : 'NO' }} |
||||||
|
|
||||||
|
{{ message.split('').reverse().join('') }} |
||||||
|
|
||||||
|
<div :id="`list-${id}`"></div> |
||||||
|
``` |
||||||
|
|
||||||
|
### 调用函数 |
||||||
|
|
||||||
|
```vue |
||||||
|
<time :title="toTitleDate(date)" :datetime="date"> |
||||||
|
{{ formatDate(date) }} |
||||||
|
</time> |
||||||
|
``` |
||||||
|
|
||||||
|
### 受限的全局访问 |
||||||
|
|
||||||
|
- Math 和 Date可以在标签中使用,其他的需要在配置中注册,`app.config.globalProperties` |
||||||
|
|
||||||
|
```js |
||||||
|
app.config.globalProperties.msg = 'hello' |
||||||
|
|
||||||
|
export default { |
||||||
|
mounted() { |
||||||
|
console.log(this.msg) // 'hello' |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### 动态参数 |
||||||
|
|
||||||
|
```vue |
||||||
|
<!-- |
||||||
|
注意,参数表达式有一些约束, |
||||||
|
参见下面“动态参数值的限制”与“动态参数语法的限制”章节的解释 |
||||||
|
--> |
||||||
|
<a v-bind:[attributeName]="url"> ... </a> |
||||||
|
|
||||||
|
<!-- 简写 --> |
||||||
|
<a :[attributeName]="url"> ... </a> |
||||||
|
``` |
||||||
|
|
||||||
|
### 动态事件 |
||||||
|
|
||||||
|
```vue |
||||||
|
<a v-on:[eventName]="doSomething"> ... </a> |
||||||
|
|
||||||
|
<!-- 简写 --> |
||||||
|
<a @[eventName]="doSomething"> ... </a> |
||||||
|
``` |
||||||
|
|
||||||
|
- 不要这样写 |
||||||
|
- 动态参数表达式因为某些字符的缘故有一些语法限制,比如空格和引号,在 HTML attribute 名称中都是不合法的。 |
||||||
|
```vue |
||||||
|
<!-- 这会触发一个编译器警告 --> |
||||||
|
<template> |
||||||
|
<!-- <a :['foo' + bar]="value"> ... </a>--> |
||||||
|
</template> |
||||||
|
|
||||||
|
``` |
||||||
|
|
||||||
|
### _修饰符 Modifiers ?_ |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 响应式基础 |
||||||
|
|
||||||
|
### 声明响应式状态 |
||||||
|
|
||||||
|
### ref的实际样子 |
||||||
|
|
||||||
|
```js |
||||||
|
const count = ref(0) |
||||||
|
|
||||||
|
console.log(count) // { value: 0 } |
||||||
|
console.log(count.value) // 0 |
||||||
|
|
||||||
|
count.value++ |
||||||
|
console.log(count.value) // 1 |
||||||
|
``` |
||||||
|
|
||||||
|
### 使用ref |
||||||
|
|
||||||
|
```js |
||||||
|
import { ref } from 'vue' |
||||||
|
|
||||||
|
export default { |
||||||
|
setup() { |
||||||
|
const count = ref(0) |
||||||
|
|
||||||
|
function increment() { |
||||||
|
// 在 JavaScript 中需要 .value |
||||||
|
count.value++ |
||||||
|
} |
||||||
|
|
||||||
|
// 不要忘记同时暴露 increment 函数 |
||||||
|
return { |
||||||
|
count, |
||||||
|
increment |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
```vue |
||||||
|
<div>{{ count }}</div> |
||||||
|
<button @click="count++"> |
||||||
|
{{ count }} |
||||||
|
</button> |
||||||
|
``` |
||||||
|
|
||||||
|
### `<script setup>` |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
import { ref } from 'vue' |
||||||
|
|
||||||
|
const count = ref(0) |
||||||
|
|
||||||
|
function increment() { |
||||||
|
count.value++ |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<button @click="increment"> |
||||||
|
{{ count }} |
||||||
|
</button> |
||||||
|
</template> |
||||||
|
``` |
||||||
|
|
||||||
|
### DOM 更新时机 |
||||||
|
|
||||||
|
```js |
||||||
|
import { nextTick } from 'vue' |
||||||
|
|
||||||
|
async function increment() { |
||||||
|
count.value++ |
||||||
|
await nextTick() |
||||||
|
// 现在 DOM 已经更新了 |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### reactive() |
||||||
|
|
||||||
|
- `reactive()` 将使对象本身具有响应性 |
||||||
|
|
||||||
|
```vue |
||||||
|
|
||||||
|
<script setup> |
||||||
|
import { reactive } from 'vue' |
||||||
|
const state = reactive({ count: 0 }) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<button @click="state.count++"> |
||||||
|
{{ state.count }} |
||||||
|
</button> |
||||||
|
</template> |
||||||
|
``` |
||||||
|
|
||||||
|
- `reactive()` 返回的是一个原始对象的 Proxy |
||||||
|
|
||||||
|
```js |
||||||
|
const raw = {} |
||||||
|
const proxy = reactive(raw) |
||||||
|
|
||||||
|
// 代理对象和原始对象不是全等的 |
||||||
|
console.log(proxy === raw) // false |
||||||
|
|
||||||
|
// 在同一个对象上调用 reactive() 会返回相同的代理 |
||||||
|
console.log(reactive(raw) === proxy) // true |
||||||
|
|
||||||
|
// 在一个代理上调用 reactive() 会返回它自己 |
||||||
|
console.log(reactive(proxy) === proxy) // true |
||||||
|
``` |
||||||
|
|
||||||
|
- **这个规则对嵌套对象也适用。依靠深层响应性,响应式对象内的嵌套对象依然是代理:** |
||||||
|
|
||||||
|
```js |
||||||
|
const proxy = reactive({}) |
||||||
|
|
||||||
|
const raw = {} |
||||||
|
proxy.nested = raw |
||||||
|
|
||||||
|
console.log(proxy.nested === raw) // false |
||||||
|
``` |
||||||
|
|
||||||
|
### `reactive()` 的局限性 |
||||||
|
|
||||||
|
- 有限的值类型:不能持有如 string、number 或 boolean 这样的原始类型。 |
||||||
|
|
||||||
|
- 不能替换整个对象: |
||||||
|
|
||||||
|
```js |
||||||
|
let state = reactive({ count: 0 }) |
||||||
|
|
||||||
|
// 上面的 ({ count: 0 }) 引用将不再被追踪 |
||||||
|
// (响应性连接已丢失!) |
||||||
|
state = reactive({ count: 1 }) |
||||||
|
``` |
||||||
|
|
||||||
|
- 对解构操作不友好 |
||||||
|
|
||||||
|
```js |
||||||
|
const state = reactive({ count: 0 }) |
||||||
|
|
||||||
|
// 当解构时,count 已经与 state.count 断开连接 |
||||||
|
let { count } = state |
||||||
|
// 不会影响原始的 state |
||||||
|
count++ |
||||||
|
|
||||||
|
// 该函数接收到的是一个普通的数字 |
||||||
|
// 并且无法追踪 state.count 的变化 |
||||||
|
// 我们必须传入整个对象以保持响应性 |
||||||
|
callSomeFunction(state.count) |
||||||
|
``` |
||||||
|
|
||||||
|
## 计算属性 |
||||||
|
|
||||||
|
### 基础示例 |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
import { reactive, computed } from 'vue' |
||||||
|
|
||||||
|
const author = reactive({ |
||||||
|
name: 'John Doe', |
||||||
|
books: [ |
||||||
|
'Vue 2 - Advanced Guide', |
||||||
|
'Vue 3 - Basic Guide', |
||||||
|
'Vue 4 - The Mystery' |
||||||
|
] |
||||||
|
}) |
||||||
|
|
||||||
|
// 一个计算属性 ref |
||||||
|
const publishedBooksMessage = computed(() => { |
||||||
|
return author.books.length > 0 ? 'Yes' : 'No' |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<p>Has published books:</p> |
||||||
|
<span>{{ publishedBooksMessage }}</span> |
||||||
|
</template> |
||||||
|
``` |
||||||
|
|
||||||
|
- 计算属性有缓存,当响应值没变化时,计算属性不会执行,如果用函数的话,函数每次都会执行 |
||||||
|
|
||||||
|
```js |
||||||
|
// now永远不会更新,因为他没有用到响应值 |
||||||
|
const now = computed(() => Date.now()) |
||||||
|
``` |
||||||
|
|
||||||
|
### 可写计算属性 |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
import { ref, computed } from 'vue' |
||||||
|
|
||||||
|
const firstName = ref('John') |
||||||
|
const lastName = ref('Doe') |
||||||
|
|
||||||
|
const fullName = computed({ |
||||||
|
// getter |
||||||
|
get() { |
||||||
|
return firstName.value + ' ' + lastName.value |
||||||
|
}, |
||||||
|
// setter |
||||||
|
set(newValue) { |
||||||
|
// 注意:我们这里使用的是解构赋值语法 |
||||||
|
[firstName.value, lastName.value] = newValue.split(' ') |
||||||
|
} |
||||||
|
}) |
||||||
|
</script> |
||||||
|
``` |
||||||
|
|
||||||
|
## 类与样式绑定 |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
const isActive = ref(true) |
||||||
|
const hasError = ref(false) |
||||||
|
</script> |
||||||
|
<template> |
||||||
|
<div |
||||||
|
class="static" |
||||||
|
:class="{ active: isActive, 'text-danger': hasError }" |
||||||
|
></div> |
||||||
|
</template> |
||||||
|
``` |
||||||
|
|
||||||
|
渲染结果: |
||||||
|
|
||||||
|
`<div class="static active"></div>` |
||||||
|
|
||||||
|
### 组件的类会叠加 |
||||||
|
|
||||||
|
```vue |
||||||
|
<!-- 子组件模板 --> |
||||||
|
<p class="foo bar">Hi!</p> |
||||||
|
|
||||||
|
<!-- 在使用组件时 --> |
||||||
|
<MyComponent class="baz boo" /> |
||||||
|
|
||||||
|
<!--渲染出的 HTML 为:--> |
||||||
|
<p class="foo bar baz boo">Hi!</p> |
||||||
|
``` |
||||||
|
|
||||||
|
## 条件渲染基本没变 |
||||||
|
|
||||||
|
```vue |
||||||
|
<template v-if="ok"> |
||||||
|
<h1>Title</h1> |
||||||
|
<p>Paragraph 1</p> |
||||||
|
<p>Paragraph 2</p> |
||||||
|
</template> |
||||||
|
``` |
||||||
|
|
||||||
|
## 列表渲染 |
||||||
|
|
||||||
|
### `v-for`可以遍历对象,顺序基于Object.keys() |
||||||
|
|
||||||
|
### 使用范围值 【整数值】 |
||||||
|
|
||||||
|
```vue |
||||||
|
<!--n从1开始--> |
||||||
|
<span v-for="n in 10">{{ n }}</span> |
||||||
|
``` |
||||||
|
|
||||||
|
### `<template>` 上也可以用 v-for |
||||||
|
|
||||||
|
## 事件处理 |
||||||
|
|
||||||
|
- `v-on` 指令 (简写为 `@`) |
||||||
|
|
||||||
|
### 内联事件处理器 |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
const count = ref(0) |
||||||
|
</script> |
||||||
|
<template> |
||||||
|
<button @click="count++">Add 1</button> |
||||||
|
<p>Count is: {{ count }}</p> |
||||||
|
</template> |
||||||
|
``` |
||||||
|
|
||||||
|
### 方法事件处理器 |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
const name = ref('Vue.js') |
||||||
|
|
||||||
|
function greet(event) { |
||||||
|
alert(`Hello ${name.value}!`) |
||||||
|
// `event` 是 DOM 原生事件 |
||||||
|
if (event) { |
||||||
|
alert(event.target.tagName) |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
<template> |
||||||
|
<!-- `greet` 是上面定义过的方法名 --> |
||||||
|
<button @click="greet">Greet</button> |
||||||
|
</template> |
||||||
|
``` |
||||||
|
|
||||||
|
### 在内联事件处理器中访问事件参数 |
||||||
|
|
||||||
|
- `$event` |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
function warn(message, event) { |
||||||
|
// 这里可以访问原生事件 |
||||||
|
if (event) { |
||||||
|
event.preventDefault() |
||||||
|
} |
||||||
|
alert(message) |
||||||
|
} |
||||||
|
</script> |
||||||
|
<template> |
||||||
|
<!-- 使用特殊的 $event 变量 --> |
||||||
|
<button @click="warn('Form cannot be submitted yet.', $event)"> |
||||||
|
Submit |
||||||
|
</button> |
||||||
|
|
||||||
|
<!-- 使用内联箭头函数 --> |
||||||
|
<button @click="(event) => warn('Form cannot be submitted yet.', event)"> |
||||||
|
Submit |
||||||
|
</button> |
||||||
|
</template> |
||||||
|
``` |
||||||
|
|
||||||
|
### 事件修饰符 |
||||||
|
|
||||||
|
- .stop |
||||||
|
- .prevent |
||||||
|
- .self |
||||||
|
- .capture |
||||||
|
- .once |
||||||
|
- .passive |
||||||
|
|
||||||
|
```vue |
||||||
|
<!-- 单击事件将停止传递 --> |
||||||
|
<a @click.stop="doThis"></a> |
||||||
|
|
||||||
|
<!-- 提交事件将不再重新加载页面 --> |
||||||
|
<form @submit.prevent="onSubmit"></form> |
||||||
|
|
||||||
|
<!-- 修饰语可以使用链式书写 --> |
||||||
|
<a @click.stop.prevent="doThat"></a> |
||||||
|
|
||||||
|
<!-- 也可以只有修饰符 --> |
||||||
|
<form @submit.prevent></form> |
||||||
|
|
||||||
|
<!-- 仅当 event.target 是元素本身时才会触发事件处理器 --> |
||||||
|
<!-- 例如:事件处理器不来自子元素 --> |
||||||
|
<div @click.self="doThat">...</div> |
||||||
|
``` |
||||||
|
|
||||||
|
![](./vue3.assets/8761f9becf244a3e93973c3a03858dc6.png) |
||||||
|
|
||||||
|
`.capture`、`.once` 和 `.passive` 修饰符与原生 addEventListener 事件相对应: |
||||||
|
|
||||||
|
```vue |
||||||
|
<!-- 添加事件监听器时,使用 `capture` 捕获模式 --> |
||||||
|
<!-- 例如:指向内部元素的事件,在被内部元素处理前,先被外部处理 --> |
||||||
|
<div @click.capture="doThis">...</div> |
||||||
|
|
||||||
|
<!-- 点击事件最多被触发一次 --> |
||||||
|
<a @click.once="doThis"></a> |
||||||
|
|
||||||
|
<!-- 滚动事件的默认行为 (scrolling) 将立即发生而非等待 `onScroll` 完成 --> |
||||||
|
<!-- 以防其中包含 `event.preventDefault()` --> |
||||||
|
<div @scroll.passive="onScroll">...</div> |
||||||
|
``` |
||||||
|
|
||||||
|
`.passive` 修饰符一般用于触摸事件的监听器,可以用来改善移动端设备的滚屏性能。 |
||||||
|
|
||||||
|
![](./vue3.assets/48618779e6cc4d9c82bacbc11a59dcae.png) |
||||||
|
|
||||||
|
|
||||||
|
### 按键修饰符 |
||||||
|
|
||||||
|
- **$event.key** |
||||||
|
|
||||||
|
- 按键别名 |
||||||
|
- .enter |
||||||
|
- .tab |
||||||
|
- .delete (捕获“Delete”和“Backspace”两个按键) |
||||||
|
- .esc |
||||||
|
- .space |
||||||
|
- .up |
||||||
|
- .down |
||||||
|
- .left |
||||||
|
- .right |
||||||
|
- 系统按键修饰符 |
||||||
|
- .ctrl |
||||||
|
- .alt |
||||||
|
- .shift |
||||||
|
- .meta |
||||||
|
|
||||||
|
```vue |
||||||
|
<!-- Alt + Enter --> |
||||||
|
<input @keyup.alt.enter="clear" /> |
||||||
|
|
||||||
|
<!-- Ctrl + 点击 --> |
||||||
|
<div @click.ctrl="doSomething">Do something</div> |
||||||
|
``` |
||||||
|
|
||||||
|
### `.exact` 修饰符允许精确控制触发事件所需的系统修饰符的组合。 |
||||||
|
|
||||||
|
```vue |
||||||
|
<!-- 当按下 Ctrl 时,即使同时按下 Alt 或 Shift 也会触发 --> |
||||||
|
<button @click.ctrl="onClick">A</button> |
||||||
|
|
||||||
|
<!-- 仅当按下 Ctrl 且未按任何其他键时才会触发 --> |
||||||
|
<button @click.ctrl.exact="onCtrlClick">A</button> |
||||||
|
|
||||||
|
<!-- 仅当没有按下任何系统按键时触发 --> |
||||||
|
<button @click.exact="onClick">A</button> |
||||||
|
``` |
||||||
|
|
||||||
|
### 鼠标按键修饰符 |
||||||
|
|
||||||
|
- .left |
||||||
|
- .right |
||||||
|
- .middle |
||||||
|
|
||||||
|
## 生命周期 |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
import { onMounted } from 'vue' |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
console.log(`the component is now mounted.`) |
||||||
|
}) |
||||||
|
</script> |
||||||
|
``` |
||||||
|
|
||||||
|
![生命周期](./vue3.assets/ecdb8a0d231140ab8584b83992c41b62.png) |
||||||
|
|
||||||
|
## 侦听器 |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
import { ref, watch } from 'vue' |
||||||
|
const x = ref(0) |
||||||
|
const y = ref(0) |
||||||
|
|
||||||
|
// 单个 ref |
||||||
|
watch(x, (newX, oldX) => { |
||||||
|
console.log(`x is ${newX}`) |
||||||
|
}) |
||||||
|
|
||||||
|
// getter 函数 |
||||||
|
watch( |
||||||
|
() => x.value + y.value, |
||||||
|
(sum) => { |
||||||
|
console.log(`sum of x + y is: ${sum}`) |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
// 多个来源组成的数组 |
||||||
|
watch([x, () => y.value], ([newX, newY]) => { |
||||||
|
console.log(`x is ${newX} and y is ${newY}`) |
||||||
|
}) |
||||||
|
</script> |
||||||
|
``` |
||||||
|
|
||||||
|
- 不能直接侦听响应式对象的属性值 |
||||||
|
|
||||||
|
```js |
||||||
|
const obj = reactive({ count: 0 }) |
||||||
|
|
||||||
|
// 错误,因为 watch() 得到的参数是一个 number |
||||||
|
watch(obj.count, (count) => { |
||||||
|
console.log(`count is: ${count}`) |
||||||
|
}) |
||||||
|
|
||||||
|
// 提供一个 getter 函数 |
||||||
|
watch( |
||||||
|
() => obj.count, |
||||||
|
(count) => { |
||||||
|
console.log(`count is: ${count}`) |
||||||
|
} |
||||||
|
) |
||||||
|
``` |
||||||
|
|
||||||
|
### 深层侦听器 `{ deep: true }` |
||||||
|
|
||||||
|
```js |
||||||
|
const obj = reactive({ count: 0 }) |
||||||
|
|
||||||
|
watch(obj, (newValue, oldValue) => { |
||||||
|
// 在嵌套的属性变更时触发 |
||||||
|
// 注意:`newValue` 此处和 `oldValue` 是相等的 |
||||||
|
// 因为它们是同一个对象! |
||||||
|
}) |
||||||
|
|
||||||
|
watch( |
||||||
|
() => state.someObject, |
||||||
|
(newValue, oldValue) => { |
||||||
|
// 注意:`newValue` 此处和 `oldValue` 是相等的 |
||||||
|
// *除非* state.someObject 被整个替换了 |
||||||
|
}, |
||||||
|
{ deep: true } |
||||||
|
) |
||||||
|
|
||||||
|
obj.count++ |
||||||
|
``` |
||||||
|
|
||||||
|
![](./vue3.assets/d791b0e7431b492b9d5dafe186d884b0.png) |
||||||
|
|
||||||
|
### 即时回调的侦听器,**在创建侦听器时,立即执行一遍回调** `{ immediate: true }` |
||||||
|
|
||||||
|
```js |
||||||
|
watch( |
||||||
|
source, |
||||||
|
(newValue, oldValue) => { |
||||||
|
// 立即执行,且当 `source` 改变时再次执行 |
||||||
|
}, |
||||||
|
{ immediate: true } |
||||||
|
) |
||||||
|
``` |
||||||
|
|
||||||
|
### 一次性侦听器 (版本>=3.4) |
||||||
|
|
||||||
|
```js |
||||||
|
watch( |
||||||
|
source, |
||||||
|
(newValue, oldValue) => { |
||||||
|
// 当 `source` 变化时,仅触发一次 |
||||||
|
}, |
||||||
|
{ once: true } |
||||||
|
) |
||||||
|
``` |
||||||
|
|
||||||
|
### `watchEffect()`, 如果函数体内有响应式参数被调用,则会自动执行,且会立即执行一次 |
||||||
|
|
||||||
|
```js |
||||||
|
watchEffect(async () => { |
||||||
|
const response = await fetch( |
||||||
|
`https://jsonplaceholder.typicode.com/todos/${todoId.value}` |
||||||
|
) |
||||||
|
data.value = await response.json() |
||||||
|
}) |
||||||
|
``` |
||||||
|
|
||||||
|
### _回调的触发时机 ?_ |
||||||
|
|
||||||
|
### 停止侦听器 |
||||||
|
|
||||||
|
```js |
||||||
|
const unwatch = watchEffect(() => {}) |
||||||
|
|
||||||
|
// ...当该侦听器不再需要时 |
||||||
|
unwatch() |
||||||
|
``` |
||||||
|
|
||||||
|
## 模板引用 |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
import { ref, onMounted } from 'vue' |
||||||
|
|
||||||
|
// 声明一个 ref 来存放该元素的引用 |
||||||
|
// 必须和模板里的 ref 同名 |
||||||
|
const input = ref(null) |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
input.value.focus() |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<input ref="input" /> |
||||||
|
</template> |
||||||
|
``` |
||||||
|
|
||||||
|
### `v-for` 中的模板引用 |
||||||
|
|
||||||
|
- 应该注意的是,ref 数组并不保证与源数组相同的顺序。 |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
import { ref, onMounted } from 'vue' |
||||||
|
|
||||||
|
const list = ref([ |
||||||
|
/* ... */ |
||||||
|
]) |
||||||
|
|
||||||
|
const itemRefs = ref([]) |
||||||
|
|
||||||
|
onMounted(() => console.log(itemRefs.value)) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<ul> |
||||||
|
<li v-for="item in list" ref="itemRefs"> |
||||||
|
{{ item }} |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
</template> |
||||||
|
``` |
||||||
|
|
||||||
|
### 函数模板引用 |
||||||
|
|
||||||
|
```vue |
||||||
|
<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }"> |
||||||
|
``` |
||||||
|
|
||||||
|
### 组件上的 ref |
||||||
|
|
||||||
|
- `setup`组合式组件,需要手动暴露子组件的变量 |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
import { ref } from 'vue' |
||||||
|
|
||||||
|
const a = 1 |
||||||
|
const b = ref(2) |
||||||
|
|
||||||
|
// 像 defineExpose 这样的编译器宏不需要导入 |
||||||
|
defineExpose({ |
||||||
|
a, |
||||||
|
b |
||||||
|
}) |
||||||
|
</script> |
||||||
|
``` |
||||||
|
|
||||||
|
## 组件基础 |
||||||
|
|
||||||
|
### 单文件组件 (简称 SFC) `.vue` |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
import { ref } from 'vue' |
||||||
|
|
||||||
|
const count = ref(0) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<button @click="count++">You clicked me {{ count }} times.</button> |
||||||
|
</template> |
||||||
|
``` |
||||||
|
|
||||||
|
### `.js` |
||||||
|
|
||||||
|
```js |
||||||
|
import { ref } from 'vue' |
||||||
|
|
||||||
|
export default { |
||||||
|
setup() { |
||||||
|
const count = ref(0) |
||||||
|
return { count } |
||||||
|
}, |
||||||
|
template: ` |
||||||
|
<button @click="count++"> |
||||||
|
You clicked me {{ count }} times. |
||||||
|
</button>` |
||||||
|
// 也可以针对一个 DOM 内联模板: |
||||||
|
// template: '#my-template-element' |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### 传递 props |
||||||
|
|
||||||
|
```vue |
||||||
|
<!-- BlogPost.vue --> |
||||||
|
<script setup> |
||||||
|
defineProps(['title']) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<h4>{{ title }}</h4> |
||||||
|
</template> |
||||||
|
``` |
||||||
|
|
||||||
|
- 如果你没有使用 <script setup>,props 必须以 props 选项的方式声明,props 对象会作为 setup() 函数的第一个参数被传入: |
||||||
|
|
||||||
|
```js |
||||||
|
export default { |
||||||
|
props: ['title'], |
||||||
|
setup(props) { |
||||||
|
console.log(props.title) |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### 自定义监听事件 |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
defineProps(['title']) |
||||||
|
defineEmits(['enlarge-text']) |
||||||
|
|
||||||
|
// 在setp中执行 |
||||||
|
// SFC组合式 |
||||||
|
const emit = defineEmits(['enlarge-text']) |
||||||
|
|
||||||
|
emit('enlarge-text') |
||||||
|
// 选项式 |
||||||
|
export default { |
||||||
|
emits: ['enlarge-text'], |
||||||
|
setup(props, ctx) { |
||||||
|
ctx.emit('enlarge-text') |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
<template> |
||||||
|
<button @click="$emit('enlarge-text')">Enlarge text</button> |
||||||
|
</template> |
||||||
|
``` |
||||||
|
|
||||||
|
```vue |
||||||
|
<!--父组件--> |
||||||
|
<script setup> |
||||||
|
function oneMethod(event){ |
||||||
|
|
||||||
|
} |
||||||
|
</script> |
||||||
|
<template> |
||||||
|
<BlogPost @enlarge-text="oneMethod"/> |
||||||
|
</template> |
||||||
|
``` |
||||||
|
|
||||||
|
### 插槽 |
||||||
|
|
||||||
|
```vue |
||||||
|
<template> |
||||||
|
<div class="alert-box"> |
||||||
|
<strong>This is an Error for Demo Purposes</strong> |
||||||
|
<slot /> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
.alert-box { |
||||||
|
/* ... */ |
||||||
|
} |
||||||
|
</style> |
||||||
|
``` |
||||||
|
|
||||||
|
### 动态组件 |
||||||
|
|
||||||
|
```vue |
||||||
|
<!-- currentTab 改变时组件也改变 --> |
||||||
|
<component :is="tabs[currentTab]"></component> |
||||||
|
``` |
@ -0,0 +1,5 @@ |
|||||||
|
# 内置组件 |
||||||
|
|
||||||
|
## Transition |
||||||
|
|
||||||
|
- 制作基于状态变化的过渡和动画 |
After Width: | Height: | Size: 41 KiB |
@ -0,0 +1,555 @@ |
|||||||
|
# 深入组件 |
||||||
|
|
||||||
|
## 组件注册 |
||||||
|
|
||||||
|
### 全局注册 |
||||||
|
|
||||||
|
```js |
||||||
|
import { createApp } from 'vue' |
||||||
|
|
||||||
|
const app = createApp({}) |
||||||
|
|
||||||
|
app.component( |
||||||
|
// 注册的名字 |
||||||
|
'MyComponent', |
||||||
|
// 组件的实现 |
||||||
|
{ |
||||||
|
/* ... */ |
||||||
|
} |
||||||
|
) |
||||||
|
// 单文件组件 |
||||||
|
import MyComponent from './App.vue' |
||||||
|
app.component('MyComponent', MyComponent) |
||||||
|
|
||||||
|
// .component() 方法可以被链式调用 |
||||||
|
app |
||||||
|
.component('ComponentA', ComponentA) |
||||||
|
.component('ComponentB', ComponentB) |
||||||
|
.component('ComponentC', ComponentC) |
||||||
|
``` |
||||||
|
|
||||||
|
### 命名 |
||||||
|
|
||||||
|
- PascalCase:大驼峰 |
||||||
|
|
||||||
|
|
||||||
|
## Props |
||||||
|
|
||||||
|
### Props 声明 |
||||||
|
|
||||||
|
- 使用 `<script setup>` |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
const props = defineProps(['foo']) |
||||||
|
|
||||||
|
console.log(props.foo) |
||||||
|
</script> |
||||||
|
``` |
||||||
|
|
||||||
|
- 没有使用 `<script setup>` |
||||||
|
|
||||||
|
```js |
||||||
|
export default { |
||||||
|
props: ['foo'], |
||||||
|
setup(props) { |
||||||
|
// setup() 接收 props 作为第一个参数 |
||||||
|
console.log(props.foo) |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### 使用一个对象绑定多个 prop |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
const post = { |
||||||
|
id: 1, |
||||||
|
title: 'My Journey with Vue' |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<BlogPost v-bind="post" /> |
||||||
|
<!--等价于:--> |
||||||
|
<BlogPost :id="post.id" :title="post.title" /> |
||||||
|
</template> |
||||||
|
``` |
||||||
|
|
||||||
|
### 单向数据流 |
||||||
|
|
||||||
|
- 不能重新赋值props |
||||||
|
- 能修改引用类型,但是会造成损耗不推荐 |
||||||
|
|
||||||
|
### Prop 校验 |
||||||
|
|
||||||
|
```js |
||||||
|
defineProps({ |
||||||
|
// 基础类型检查 |
||||||
|
// (给出 `null` 和 `undefined` 值则会跳过任何类型检查) |
||||||
|
propA: Number, |
||||||
|
// 多种可能的类型 |
||||||
|
propB: [String, Number], |
||||||
|
// 必传,且为 String 类型 |
||||||
|
propC: { |
||||||
|
type: String, |
||||||
|
required: true |
||||||
|
}, |
||||||
|
// Number 类型的默认值 |
||||||
|
propD: { |
||||||
|
type: Number, |
||||||
|
default: 100 |
||||||
|
}, |
||||||
|
// 对象类型的默认值 |
||||||
|
propE: { |
||||||
|
type: Object, |
||||||
|
// 对象或数组的默认值 |
||||||
|
// 必须从一个工厂函数返回。 |
||||||
|
// 该函数接收组件所接收到的原始 prop 作为参数。 |
||||||
|
default(rawProps) { |
||||||
|
return { message: 'hello' } |
||||||
|
} |
||||||
|
}, |
||||||
|
// 自定义类型校验函数 |
||||||
|
// 在 3.4+ 中完整的 props 作为第二个参数传入 |
||||||
|
propF: { |
||||||
|
validator(value, props) { |
||||||
|
// The value must match one of these strings |
||||||
|
return ['success', 'warning', 'danger'].includes(value) |
||||||
|
} |
||||||
|
}, |
||||||
|
// 函数类型的默认值 |
||||||
|
propG: { |
||||||
|
type: Function, |
||||||
|
// 不像对象或数组的默认,这不是一个 |
||||||
|
// 工厂函数。这会是一个用来作为默认值的函数 |
||||||
|
default() { |
||||||
|
return 'Default function' |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
``` |
||||||
|
|
||||||
|
## 组件事件 |
||||||
|
|
||||||
|
```vue |
||||||
|
<!-- MyComponent --> |
||||||
|
<button @click="$emit('someEvent')">click me</button> |
||||||
|
|
||||||
|
<!--推荐使用 kebab-case 形式来编写监听器--> |
||||||
|
<MyComponent @some-event="callback" /> |
||||||
|
<MyComponent @some-event.once="callback" /> |
||||||
|
``` |
||||||
|
|
||||||
|
![](./vue3深入组件.assets/3d9f574cc12244ecb8dcab9ef49d7697.png) |
||||||
|
|
||||||
|
### 事件参数 |
||||||
|
|
||||||
|
```vue |
||||||
|
<button @click="$emit('increaseBy', 1, 2, 3)"> |
||||||
|
Increase by 1 |
||||||
|
</button> |
||||||
|
|
||||||
|
<MyButton @increase-by="(n, l, m) => count += n" /> |
||||||
|
``` |
||||||
|
|
||||||
|
### 声明触发的事件 |
||||||
|
|
||||||
|
- 我们在 `<template>` 中使用的 `$emit` 方法不能在组件的 `<script setup>` 部分中使用,但 `defineEmits()` 会返回一个相同作用的函数供我们使用: |
||||||
|
- `defineEmits()` 宏不能在子函数中使用。如上所示,它必须直接放置在 `<script setup>` 的顶级作用域下。 |
||||||
|
|
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
// 这个必须写在最外曾 |
||||||
|
const emit = defineEmits(['inFocus', 'submit']) |
||||||
|
|
||||||
|
function buttonClick() { |
||||||
|
emit('submit') |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default { |
||||||
|
emits: ['inFocus', 'submit'], |
||||||
|
setup(props, ctx) { |
||||||
|
ctx.emit('submit') |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
``` |
||||||
|
|
||||||
|
### 事件校验 |
||||||
|
|
||||||
|
- 要为事件添加校验,那么事件可以被赋值为一个函数,接受的参数就是抛出事件时传入 emit 的内容,返回一个布尔值来表明事件是否合法。 |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
const emit = defineEmits({ |
||||||
|
// 没有校验 |
||||||
|
click: null, |
||||||
|
|
||||||
|
// 校验 submit 事件 |
||||||
|
submit: ({ email, password }) => { |
||||||
|
if (email && password) { |
||||||
|
return true |
||||||
|
} else { |
||||||
|
console.warn('Invalid submit event payload!') |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
function submitForm(email, password) { |
||||||
|
emit('submit', { email, password }) |
||||||
|
} |
||||||
|
</script> |
||||||
|
``` |
||||||
|
|
||||||
|
## 组件 v-model |
||||||
|
|
||||||
|
- `v-model` 可以在组件上使用以实现双向绑定。 |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
// 子组件 |
||||||
|
const model = defineModel() |
||||||
|
|
||||||
|
// 使 v-model 必填 |
||||||
|
const model = defineModel({ required: true }) |
||||||
|
|
||||||
|
// 提供一个默认值 |
||||||
|
const model = defineModel({ default: 0 }) |
||||||
|
function update() { |
||||||
|
// 以ref的方式调用 |
||||||
|
model.value++ |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<!-- 父组件Parent.vue --> |
||||||
|
<Child v-model="count" /> |
||||||
|
</template> |
||||||
|
``` |
||||||
|
|
||||||
|
### v-model 的参数 |
||||||
|
|
||||||
|
```vue |
||||||
|
<MyComponent v-model:title="bookTitle" /> |
||||||
|
``` |
||||||
|
|
||||||
|
```vue |
||||||
|
<!-- MyComponent.vue --> |
||||||
|
<script setup> |
||||||
|
const title = defineModel('title') |
||||||
|
const title = defineModel('title', { required: true }) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<input type="text" v-model="title" /> |
||||||
|
</template> |
||||||
|
``` |
||||||
|
|
||||||
|
### 多个 v-model 绑定 |
||||||
|
|
||||||
|
```vue |
||||||
|
<UserName |
||||||
|
v-model:first-name="first" |
||||||
|
v-model:last-name="last" |
||||||
|
/> |
||||||
|
``` |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
const firstName = defineModel('firstName') |
||||||
|
const lastName = defineModel('lastName') |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<input type="text" v-model="firstName" /> |
||||||
|
<input type="text" v-model="lastName" /> |
||||||
|
</template> |
||||||
|
``` |
||||||
|
|
||||||
|
# ? 传递对象是否还在使用ref响应式的参数 |
||||||
|
|
||||||
|
## 透传 Attributes |
||||||
|
|
||||||
|
### Attributes 继承 |
||||||
|
|
||||||
|
```vue |
||||||
|
<!--父组件--> |
||||||
|
<MyButton class="large" /> |
||||||
|
<!-- <MyButton> 子组件的模板 --> |
||||||
|
<button>click me</button> |
||||||
|
<!--最后渲染出的 DOM 结果--> |
||||||
|
<button class="large">click me</button> |
||||||
|
``` |
||||||
|
### `class` 和 `style`合并 |
||||||
|
|
||||||
|
- `template`下只有一个元素的话会继承,多个的话就不会,见**多根节点的 Attributes 继承** |
||||||
|
|
||||||
|
### `v-on` 监听器继承 |
||||||
|
|
||||||
|
### 深层组件继承——重写组件的话,会被子组件调用的子组件继承 |
||||||
|
|
||||||
|
### 禁用 Attributes 继承 |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
defineOptions({ |
||||||
|
inheritAttrs: false |
||||||
|
}) |
||||||
|
// ...setup 逻辑 |
||||||
|
</script> |
||||||
|
``` |
||||||
|
|
||||||
|
### 多根节点的 Attributes 继承 |
||||||
|
|
||||||
|
- 和单根节点组件有所不同,有着多个根节点的组件没有自动 attribute 透传行为。如果 $attrs 没有被显式绑定,将会抛出一个运行时警告。 |
||||||
|
- 如果 <CustomLayout> 有下面这样的多根节点模板,由于 Vue 不知道要将 attribute 透传到哪里,所以会抛出一个警告。 |
||||||
|
- 如果 $attrs 被显式绑定,则不会有警告: |
||||||
|
|
||||||
|
```vue |
||||||
|
<header>...</header> |
||||||
|
<main v-bind="$attrs">...</main> |
||||||
|
<footer>...</footer> |
||||||
|
``` |
||||||
|
|
||||||
|
### 在 JavaScript 中访问透传 Attributes |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
import { useAttrs } from 'vue' |
||||||
|
|
||||||
|
const attrs = useAttrs() |
||||||
|
|
||||||
|
// 如果没有使用 <script setup>,attrs 会作为 setup() 上下文对象的一个属性暴露: |
||||||
|
export default { |
||||||
|
setup(props, ctx) { |
||||||
|
// 透传 attribute 被暴露为 ctx.attrs |
||||||
|
console.log(ctx.attrs) |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
``` |
||||||
|
|
||||||
|
## 插槽 Slots |
||||||
|
|
||||||
|
### 默认内容 |
||||||
|
|
||||||
|
```vue |
||||||
|
<button type="submit"> |
||||||
|
<slot> |
||||||
|
Submit <!-- 默认内容 --> |
||||||
|
</slot> |
||||||
|
</button> |
||||||
|
``` |
||||||
|
|
||||||
|
### 具名插槽 |
||||||
|
|
||||||
|
- `v-slot` 有对应的简写 `#`,因此 `<template v-slot:header>` 可以简写为 `<template #header>`。其意思就是“将这部分模板片段传入子组件的 `header` 插槽中”。 |
||||||
|
|
||||||
|
```vue |
||||||
|
<div class="container"> |
||||||
|
<header> |
||||||
|
<slot name="header"></slot> |
||||||
|
</header> |
||||||
|
<main> |
||||||
|
<slot></slot> |
||||||
|
</main> |
||||||
|
<footer> |
||||||
|
<slot name="footer"></slot> |
||||||
|
</footer> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!--父组件--> |
||||||
|
<BaseLayout> |
||||||
|
<template v-slot:header> |
||||||
|
<!-- header 插槽的内容放这里 --> |
||||||
|
</template> |
||||||
|
</BaseLayout> |
||||||
|
|
||||||
|
<BaseLayout> |
||||||
|
<template #header> |
||||||
|
<h1>Here might be a page title</h1> |
||||||
|
</template> |
||||||
|
|
||||||
|
<template #default> |
||||||
|
<p>A paragraph for the main content.</p> |
||||||
|
<p>And another one.</p> |
||||||
|
</template> |
||||||
|
|
||||||
|
<template #footer> |
||||||
|
<p>Here's some contact info</p> |
||||||
|
</template> |
||||||
|
</BaseLayout> |
||||||
|
``` |
||||||
|
|
||||||
|
### 动态插槽名 |
||||||
|
|
||||||
|
```vue |
||||||
|
<base-layout> |
||||||
|
<template v-slot:[dynamicSlotName]> |
||||||
|
... |
||||||
|
</template> |
||||||
|
|
||||||
|
<!-- 缩写为 --> |
||||||
|
<template #[dynamicSlotName]> |
||||||
|
... |
||||||
|
</template> |
||||||
|
</base-layout> |
||||||
|
``` |
||||||
|
|
||||||
|
### 作用域插槽——使用自组建的传值 |
||||||
|
|
||||||
|
```vue |
||||||
|
<!-- <MyComponent> 的模板 --> |
||||||
|
<div> |
||||||
|
<slot :text="greetingMessage" :count="1"></slot> |
||||||
|
</div> |
||||||
|
|
||||||
|
<MyComponent v-slot="slotProps"> |
||||||
|
{{ slotProps.text }} {{ slotProps.count }} |
||||||
|
</MyComponent> |
||||||
|
<MyComponent v-slot="{ text, count }"> |
||||||
|
{{ text }} {{ count }} |
||||||
|
</MyComponent> |
||||||
|
``` |
||||||
|
|
||||||
|
### 具名作用域插槽 |
||||||
|
|
||||||
|
```vue |
||||||
|
<MyComponent> |
||||||
|
<template #header="headerProps"> |
||||||
|
{{ headerProps }} |
||||||
|
</template> |
||||||
|
|
||||||
|
<template #default="defaultProps"> |
||||||
|
{{ defaultProps }} |
||||||
|
</template> |
||||||
|
|
||||||
|
<template #footer="footerProps"> |
||||||
|
{{ footerProps }} |
||||||
|
</template> |
||||||
|
</MyComponent> |
||||||
|
|
||||||
|
<!--向具名插槽中传入 props:--> |
||||||
|
<slot name="header" message="hello"></slot> |
||||||
|
``` |
||||||
|
|
||||||
|
## 依赖注入 |
||||||
|
|
||||||
|
- 解决多级props传递 |
||||||
|
|
||||||
|
### Provide (提供) |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
import { provide } from 'vue' |
||||||
|
|
||||||
|
provide(/* 注入名 */ 'message', /* 值 */ 'hello!') |
||||||
|
</script> |
||||||
|
``` |
||||||
|
|
||||||
|
### 应用层 Provide |
||||||
|
|
||||||
|
```js |
||||||
|
import { createApp } from 'vue' |
||||||
|
|
||||||
|
const app = createApp({}) |
||||||
|
|
||||||
|
app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!') |
||||||
|
``` |
||||||
|
|
||||||
|
### Inject (注入) |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
import { inject } from 'vue' |
||||||
|
|
||||||
|
const message = inject('message') |
||||||
|
|
||||||
|
// 如果没有祖先组件提供 "message" |
||||||
|
// `value` 会是 "这是默认值" |
||||||
|
const value = inject('message', '这是默认值') |
||||||
|
|
||||||
|
// 在一些场景中,默认值可能需要通过调用一个函数或初始化一个类来取得。为了避免在用不到默认值的情况下进行不必要的计算或产生副作用,我们可以使用工厂函数来创建默认值: |
||||||
|
const value = inject('key', () => new ExpensiveClass(), true) |
||||||
|
// 第三个参数表示默认值应该被当作一个工厂函数 |
||||||
|
</script> |
||||||
|
``` |
||||||
|
|
||||||
|
### 使用 Symbol 作注入名 |
||||||
|
|
||||||
|
- 包含非常多的依赖提供,很麻烦 |
||||||
|
|
||||||
|
```js |
||||||
|
// keys.js |
||||||
|
export const myInjectionKey = Symbol() |
||||||
|
``` |
||||||
|
|
||||||
|
## 异步组件 ? 懒加载? |
||||||
|
|
||||||
|
### 基本用法 |
||||||
|
|
||||||
|
```js |
||||||
|
import { defineAsyncComponent } from 'vue' |
||||||
|
|
||||||
|
const AsyncComp = defineAsyncComponent(() => { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
// ...从服务器获取组件 |
||||||
|
resolve(/* 获取到的组件 */) |
||||||
|
}) |
||||||
|
}) |
||||||
|
// ... 像使用其他一般组件一样使用 `AsyncComp` |
||||||
|
``` |
||||||
|
|
||||||
|
### ES 模块动态导入也会返回一个 Promise |
||||||
|
|
||||||
|
```js |
||||||
|
import { defineAsyncComponent } from 'vue' |
||||||
|
|
||||||
|
const AsyncComp = defineAsyncComponent(() => |
||||||
|
import('./components/MyComponent.vue') |
||||||
|
) |
||||||
|
|
||||||
|
app.component('MyComponent', defineAsyncComponent(() => |
||||||
|
import('./components/MyComponent.vue') |
||||||
|
)) |
||||||
|
``` |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
import { defineAsyncComponent } from 'vue' |
||||||
|
|
||||||
|
const AdminPage = defineAsyncComponent(() => |
||||||
|
import('./components/AdminPageComponent.vue') |
||||||
|
) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<AdminPage /> |
||||||
|
</template> |
||||||
|
``` |
||||||
|
|
||||||
|
### 加载与错误状态 |
||||||
|
|
||||||
|
```js |
||||||
|
const AsyncComp = defineAsyncComponent({ |
||||||
|
// 加载函数 |
||||||
|
loader: () => import('./Foo.vue'), |
||||||
|
|
||||||
|
// 加载异步组件时使用的组件 |
||||||
|
loadingComponent: LoadingComponent, |
||||||
|
// 展示加载组件前的延迟时间,默认为 200ms |
||||||
|
delay: 200, |
||||||
|
|
||||||
|
// 加载失败后展示的组件 |
||||||
|
errorComponent: ErrorComponent, |
||||||
|
// 如果提供了一个 timeout 时间限制,并超时了 |
||||||
|
// 也会显示这里配置的报错组件,默认值是:Infinity |
||||||
|
timeout: 3000 |
||||||
|
}) |
||||||
|
``` |
||||||
|
|
@ -0,0 +1,244 @@ |
|||||||
|
# 逻辑复用 |
||||||
|
|
||||||
|
> 高深莫测 |
||||||
|
|
||||||
|
|
||||||
|
## 什么是“组合式函数”? |
||||||
|
|
||||||
|
- 利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数 |
||||||
|
|
||||||
|
```js |
||||||
|
// mouse.js |
||||||
|
import { ref, onMounted, onUnmounted } from 'vue' |
||||||
|
|
||||||
|
// 按照惯例,组合式函数名以“use”开头 |
||||||
|
export function useMouse() { |
||||||
|
// 被组合式函数封装和管理的状态 |
||||||
|
const x = ref(0) |
||||||
|
const y = ref(0) |
||||||
|
|
||||||
|
// 组合式函数可以随时更改其状态。 |
||||||
|
function update(event) { |
||||||
|
x.value = event.pageX |
||||||
|
y.value = event.pageY |
||||||
|
} |
||||||
|
|
||||||
|
// 一个组合式函数也可以挂靠在所属组件的生命周期上 |
||||||
|
// 来启动和卸载副作用 |
||||||
|
onMounted(() => window.addEventListener('mousemove', update)) |
||||||
|
onUnmounted(() => window.removeEventListener('mousemove', update)) |
||||||
|
|
||||||
|
// 通过返回值暴露所管理的状态 |
||||||
|
return { x, y } |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
import { useMouse } from './mouse.js' |
||||||
|
|
||||||
|
const { x, y } = useMouse() |
||||||
|
|
||||||
|
// x,y是可以监听变化的 |
||||||
|
</script> |
||||||
|
|
||||||
|
<template>Mouse position is at: {{ x }}, {{ y }}</template> |
||||||
|
``` |
||||||
|
|
||||||
|
### 异步状态示例 |
||||||
|
|
||||||
|
- 封装请求 |
||||||
|
|
||||||
|
```js |
||||||
|
// fetch.js |
||||||
|
import { ref } from 'vue' |
||||||
|
|
||||||
|
export function useFetch(url) { |
||||||
|
const data = ref(null) |
||||||
|
const error = ref(null) |
||||||
|
|
||||||
|
fetch(url) |
||||||
|
.then((res) => res.json()) |
||||||
|
.then((json) => (data.value = json)) |
||||||
|
.catch((err) => (error.value = err)) |
||||||
|
|
||||||
|
return { data, error } |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
import { useFetch } from './fetch.js' |
||||||
|
|
||||||
|
const { data, error } = useFetch('...') |
||||||
|
</script> |
||||||
|
``` |
||||||
|
|
||||||
|
### 接收响应式状态 |
||||||
|
|
||||||
|
```js |
||||||
|
// fetch.js |
||||||
|
import { ref, toValue, watchEffect } from 'vue' |
||||||
|
|
||||||
|
// url必须是响应式参数才能被watchEffect监听到 |
||||||
|
export function useFetch(url) { |
||||||
|
const data = ref(null) |
||||||
|
const error = ref(null) |
||||||
|
|
||||||
|
setTimeout(() => { |
||||||
|
data.value = "数据" + url.value |
||||||
|
}, 3000) |
||||||
|
watchEffect(() => { |
||||||
|
data.value = "数据SSS" + toValue(url) |
||||||
|
}) |
||||||
|
|
||||||
|
return { data, error } |
||||||
|
} |
||||||
|
|
||||||
|
``` |
||||||
|
|
||||||
|
|
||||||
|
### 约定和最佳实践 |
||||||
|
|
||||||
|
### 命名: 用驼峰命名法命名,以“use”作为开头。 |
||||||
|
|
||||||
|
### 输入参数 |
||||||
|
|
||||||
|
- 最好处理一下输入参数是 ref 或 getter 而非原始值的情况。可以利用 toValue() 工具函数来实现 |
||||||
|
|
||||||
|
```js |
||||||
|
import { toValue } from 'vue' |
||||||
|
|
||||||
|
function useFeature(maybeRefOrGetter) { |
||||||
|
// 如果 maybeRefOrGetter 是一个 ref 或 getter, |
||||||
|
// 将返回它的规范化值。 |
||||||
|
// 否则原样返回。 |
||||||
|
const value = toValue(maybeRefOrGetter) |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### 返回值 |
||||||
|
|
||||||
|
- 使用 `ref()` 而不是 `reactive()`, 使用一个不是响应式的对象包含响应式参数,可以被很好的解构赋值 |
||||||
|
|
||||||
|
## 自定义指令 |
||||||
|
|
||||||
|
- 由一个包含类似组件生命周期钩子的对象来定义 |
||||||
|
|
||||||
|
```vue |
||||||
|
<script setup> |
||||||
|
// 在模板中启用 v-focus |
||||||
|
const vFocus = { |
||||||
|
mounted: (el) => el.focus() |
||||||
|
} |
||||||
|
|
||||||
|
// 选项式 |
||||||
|
export default { |
||||||
|
setup() { |
||||||
|
/*...*/ |
||||||
|
}, |
||||||
|
directives: { |
||||||
|
// 在模板中启用 v-focus |
||||||
|
focus: { |
||||||
|
/* ... */ |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<!-- 使用后,会在挂在这个元素时聚焦--> |
||||||
|
<input v-focus /> |
||||||
|
</template> |
||||||
|
``` |
||||||
|
|
||||||
|
### 指令钩子 |
||||||
|
|
||||||
|
```js |
||||||
|
const myDirective = { |
||||||
|
// 在绑定元素的 attribute 前 |
||||||
|
// 或事件监听器应用前调用 |
||||||
|
created(el, binding, vnode, prevVnode) { |
||||||
|
// 下面会介绍各个参数的细节 |
||||||
|
}, |
||||||
|
// 在元素被插入到 DOM 前调用 |
||||||
|
beforeMount(el, binding, vnode, prevVnode) {}, |
||||||
|
// 在绑定元素的父组件 |
||||||
|
// 及他自己的所有子节点都挂载完成后调用 |
||||||
|
mounted(el, binding, vnode, prevVnode) {}, |
||||||
|
// 绑定元素的父组件更新前调用 |
||||||
|
beforeUpdate(el, binding, vnode, prevVnode) {}, |
||||||
|
// 在绑定元素的父组件 |
||||||
|
// 及他自己的所有子节点都更新后调用 |
||||||
|
updated(el, binding, vnode, prevVnode) {}, |
||||||
|
// 绑定元素的父组件卸载前调用 |
||||||
|
beforeUnmount(el, binding, vnode, prevVnode) {}, |
||||||
|
// 绑定元素的父组件卸载后调用 |
||||||
|
unmounted(el, binding, vnode, prevVnode) {} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### 全局 |
||||||
|
|
||||||
|
```js |
||||||
|
const app = createApp({}) |
||||||
|
|
||||||
|
// 使 v-focus 在所有组件中都可用 |
||||||
|
app.directive('focus', { |
||||||
|
/* ... */ |
||||||
|
}) |
||||||
|
``` |
||||||
|
|
||||||
|
### 钩子参数 |
||||||
|
|
||||||
|
- el:指令绑定到的元素。这可以用于直接操作 DOM。 |
||||||
|
|
||||||
|
- binding:一个对象,包含以下属性。 |
||||||
|
|
||||||
|
- value:传递给指令的值。例如在 v-my-directive="1 + 1" 中,值是 2。 |
||||||
|
- oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。 |
||||||
|
- arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"。 |
||||||
|
- modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }。 |
||||||
|
- instance:使用该指令的组件实例。 |
||||||
|
- dir:指令的定义对象。 |
||||||
|
- |
||||||
|
- vnode:代表绑定元素的底层 VNode。 |
||||||
|
|
||||||
|
- prevVnode:代表之前的渲染中指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。 |
||||||
|
|
||||||
|
### 在组件上使用 |
||||||
|
|
||||||
|
- 会透传,但是不建议在组件上使用 |
||||||
|
|
||||||
|
|
||||||
|
## 插件 |
||||||
|
|
||||||
|
### 使用 |
||||||
|
|
||||||
|
```js |
||||||
|
import { createApp } from 'vue' |
||||||
|
|
||||||
|
const app = createApp({}) |
||||||
|
|
||||||
|
app.use(myPlugin, { |
||||||
|
/* 可选的选项 */ |
||||||
|
}) |
||||||
|
``` |
||||||
|
|
||||||
|
### 插件常见场景 |
||||||
|
|
||||||
|
1. 通过 app.component() 和 app.directive() 注册一到多个全局组件或自定义指令。 |
||||||
|
2. 通过 app.provide() 使一个资源可被注入进整个应用。 |
||||||
|
3. 向 app.config.globalProperties 中添加一些全局实例属性或方法 |
||||||
|
4. 一个可能上述三种都包含了的功能库 (例如 vue-router)。 |
||||||
|
|
||||||
|
### 编写插件 |
||||||
|
|
||||||
|
```js |
||||||
|
// plugins/i18n.js |
||||||
|
export default { |
||||||
|
install: (app, options) => { |
||||||
|
// 在这里编写插件代码 |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
@ -0,0 +1,63 @@ |
|||||||
|
function isObject(obj) { |
||||||
|
return obj !== null && typeof obj === 'object'; |
||||||
|
} |
||||||
|
|
||||||
|
function reactive(target) { |
||||||
|
if (!isObject(target)) return target; |
||||||
|
const handler = { |
||||||
|
get(target, key, receiver) { |
||||||
|
track(target, key); |
||||||
|
const result = Reflect.get(target, key, receiver); |
||||||
|
return isObject(result) ? reactive(result) : result; |
||||||
|
}, |
||||||
|
set(target, key, value, receiver) { |
||||||
|
const oldValue = target[key]; |
||||||
|
if (oldValue !== value) { |
||||||
|
const result = Reflect.set(target, key, value, receiver); |
||||||
|
trigger(target, key); |
||||||
|
return result; |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
return new Proxy(target, handler); |
||||||
|
} |
||||||
|
|
||||||
|
const targetMap = new WeakMap(); |
||||||
|
const effectStack = []; |
||||||
|
|
||||||
|
function track(target, key) { |
||||||
|
if (effectStack.length) { |
||||||
|
let depsMap = targetMap.get(target); |
||||||
|
if (!depsMap) targetMap.set(target, (depsMap = new Map())); |
||||||
|
let dep = depsMap.get(key); |
||||||
|
if (!dep) depsMap.set(key, (dep = new Set())); |
||||||
|
dep.add(effectStack[effectStack.length - 1]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function trigger(target, key) { |
||||||
|
const depsMap = targetMap.get(target); |
||||||
|
if (depsMap) { |
||||||
|
const effects = new Set([...(depsMap.get(key) || [])]); |
||||||
|
effects.forEach(effect => effect()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function watchEffect(effect) { |
||||||
|
effectStack.push(effect); |
||||||
|
try { |
||||||
|
return effect(); |
||||||
|
} finally { |
||||||
|
effectStack.pop(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
const data = reactive({ count: 0 }); |
||||||
|
watchEffect(() => { |
||||||
|
console.log(`Count is: ${data.count}`); |
||||||
|
}); |
||||||
|
|
||||||
|
setTimeout(() => { |
||||||
|
data.count++; // 触发副作用,在控制台打印出新的count值
|
||||||
|
}, 1000); |
@ -0,0 +1,13 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="zh"> |
||||||
|
<head> |
||||||
|
<meta charset="UTF-8"> |
||||||
|
<link rel="icon" href="/favicon.ico"> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||||
|
<title>Vite App</title> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div id="app"></div> |
||||||
|
<script type="module" src="/src/main.js"></script> |
||||||
|
</body> |
||||||
|
</html> |
@ -0,0 +1,8 @@ |
|||||||
|
{ |
||||||
|
"compilerOptions": { |
||||||
|
"paths": { |
||||||
|
"@/*": ["./src/*"] |
||||||
|
} |
||||||
|
}, |
||||||
|
"exclude": ["node_modules", "dist"] |
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
{ |
||||||
|
"name": "vue3-test", |
||||||
|
"version": "0.0.0", |
||||||
|
"private": true, |
||||||
|
"type": "module", |
||||||
|
"scripts": { |
||||||
|
"dev": "vite", |
||||||
|
"build": "vite build", |
||||||
|
"preview": "vite preview", |
||||||
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore", |
||||||
|
"format": "prettier --write src/" |
||||||
|
}, |
||||||
|
"dependencies": { |
||||||
|
"@element-plus/icons-vue": "^2.3.1", |
||||||
|
"element-plus": "^2.7.1", |
||||||
|
"pinia": "^2.1.7", |
||||||
|
"vue": "^3.4.21", |
||||||
|
"vue-router": "^4.3.0" |
||||||
|
}, |
||||||
|
"devDependencies": { |
||||||
|
"@rushstack/eslint-patch": "^1.8.0", |
||||||
|
"@vitejs/plugin-vue": "^5.0.4", |
||||||
|
"@vitejs/plugin-vue-jsx": "^3.1.0", |
||||||
|
"@vue/eslint-config-prettier": "^9.0.0", |
||||||
|
"eslint": "^8.57.0", |
||||||
|
"eslint-plugin-vue": "^9.23.0", |
||||||
|
"prettier": "^3.2.5", |
||||||
|
"sass": "^1.75.0", |
||||||
|
"vite": "^5.2.8", |
||||||
|
"vite-plugin-vue-devtools": "^7.0.25" |
||||||
|
} |
||||||
|
} |
After Width: | Height: | Size: 4.2 KiB |
@ -0,0 +1,9 @@ |
|||||||
|
<script setup> |
||||||
|
import { RouterLink, RouterView } from 'vue-router' |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<RouterView /> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped></style> |
@ -0,0 +1,86 @@ |
|||||||
|
/* color palette from <https://github.com/vuejs/theme> */ |
||||||
|
:root { |
||||||
|
--vt-c-white: #ffffff; |
||||||
|
--vt-c-white-soft: #f8f8f8; |
||||||
|
--vt-c-white-mute: #f2f2f2; |
||||||
|
|
||||||
|
--vt-c-black: #181818; |
||||||
|
--vt-c-black-soft: #222222; |
||||||
|
--vt-c-black-mute: #282828; |
||||||
|
|
||||||
|
--vt-c-indigo: #2c3e50; |
||||||
|
|
||||||
|
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29); |
||||||
|
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12); |
||||||
|
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); |
||||||
|
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); |
||||||
|
|
||||||
|
--vt-c-text-light-1: var(--vt-c-indigo); |
||||||
|
--vt-c-text-light-2: rgba(60, 60, 60, 0.66); |
||||||
|
--vt-c-text-dark-1: var(--vt-c-white); |
||||||
|
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64); |
||||||
|
} |
||||||
|
|
||||||
|
/* semantic color variables for this project */ |
||||||
|
:root { |
||||||
|
--color-background: var(--vt-c-white); |
||||||
|
--color-background-soft: var(--vt-c-white-soft); |
||||||
|
--color-background-mute: var(--vt-c-white-mute); |
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-light-2); |
||||||
|
--color-border-hover: var(--vt-c-divider-light-1); |
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-light-1); |
||||||
|
--color-text: var(--vt-c-text-light-1); |
||||||
|
|
||||||
|
--section-gap: 160px; |
||||||
|
} |
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) { |
||||||
|
:root { |
||||||
|
--color-background: var(--vt-c-black); |
||||||
|
--color-background-soft: var(--vt-c-black-soft); |
||||||
|
--color-background-mute: var(--vt-c-black-mute); |
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-dark-2); |
||||||
|
--color-border-hover: var(--vt-c-divider-dark-1); |
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-dark-1); |
||||||
|
--color-text: var(--vt-c-text-dark-2); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
*, |
||||||
|
*::before, |
||||||
|
*::after { |
||||||
|
box-sizing: border-box; |
||||||
|
margin: 0; |
||||||
|
font-weight: normal; |
||||||
|
} |
||||||
|
|
||||||
|
body { |
||||||
|
min-height: 100vh; |
||||||
|
color: var(--color-text); |
||||||
|
background: var(--color-background); |
||||||
|
transition: |
||||||
|
color 0.5s, |
||||||
|
background-color 0.5s; |
||||||
|
line-height: 1.6; |
||||||
|
font-family: |
||||||
|
Inter, |
||||||
|
-apple-system, |
||||||
|
BlinkMacSystemFont, |
||||||
|
'Segoe UI', |
||||||
|
Roboto, |
||||||
|
Oxygen, |
||||||
|
Ubuntu, |
||||||
|
Cantarell, |
||||||
|
'Fira Sans', |
||||||
|
'Droid Sans', |
||||||
|
'Helvetica Neue', |
||||||
|
sans-serif; |
||||||
|
font-size: 14px; |
||||||
|
text-rendering: optimizeLegibility; |
||||||
|
-webkit-font-smoothing: antialiased; |
||||||
|
-moz-osx-font-smoothing: grayscale; |
||||||
|
} |
After Width: | Height: | Size: 276 B |
@ -0,0 +1,11 @@ |
|||||||
|
@import './base.css'; |
||||||
|
|
||||||
|
#app { |
||||||
|
position: relative; |
||||||
|
height: 100vh; |
||||||
|
width: 100vw; |
||||||
|
overflow: hidden; |
||||||
|
box-sizing: border-box; |
||||||
|
/*max-width: 1280px;*/ |
||||||
|
font-weight: normal; |
||||||
|
} |
@ -0,0 +1,31 @@ |
|||||||
|
<script setup> |
||||||
|
import { RouterView } from 'vue-router' |
||||||
|
import { useSystemStore } from '@/stores/systemStore.js' |
||||||
|
const systemStore = useSystemStore() |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<RouterView v-slot="{ Component }"> |
||||||
|
<transition name="mainPage"> |
||||||
|
<keep-alive :include="systemStore.menuTagList.map(i => i.name).join(',')"> |
||||||
|
<component :is="Component" /> |
||||||
|
</keep-alive> |
||||||
|
</transition> |
||||||
|
</RouterView> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped lang="scss"> |
||||||
|
.mainPage-fade-enter-active { |
||||||
|
transition: all 0.3s ease-out; |
||||||
|
} |
||||||
|
|
||||||
|
.mainPage-fade-leave-active { |
||||||
|
transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1); |
||||||
|
} |
||||||
|
|
||||||
|
.mainPage-fade-enter-from, |
||||||
|
.mainPage-fade-leave-to { |
||||||
|
transform: translateX(20px); |
||||||
|
opacity: 0; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,18 @@ |
|||||||
|
<script setup> |
||||||
|
import { ref } from 'vue' |
||||||
|
const input = ref('') |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="aaa"> |
||||||
|
<el-input v-model="input" style="width: 240px" placeholder="Please input" /> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
.aaa { |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
background: #181818; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,44 @@ |
|||||||
|
<script setup> |
||||||
|
defineProps({ |
||||||
|
msg: { |
||||||
|
type: String, |
||||||
|
required: true |
||||||
|
} |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="greetings"> |
||||||
|
<h1 class="green">{{ msg }}</h1> |
||||||
|
<h3> |
||||||
|
You’ve successfully created a project with |
||||||
|
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> + |
||||||
|
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. |
||||||
|
</h3> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
h1 { |
||||||
|
font-weight: 500; |
||||||
|
font-size: 2.6rem; |
||||||
|
position: relative; |
||||||
|
top: -10px; |
||||||
|
} |
||||||
|
|
||||||
|
h3 { |
||||||
|
font-size: 1.2rem; |
||||||
|
} |
||||||
|
|
||||||
|
.greetings h1, |
||||||
|
.greetings h3 { |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
@media (min-width: 1024px) { |
||||||
|
.greetings h1, |
||||||
|
.greetings h3 { |
||||||
|
text-align: left; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,120 @@ |
|||||||
|
<script setup> |
||||||
|
import { ArrowRight, Operation } from '@element-plus/icons-vue' |
||||||
|
import { useSystemStore } from '@/stores/systemStore.js' |
||||||
|
const systemStore = useSystemStore() |
||||||
|
import { useRoute, useRouter } from 'vue-router' |
||||||
|
const route = useRoute() |
||||||
|
const router = useRouter() |
||||||
|
// 关闭标签 |
||||||
|
const handleCloseMenuTag = (item, index) => { |
||||||
|
systemStore.menuTagList.splice(index, 1) |
||||||
|
systemStore.menuTagList.slice(-1)[0].activation = true |
||||||
|
router.push(systemStore.menuTagList.slice(-1)[0].path) |
||||||
|
} |
||||||
|
// 切换标签页 |
||||||
|
const handleChangeMenuTag = (item, index) => { |
||||||
|
systemStore.menuTagList.forEach((itemL,indexL) => { |
||||||
|
if(index == indexL){ |
||||||
|
itemL.activation = true |
||||||
|
router.push(systemStore.menuTagList[indexL].path) |
||||||
|
}else{ |
||||||
|
itemL.activation = false |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<header class="mainFrameworkHeader"> |
||||||
|
<div class="lineUp"> |
||||||
|
<div class="menuAndPageContainer"> |
||||||
|
<div class="menuBut"> |
||||||
|
<el-button |
||||||
|
type="primary" |
||||||
|
@click="systemStore.handleChangeMenuExpandStatus" |
||||||
|
:icon="Operation" |
||||||
|
circle |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<div class="pageList"> |
||||||
|
<el-breadcrumb :separator-icon="ArrowRight"> |
||||||
|
<TransitionGroup name="menuBreadcrumb"> |
||||||
|
<el-breadcrumb-item v-for="item in route.matched" :key="item.name" class="menuBreadcrumbItem">{{item.meta.title}}</el-breadcrumb-item> |
||||||
|
</TransitionGroup> |
||||||
|
</el-breadcrumb> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="userDataContainer"> |
||||||
|
<div>用户信息</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="lineDown"> |
||||||
|
<el-tag |
||||||
|
class="menuTag" |
||||||
|
v-for="(item, index) in systemStore.menuTagList" |
||||||
|
:key="item.name" |
||||||
|
type="primary" |
||||||
|
:effect="item.activation ? `dark` : `plain`" |
||||||
|
round |
||||||
|
:closable="!item.notClose" |
||||||
|
@close="handleCloseMenuTag(item, index)" |
||||||
|
@click="handleChangeMenuTag(item, index)" |
||||||
|
> |
||||||
|
{{ item.title }} |
||||||
|
</el-tag> |
||||||
|
</div> |
||||||
|
</header> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped lang="scss"> |
||||||
|
.mainFrameworkHeader { |
||||||
|
position: relative; |
||||||
|
& > div.lineUp { |
||||||
|
position: relative; |
||||||
|
display: flex; |
||||||
|
padding: 1rem; |
||||||
|
border-bottom: 1px solid #cdcdcd; |
||||||
|
& > div.menuAndPageContainer { |
||||||
|
position: relative; |
||||||
|
flex-shrink: 0; |
||||||
|
display: flex; |
||||||
|
width: 300px; |
||||||
|
& > div.menuBut { |
||||||
|
position: relative; |
||||||
|
} |
||||||
|
& > div.pageList { |
||||||
|
position: relative; |
||||||
|
padding-left: 20px; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
} |
||||||
|
} |
||||||
|
& > div.userDataContainer { |
||||||
|
position: relative; |
||||||
|
flex: 1; |
||||||
|
display: flex; |
||||||
|
flex-direction: row-reverse; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
// 菜单标签 |
||||||
|
.menuTag{ |
||||||
|
cursor: pointer; |
||||||
|
margin: 0 5px; |
||||||
|
} |
||||||
|
|
||||||
|
// 面包屑动画 |
||||||
|
.menuBreadcrumb-enter-active{ |
||||||
|
transition: all 0.3s ease; |
||||||
|
} |
||||||
|
.menuBreadcrumb-leave-active { |
||||||
|
position: absolute; |
||||||
|
transition: all 0.3s ease; |
||||||
|
white-space: nowrap; |
||||||
|
} |
||||||
|
.menuBreadcrumb-enter-from, |
||||||
|
.menuBreadcrumb-leave-to { |
||||||
|
opacity: 0; |
||||||
|
transform: translateX(20px); |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,30 @@ |
|||||||
|
<script setup> |
||||||
|
import MenuItem from '@/components/MainFramework/SideBar/MenuItem.vue' |
||||||
|
const { routeList, rootPath } = defineProps(['routeList', 'rootPath']) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<template v-for="(item, index) of routeList" :key="item.path"> |
||||||
|
<el-sub-menu |
||||||
|
v-if="item?.children && item.children.length > 0" |
||||||
|
:disabled="item?.meta?.disabled" |
||||||
|
:index="`${rootPath}/${item.name}`" |
||||||
|
> |
||||||
|
<template #title> |
||||||
|
<el-icon><HomeFilled /></el-icon> |
||||||
|
<span>{{ item.meta.title }}</span> |
||||||
|
</template> |
||||||
|
<MenuItem :routeList="item.children" :rootPath="`${rootPath}/${item.name}`" /> |
||||||
|
</el-sub-menu> |
||||||
|
<el-menu-item |
||||||
|
v-else-if="item.name" |
||||||
|
:disabled="item?.meta?.disabled" |
||||||
|
:index="`${rootPath}/${item.name}`" |
||||||
|
> |
||||||
|
<el-icon><Management /></el-icon> |
||||||
|
<span>{{ item.meta.title }}</span> |
||||||
|
</el-menu-item> |
||||||
|
</template> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped></style> |
@ -0,0 +1,79 @@ |
|||||||
|
<script setup> |
||||||
|
import MenuItem from '@/components/MainFramework/SideBar/MenuItem.vue' |
||||||
|
import { useSystemStore } from '@/stores/systemStore.js' |
||||||
|
|
||||||
|
const systemStore = useSystemStore() |
||||||
|
|
||||||
|
const handleOpen = (key, keyPath) => { |
||||||
|
console.log(key, keyPath) |
||||||
|
} |
||||||
|
const handleClose = (key, keyPath) => { |
||||||
|
console.log(key, keyPath) |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<!--default-active 页面加载时默认激活菜单的 index string--> |
||||||
|
<!--unique-opened 是否只保持一个子菜单的展开--> |
||||||
|
<!--:collapse-transition="false" 展开动画--> |
||||||
|
<template> |
||||||
|
<aside class="mainSideMenuContainer"> |
||||||
|
<header class="mainSideMenuContainerHeader"> |
||||||
|
<div>icon</div> |
||||||
|
<Transition name="mainSideMenuContainerHeader"> |
||||||
|
<div v-if="systemStore.menuExpandStatus">这里可以放系统名称</div> |
||||||
|
</Transition> |
||||||
|
</header> |
||||||
|
<nav> |
||||||
|
<el-menu |
||||||
|
class="mainmenu" |
||||||
|
:default-active="systemStore.nowRoutePath" |
||||||
|
:collapse="!systemStore.menuExpandStatus" |
||||||
|
@open="handleOpen" |
||||||
|
@close="handleClose" |
||||||
|
:router="true" |
||||||
|
> |
||||||
|
<MenuItem :routeList="systemStore.menuList" rootPath="" /> |
||||||
|
</el-menu> |
||||||
|
</nav> |
||||||
|
</aside> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped lang="scss"> |
||||||
|
.mainSideMenuContainer { |
||||||
|
position: relative; |
||||||
|
height: 100%; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
& > header.mainSideMenuContainerHeader { |
||||||
|
position: relative; |
||||||
|
flex-shrink: 0; |
||||||
|
height: var(--mainFramework-header-height); |
||||||
|
} |
||||||
|
& > nav { |
||||||
|
position: relative; |
||||||
|
flex: 1; |
||||||
|
overflow: hidden; |
||||||
|
& > ul { |
||||||
|
position: relative; |
||||||
|
height: 100%; |
||||||
|
overflow: auto; |
||||||
|
|
||||||
|
max-width: 300px; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.mainmenu:not(.el-menu--collapse) { |
||||||
|
width: 200px; |
||||||
|
} |
||||||
|
|
||||||
|
// 过渡动画,系统名 |
||||||
|
.mainSideMenuContainerHeader-enter-active, |
||||||
|
.mainSideMenuContainerHeader-leave-active { |
||||||
|
transition: opacity 0.5s ease; |
||||||
|
} |
||||||
|
|
||||||
|
.mainSideMenuContainerHeader-enter-from, |
||||||
|
.mainSideMenuContainerHeader-leave-to { |
||||||
|
opacity: 0; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,84 @@ |
|||||||
|
<script setup> |
||||||
|
import HeaderBar from '@/components/MainFramework/HeaderBar/index.vue' |
||||||
|
import SideBar from '@/components/MainFramework/SideBar/index.vue' |
||||||
|
import { RouterView } from 'vue-router' |
||||||
|
import CachingAndTransition from '@/components/CachingAndTransition/index.vue' |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="mainFramework"> |
||||||
|
<SideBar class="mainSide" /> |
||||||
|
<div class="mainBodyContainer"> |
||||||
|
<HeaderBar class="mainHeader" /> |
||||||
|
<div class="mainBody"> |
||||||
|
<CachingAndTransition/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped lang="scss"> |
||||||
|
.mainFramework { |
||||||
|
position: relative; |
||||||
|
height: 100%; |
||||||
|
display: flex; |
||||||
|
// 变量 header高度 |
||||||
|
--mainFramework-header-height: 100px; |
||||||
|
& > .mainSide { |
||||||
|
position: relative; |
||||||
|
flex-shrink: 0; |
||||||
|
height: 100%; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
& > .mainBodyContainer { |
||||||
|
position: relative; |
||||||
|
flex: 1; |
||||||
|
height: 100%; |
||||||
|
overflow: hidden; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
& .mainHeader { |
||||||
|
position: relative; |
||||||
|
flex-shrink: 0; |
||||||
|
height: var(--mainFramework-header-height); |
||||||
|
} |
||||||
|
& > .mainBody { |
||||||
|
position: relative; |
||||||
|
flex: 1; |
||||||
|
overflow: hidden; |
||||||
|
margin: 10px; |
||||||
|
border-radius: 5px; |
||||||
|
overflow: hidden; |
||||||
|
box-shadow: 2px 2px 10px 0px #aaaaaa66; |
||||||
|
background: #fff; |
||||||
|
} |
||||||
|
|
||||||
|
& > div.mainBody.mainNoData { |
||||||
|
position: relative; |
||||||
|
height: 100%; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.mainPage-enter-active { |
||||||
|
transition: all 0.3s ease-out; |
||||||
|
z-index: 3; |
||||||
|
} |
||||||
|
|
||||||
|
.mainPage-leave-active { |
||||||
|
position: absolute; |
||||||
|
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1); |
||||||
|
} |
||||||
|
|
||||||
|
.mainPage-enter-from, |
||||||
|
.mainPage-leave-to { |
||||||
|
transform: translateX(20px); |
||||||
|
opacity: 0; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,7 @@ |
|||||||
|
<script setup></script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div>NOTFOUND 404</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped></style> |
@ -0,0 +1,23 @@ |
|||||||
|
<script setup> |
||||||
|
import { ref, toValue, useAttrs, watchEffect } from 'vue' |
||||||
|
const attrs = useAttrs() |
||||||
|
|
||||||
|
const num = ref(0) |
||||||
|
|
||||||
|
function test() { |
||||||
|
console.log(toValue(num)) |
||||||
|
} |
||||||
|
setTimeout(() => { |
||||||
|
num.value++ |
||||||
|
}, 3000) |
||||||
|
watchEffect(() => { |
||||||
|
test() |
||||||
|
console.log('??') |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<button class="aaa">哈哈哈</button> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped></style> |
@ -0,0 +1,91 @@ |
|||||||
|
<script setup> |
||||||
|
import WelcomeItem from './WelcomeItem.vue' |
||||||
|
import DocumentationIcon from './icons/IconDocumentation.vue' |
||||||
|
import ToolingIcon from './icons/IconTooling.vue' |
||||||
|
import EcosystemIcon from './icons/IconEcosystem.vue' |
||||||
|
import CommunityIcon from './icons/IconCommunity.vue' |
||||||
|
import SupportIcon from './icons/IconSupport.vue' |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<WelcomeItem> |
||||||
|
<template #icon> |
||||||
|
<DocumentationIcon /> |
||||||
|
</template> |
||||||
|
<template #heading>Documentation</template> |
||||||
|
|
||||||
|
Vue’s |
||||||
|
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a> |
||||||
|
provides you with all information you need to get started. |
||||||
|
</WelcomeItem> |
||||||
|
|
||||||
|
<WelcomeItem> |
||||||
|
<template #icon> |
||||||
|
<ToolingIcon /> |
||||||
|
</template> |
||||||
|
<template #heading>Tooling</template> |
||||||
|
|
||||||
|
This project is served and bundled with |
||||||
|
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. |
||||||
|
The recommended IDE setup is |
||||||
|
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> + |
||||||
|
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. |
||||||
|
If you need to test your components and web pages, check out |
||||||
|
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and |
||||||
|
<a href="https://on.cypress.io/component" target="_blank" rel="noopener" |
||||||
|
>Cypress Component Testing</a |
||||||
|
>. |
||||||
|
|
||||||
|
<br /> |
||||||
|
|
||||||
|
More instructions are available in <code>README.md</code>. |
||||||
|
</WelcomeItem> |
||||||
|
|
||||||
|
<WelcomeItem> |
||||||
|
<template #icon> |
||||||
|
<EcosystemIcon /> |
||||||
|
</template> |
||||||
|
<template #heading>Ecosystem</template> |
||||||
|
|
||||||
|
Get official tools and libraries for your project: |
||||||
|
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>, |
||||||
|
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>, |
||||||
|
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, |
||||||
|
and |
||||||
|
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a |
||||||
|
>. If you need more resources, we suggest paying |
||||||
|
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener" |
||||||
|
>Awesome Vue</a |
||||||
|
> |
||||||
|
a visit. |
||||||
|
</WelcomeItem> |
||||||
|
|
||||||
|
<WelcomeItem> |
||||||
|
<template #icon> |
||||||
|
<CommunityIcon /> |
||||||
|
</template> |
||||||
|
<template #heading>Community</template> |
||||||
|
|
||||||
|
Got stuck? Ask your question on |
||||||
|
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official |
||||||
|
Discord server, or |
||||||
|
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener" |
||||||
|
>StackOverflow</a |
||||||
|
>. You should also subscribe to |
||||||
|
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and |
||||||
|
follow the official |
||||||
|
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a> |
||||||
|
twitter account for latest news in the Vue world. |
||||||
|
</WelcomeItem> |
||||||
|
|
||||||
|
<WelcomeItem> |
||||||
|
<template #icon> |
||||||
|
<SupportIcon /> |
||||||
|
</template> |
||||||
|
<template #heading>Support Vue</template> |
||||||
|
|
||||||
|
As an independent project, Vue relies on community backing for its sustainability. You can |
||||||
|
help us by |
||||||
|
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>. |
||||||
|
</WelcomeItem> |
||||||
|
</template> |
@ -0,0 +1,86 @@ |
|||||||
|
<template> |
||||||
|
<div class="item"> |
||||||
|
<i> |
||||||
|
<slot name="icon"></slot> |
||||||
|
</i> |
||||||
|
<div class="details"> |
||||||
|
<h3> |
||||||
|
<slot name="heading"></slot> |
||||||
|
</h3> |
||||||
|
<slot></slot> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
.item { |
||||||
|
margin-top: 2rem; |
||||||
|
display: flex; |
||||||
|
position: relative; |
||||||
|
} |
||||||
|
|
||||||
|
.details { |
||||||
|
flex: 1; |
||||||
|
margin-left: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
i { |
||||||
|
display: flex; |
||||||
|
place-items: center; |
||||||
|
place-content: center; |
||||||
|
width: 32px; |
||||||
|
height: 32px; |
||||||
|
color: var(--color-text); |
||||||
|
} |
||||||
|
|
||||||
|
h3 { |
||||||
|
font-size: 1.2rem; |
||||||
|
font-weight: 500; |
||||||
|
margin-bottom: 0.4rem; |
||||||
|
color: var(--color-heading); |
||||||
|
} |
||||||
|
|
||||||
|
@media (min-width: 1024px) { |
||||||
|
.item { |
||||||
|
margin-top: 0; |
||||||
|
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2); |
||||||
|
} |
||||||
|
|
||||||
|
i { |
||||||
|
top: calc(50% - 25px); |
||||||
|
left: -26px; |
||||||
|
position: absolute; |
||||||
|
border: 1px solid var(--color-border); |
||||||
|
background: var(--color-background); |
||||||
|
border-radius: 8px; |
||||||
|
width: 50px; |
||||||
|
height: 50px; |
||||||
|
} |
||||||
|
|
||||||
|
.item:before { |
||||||
|
content: ' '; |
||||||
|
border-left: 1px solid var(--color-border); |
||||||
|
position: absolute; |
||||||
|
left: 0; |
||||||
|
bottom: calc(50% + 25px); |
||||||
|
height: calc(50% - 25px); |
||||||
|
} |
||||||
|
|
||||||
|
.item:after { |
||||||
|
content: ' '; |
||||||
|
border-left: 1px solid var(--color-border); |
||||||
|
position: absolute; |
||||||
|
left: 0; |
||||||
|
top: calc(50% + 25px); |
||||||
|
height: calc(50% - 25px); |
||||||
|
} |
||||||
|
|
||||||
|
.item:first-of-type:before { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
|
||||||
|
.item:last-of-type:after { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,7 @@ |
|||||||
|
<template> |
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor"> |
||||||
|
<path |
||||||
|
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z" |
||||||
|
/> |
||||||
|
</svg> |
||||||
|
</template> |
@ -0,0 +1,7 @@ |
|||||||
|
<template> |
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor"> |
||||||
|
<path |
||||||
|
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z" |
||||||
|
/> |
||||||
|
</svg> |
||||||
|
</template> |
@ -0,0 +1,7 @@ |
|||||||
|
<template> |
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor"> |
||||||
|
<path |
||||||
|
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z" |
||||||
|
/> |
||||||
|
</svg> |
||||||
|
</template> |
@ -0,0 +1,7 @@ |
|||||||
|
<template> |
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor"> |
||||||
|
<path |
||||||
|
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z" |
||||||
|
/> |
||||||
|
</svg> |
||||||
|
</template> |
@ -0,0 +1,19 @@ |
|||||||
|
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license--> |
||||||
|
<template> |
||||||
|
<svg |
||||||
|
xmlns="http://www.w3.org/2000/svg" |
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink" |
||||||
|
aria-hidden="true" |
||||||
|
role="img" |
||||||
|
class="iconify iconify--mdi" |
||||||
|
width="24" |
||||||
|
height="24" |
||||||
|
preserveAspectRatio="xMidYMid meet" |
||||||
|
viewBox="0 0 24 24" |
||||||
|
> |
||||||
|
<path |
||||||
|
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z" |
||||||
|
fill="currentColor" |
||||||
|
></path> |
||||||
|
</svg> |
||||||
|
</template> |
@ -0,0 +1,16 @@ |
|||||||
|
// fetch.js
|
||||||
|
import { ref, toValue, watchEffect } from 'vue' |
||||||
|
|
||||||
|
export function useFetch(url) { |
||||||
|
const data = ref(null) |
||||||
|
const error = ref(null) |
||||||
|
|
||||||
|
setTimeout(() => { |
||||||
|
data.value = '数据' + url.value |
||||||
|
}, 3000) |
||||||
|
watchEffect(() => { |
||||||
|
data.value = '数据SSS' + toValue(url) |
||||||
|
}) |
||||||
|
|
||||||
|
return { data, error } |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
// mouse.js
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue' |
||||||
|
|
||||||
|
// 按照惯例,组合式函数名以“use”开头
|
||||||
|
export function useMouse() { |
||||||
|
// 被组合式函数封装和管理的状态
|
||||||
|
const x = ref(0) |
||||||
|
const y = ref(0) |
||||||
|
|
||||||
|
// 组合式函数可以随时更改其状态。
|
||||||
|
function update(event) { |
||||||
|
x.value = event.pageX |
||||||
|
y.value = event.pageY |
||||||
|
} |
||||||
|
|
||||||
|
// 一个组合式函数也可以挂靠在所属组件的生命周期上
|
||||||
|
// 来启动和卸载副作用
|
||||||
|
onMounted(() => window.addEventListener('mousemove', update)) |
||||||
|
onUnmounted(() => window.removeEventListener('mousemove', update)) |
||||||
|
|
||||||
|
// 通过返回值暴露所管理的状态
|
||||||
|
return { x, y } |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
// 导入基础样式
|
||||||
|
import './assets/main.css' |
||||||
|
|
||||||
|
// 导入Vue初始化程序
|
||||||
|
import { createApp } from 'vue' |
||||||
|
// 导入Pinia状态管理
|
||||||
|
import { createPinia } from 'pinia' |
||||||
|
|
||||||
|
// 引入根组件
|
||||||
|
import App from './App.vue' |
||||||
|
// 引入路由表
|
||||||
|
import router from './router' |
||||||
|
|
||||||
|
// 引入Element Plus
|
||||||
|
import ElementPlus from 'element-plus' |
||||||
|
import 'element-plus/dist/index.css' |
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue' |
||||||
|
|
||||||
|
// 路由守卫
|
||||||
|
import authRouter from '@/router/auth/index.js' |
||||||
|
|
||||||
|
// 创建根组件渲染器
|
||||||
|
const app = createApp(App) |
||||||
|
|
||||||
|
// 使用状态管理器
|
||||||
|
app.use(createPinia()) |
||||||
|
// 使用路由
|
||||||
|
app.use(router) |
||||||
|
// 使用ElementPlus
|
||||||
|
app.use(ElementPlus) |
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) { |
||||||
|
app.component(key, component) |
||||||
|
} |
||||||
|
|
||||||
|
// 挂载渲染节点
|
||||||
|
app.mount('#app') |
||||||
|
|
||||||
|
app.use(authRouter, { |
||||||
|
router |
||||||
|
}) |
||||||
|
|
||||||
|
app.component('CacheRouterView', () => import('@/components/CachingAndTransition/index.vue')) |
||||||
|
// 路由守卫
|
@ -0,0 +1,42 @@ |
|||||||
|
import { useSystemStore } from '@/stores/systemStore.js' |
||||||
|
export default function authRouter(app, option){ |
||||||
|
const {router} = option; |
||||||
|
const systemStore = useSystemStore() |
||||||
|
|
||||||
|
router.beforeEach(async (to, from) => { |
||||||
|
|
||||||
|
}) |
||||||
|
|
||||||
|
|
||||||
|
router.afterEach(async (to, from) => { |
||||||
|
|
||||||
|
await menuTag(to, systemStore) |
||||||
|
systemStore.nowRoutePath = to.path |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// 标签页
|
||||||
|
async function menuTag(to, systemStore){ |
||||||
|
console.log(to) |
||||||
|
const {name, meta, path } = to |
||||||
|
if(name){ |
||||||
|
const menuTagList = systemStore.menuTagList; |
||||||
|
let existTag = false |
||||||
|
menuTagList.forEach(tag => { |
||||||
|
if(tag.name == name){ |
||||||
|
existTag = true; |
||||||
|
tag.activation = true |
||||||
|
}else{ |
||||||
|
tag.activation = false |
||||||
|
} |
||||||
|
}) |
||||||
|
if(!existTag){ |
||||||
|
menuTagList.push({ |
||||||
|
name, |
||||||
|
title: meta.title, |
||||||
|
path, |
||||||
|
activation: true, |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,10 @@ |
|||||||
|
import { createRouter, createWebHistory } from 'vue-router' |
||||||
|
import routeList from '@/router/routeList/index.js' |
||||||
|
|
||||||
|
const router = createRouter({ |
||||||
|
history: createWebHistory(import.meta.env.VITE_BASE_URL), |
||||||
|
routes: routeList |
||||||
|
}) |
||||||
|
|
||||||
|
|
||||||
|
export default router |
@ -0,0 +1,29 @@ |
|||||||
|
import mainChildrem from './main/index.js' |
||||||
|
export default [ |
||||||
|
{ |
||||||
|
path: '', |
||||||
|
redirect: '/home' |
||||||
|
}, |
||||||
|
{ |
||||||
|
path: '/home', |
||||||
|
name: 'home', |
||||||
|
component: () => import('@/components/MainFramework/index.vue'), |
||||||
|
meta: { |
||||||
|
title: '首页' |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
path: '/main', |
||||||
|
name: 'main', |
||||||
|
component: () => import('@/components/MainFramework/index.vue'), |
||||||
|
children: mainChildrem, |
||||||
|
meta: { |
||||||
|
title: '设置' |
||||||
|
} |
||||||
|
}, |
||||||
|
// 其他路由...
|
||||||
|
{ |
||||||
|
path: '/:pathMatch(.*)*', |
||||||
|
component: () => import('@/components/NotFound/index.vue') |
||||||
|
} |
||||||
|
] |
@ -0,0 +1,28 @@ |
|||||||
|
export default [ |
||||||
|
{ |
||||||
|
path: 'about', |
||||||
|
name: 'about', |
||||||
|
component: async () => { |
||||||
|
const a = await import('@/components/ColorComponent/ColorComponent.vue'); |
||||||
|
a.default.__name = 'about' |
||||||
|
return a |
||||||
|
}, |
||||||
|
children: [], |
||||||
|
meta: { |
||||||
|
title: '关于' |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
path: 'myhome', |
||||||
|
name: 'myhome', |
||||||
|
component: async () => { |
||||||
|
const a = await import('@/views/HomeView.vue') |
||||||
|
a.default.__name = 'myhome' |
||||||
|
return a |
||||||
|
}, |
||||||
|
children: [], |
||||||
|
meta: { |
||||||
|
title: '我家' |
||||||
|
} |
||||||
|
} |
||||||
|
] |
@ -0,0 +1,12 @@ |
|||||||
|
import { ref, computed } from 'vue' |
||||||
|
import { defineStore } from 'pinia' |
||||||
|
|
||||||
|
export const useCounterStore = defineStore('counter', () => { |
||||||
|
const count = ref(0) |
||||||
|
const doubleCount = computed(() => count.value * 2) |
||||||
|
function increment() { |
||||||
|
count.value++ |
||||||
|
} |
||||||
|
|
||||||
|
return { count, doubleCount, increment } |
||||||
|
}) |
@ -0,0 +1,28 @@ |
|||||||
|
import { ref, computed } from 'vue' |
||||||
|
import { defineStore } from 'pinia' |
||||||
|
import routeList from '@/router/routeList/index.js' |
||||||
|
|
||||||
|
export const useSystemStore = defineStore('system', () => { |
||||||
|
// 菜单开合状态
|
||||||
|
const menuExpandStatus = ref(true); |
||||||
|
// 菜单列表
|
||||||
|
const menuList = ref(routeList); |
||||||
|
// 已打开的菜单
|
||||||
|
const menuTagList = ref([{ |
||||||
|
name: 'home', |
||||||
|
notClose: true, |
||||||
|
title: '首页', |
||||||
|
path: '/home' |
||||||
|
}]) |
||||||
|
// 当前路由菜单
|
||||||
|
const nowRoutePath = ref('home'); |
||||||
|
// 可缓存列表
|
||||||
|
const keepAliveList = [] |
||||||
|
|
||||||
|
// 开合菜单
|
||||||
|
function handleChangeMenuExpandStatus() { |
||||||
|
menuExpandStatus.value = !menuExpandStatus.value |
||||||
|
} |
||||||
|
|
||||||
|
return { menuExpandStatus, handleChangeMenuExpandStatus, menuList, menuTagList, nowRoutePath, keepAliveList } |
||||||
|
}) |
@ -0,0 +1,15 @@ |
|||||||
|
<template> |
||||||
|
<div class="about"> |
||||||
|
<h1>This is an about page</h1> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style> |
||||||
|
@media (min-width: 1024px) { |
||||||
|
.about { |
||||||
|
min-height: 100vh; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,9 @@ |
|||||||
|
<script setup> |
||||||
|
import TheWelcome from '../components/TheWelcome.vue' |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<main> |
||||||
|
<TheWelcome /> |
||||||
|
</main> |
||||||
|
</template> |
@ -0,0 +1,20 @@ |
|||||||
|
import { fileURLToPath, URL } from 'node:url' |
||||||
|
|
||||||
|
import { defineConfig } from 'vite' |
||||||
|
import vue from '@vitejs/plugin-vue' |
||||||
|
import vueJsx from '@vitejs/plugin-vue-jsx' |
||||||
|
import VueDevTools from 'vite-plugin-vue-devtools' |
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({ |
||||||
|
plugins: [ |
||||||
|
vue(), |
||||||
|
vueJsx(), |
||||||
|
VueDevTools(), |
||||||
|
], |
||||||
|
resolve: { |
||||||
|
alias: { |
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url)) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |