@ -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)) |
||||
} |
||||
} |
||||
}) |