feat: update vue example using modern-vue-template
This commit is contained in:
parent
6c1164c5e0
commit
5187b31a8b
|
@ -412,6 +412,7 @@ In the Vue runtime environment, the default is the installed code that has been
|
|||
|
||||
No back-end API is configured in the `Vue` case. For details, please refer to `React` and `api` to set reverse proxy
|
||||
|
||||
> Vue example powered by [**modern-vue-template**](https://github.com/byoungd/modern-vue-template)
|
||||
## Contribution
|
||||
|
||||
Thanks [pleasedmi](https://github.com/pleasedmi)、[Elena211314](https://github.com/Elena211314)、[zb201307](https://github.com/zb201307) for donation
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
extends: ['cz'],
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
module.exports = {
|
||||
types: [
|
||||
{ value: 'feat', name: 'feat: 增加新功能' },
|
||||
{ value: 'fix', name: 'fix: 修复bug' },
|
||||
{ value: 'ui', name: 'ui: 更新UI' },
|
||||
{ value: 'refactor', name: 'refactor: 代码重构' },
|
||||
{ value: 'release', name: 'release: 发布' },
|
||||
{ value: 'deploy', name: 'deploy: 部署' },
|
||||
{ value: 'docs', name: 'docs: 修改文档' },
|
||||
{ value: 'test', name: 'test: 增删测试' },
|
||||
{ value: 'chore', name: 'chore: 更改配置文件' },
|
||||
{ value: 'style', name: 'style: 样式修改不影响逻辑' },
|
||||
{ value: 'revert', name: 'revert: 版本回退' },
|
||||
{ value: 'add', name: 'add: 添加依赖' },
|
||||
{ value: 'minus', name: 'minus: 版本回退' },
|
||||
{ value: 'del', name: 'del: 删除代码/文件' },
|
||||
{ value: 'init', name: 'init: 初始提交' },
|
||||
],
|
||||
scopes: [],
|
||||
messages: {
|
||||
type: '选择更改类型:\n',
|
||||
scope: '更改的范围:\n',
|
||||
// 如果allowcustomscopes为true,则使用
|
||||
// customScope: 'Denote the SCOPE of this change:',
|
||||
subject: '简短描述:\n',
|
||||
body: '详细描述. 使用"|"换行:\n',
|
||||
breaking: 'Breaking Changes列表:\n',
|
||||
footer: '关闭的issues列表. E.g.: #31, #34:\n',
|
||||
confirmCommit: '确认提交?',
|
||||
},
|
||||
allowCustomScopes: true,
|
||||
allowBreakingChanges: ['feat', 'fix'],
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
# editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
|
@ -0,0 +1,16 @@
|
|||
# server env
|
||||
VITE_SERVER_ENV = 'development'
|
||||
|
||||
# open dev login
|
||||
VITE_OPEN_DEV_LOGIN = 'YES'
|
||||
|
||||
# drop console
|
||||
VITE_BUILD_DROP_CONSOLE = 'NO'
|
||||
|
||||
# base api
|
||||
VITE_BASE_API = 'https://api.xxx.com'
|
||||
|
||||
# Versions
|
||||
VITE_VER_MAIN = '1'
|
||||
VITE_VER_SUB = '0.0.1'
|
||||
VITE_VER_STATE = 'dev'
|
|
@ -0,0 +1,16 @@
|
|||
# server env
|
||||
VITE_SERVER_ENV = 'production'
|
||||
|
||||
# open dev login
|
||||
VITE_OPEN_DEV_LOGIN = 'NO'
|
||||
|
||||
# drop console
|
||||
VITE_BUILD_DROP_CONSOLE = 'YES'
|
||||
|
||||
# base api
|
||||
VITE_BASE_API = 'https://api.xxx.com'
|
||||
|
||||
# Versions
|
||||
VITE_VER_MAIN = '1'
|
||||
VITE_VER_SUB = '0.0.1'
|
||||
VITE_VER_STATE = 'prod'
|
|
@ -0,0 +1,16 @@
|
|||
# server env
|
||||
VITE_SERVER_ENV = 'production'
|
||||
|
||||
# open dev login
|
||||
VITE_OPEN_DEV_LOGIN = 'YES'
|
||||
|
||||
# drop console
|
||||
VITE_BUILD_DROP_CONSOLE = 'NO'
|
||||
|
||||
# base api
|
||||
VITE_BASE_API = 'https://api.xxx.com'
|
||||
|
||||
# Versions
|
||||
VITE_VER_MAIN = '1'
|
||||
VITE_VER_SUB = '0.0.1'
|
||||
VITE_VER_STATE = 'prod'
|
|
@ -0,0 +1,17 @@
|
|||
node_modules
|
||||
public
|
||||
apps/modern-vue/dist
|
||||
apps/modern-vue/build
|
||||
.build/*
|
||||
.nuxt
|
||||
.history
|
||||
apps/modern-vue/static/*
|
||||
apps/modern-vue/static/js
|
||||
apps/modern-vue/static/sw.js
|
||||
apps/modern-vue/static/pdf
|
||||
apps/modern-vue/static/simple-project
|
||||
jsconfig.json
|
||||
package.json
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
types/generated/*
|
|
@ -0,0 +1,31 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
browser: true,
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:vue/vue3-essential',
|
||||
'plugin:vue/vue3-strongly-recommended',
|
||||
'@vue/typescript/recommended',
|
||||
'@vue/eslint-config-prettier',
|
||||
'@vue/eslint-config-typescript',
|
||||
'prettier',
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
},
|
||||
plugins: ['vue', 'prettier'],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
'prettier/prettier': [
|
||||
'error',
|
||||
{},
|
||||
{
|
||||
usePrettierrc: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
},
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
# Don't allow people to merge changes to these generated files, because the result
|
||||
# may be invalid. You need to run "rush update" again.
|
||||
pnpm-lock.yaml merge=text
|
||||
shrinkwrap.yaml merge=binary
|
||||
npm-shrinkwrap.json merge=binary
|
||||
yarn.lock merge=binary
|
||||
|
||||
# Rush's JSON config files use JavaScript-style code comments. The rule below prevents pedantic
|
||||
# syntax highlighters such as GitHub's from highlighting these comments as errors. Your text editor
|
||||
# may also require a special configuration to allow comments in JSON.
|
||||
#
|
||||
# For more information, see this issue: https://github.com/microsoft/rushstack/issues/1088
|
||||
#
|
||||
*.json linguist-language=JSON-with-Comments
|
|
@ -1,23 +1,111 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
build
|
||||
.vite-ssg-temp
|
||||
|
||||
# gen
|
||||
src/auto-imports.d.ts
|
||||
src/components.d.ts
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"printWidth": 90,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"proseWrap": "always",
|
||||
"endOfLine": "auto",
|
||||
"bracketSpacing": true
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 han
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,194 @@
|
|||
## Modern Vue
|
||||
|
||||
[Modern Vue](https://github.com/byoungd/modern-vue-template) stack 2022 with **Micro front
|
||||
end** & **Monorepo** 🎉.
|
||||
|
||||
Joyful development experience 😄.
|
||||
|
||||
The `main` branch will keep clean for quickly creating Vue3 web app.
|
||||
|
||||
Monorepo architecture please visit branch
|
||||
[monorepo](https://github.com/byoungd/modern-vue-template/tree/monorepo).
|
||||
|
||||
<p align='center'>
|
||||
<b>English</b> | <a href="https://github.com/byoungd/modern-vue-template/blob/main/README.zh-CN.md">简体中文</a>
|
||||
</p>
|
||||
|
||||
## Features
|
||||
|
||||
- ⚡️ [Vue 3](https://github.com/vuejs/vue-next),
|
||||
[Vite 2](https://github.com/vitejs/vite), [pnpm](https://pnpm.js.org/),
|
||||
[ESBuild](https://github.com/evanw/esbuild) - born with fastness
|
||||
|
||||
- ⚡️ Build Optimization with compress
|
||||
|
||||
- ⚡️ CDN by Uploading static files to OSS
|
||||
|
||||
- 🦾 Environmental distinction
|
||||
|
||||
- 🦾 **Monorepo** by Rush
|
||||
|
||||
- 🎨 Commitlint
|
||||
|
||||
- 🎨 Formatting with **prettier** and **pretty-quick**
|
||||
|
||||
- 🗂 File based routing
|
||||
|
||||
- 📦 Components auto importing
|
||||
|
||||
- 🍍 [State Management via Pinia](https://pinia.esm.dev/)
|
||||
|
||||
- 📑 Layout system
|
||||
|
||||
- [Extend Script Setup Component Name to co-operate with Vue Devtools](https://github.com/vbenjs/vite-plugin-vue-setup-extend)
|
||||
|
||||
- 📲 [PWA](https://github.com/antfu/vite-plugin-pwa)
|
||||
|
||||
- 🎨 [Windi CSS](https://github.com/windicss/windicss) - next generation utility-first CSS
|
||||
framework
|
||||
|
||||
- 😃
|
||||
[Use icons from any icon sets, with no compromise](https://github.com/antfu/unplugin-icons)
|
||||
|
||||
- 🌍 I18n ready
|
||||
|
||||
- 🗒 [Markdown Support](https://github.com/antfu/vite-plugin-md)
|
||||
|
||||
- 🔥 Use the [new `<script setup>` syntax](https://github.com/vuejs/rfcs/pull/227)
|
||||
|
||||
- 📥 [APIs auto importing](https://github.com/antfu/unplugin-auto-import) - use
|
||||
Composition API and others directly
|
||||
|
||||
- 🖨 Server-side generation (SSG) via [vite-ssg](https://github.com/antfu/vite-ssg)
|
||||
|
||||
- 🦔 Critical CSS via [critters](https://github.com/GoogleChromeLabs/critters)
|
||||
|
||||
- 🦾 TypeScript, of course
|
||||
|
||||
- ⚙️ Unit Testing with [Vitest](https://github.com/vitest-dev/vitest), E2E Testing with
|
||||
[Cypress](https://cypress.io/) on [GitHub Actions](https://github.com/features/actions)
|
||||
|
||||
- ☁️ Deploy on Netlify, zero-config
|
||||
|
||||
## Pre-packed
|
||||
|
||||
### UI Frameworks
|
||||
|
||||
- [Windi CSS](https://github.com/windicss/windicss) (On-demand
|
||||
[TailwindCSS](https://tailwindcss.com/)) - lighter and faster, with a bunch of
|
||||
additional features!
|
||||
- [Windi CSS Typography](https://windicss.org/plugins/official/typography.html)
|
||||
|
||||
### Icons
|
||||
|
||||
- [Iconify](https://iconify.design) - use icons from any icon sets
|
||||
[🔍Icônes](https://icones.netlify.app/)
|
||||
- [`unplugin-icons`](https://github.com/antfu/unplugin-icons) - icons as Vue components
|
||||
|
||||
### Plugins
|
||||
|
||||
- [Vue Router](https://github.com/vuejs/vue-router)
|
||||
- [`vite-plugin-pages`](https://github.com/hannoeru/vite-plugin-pages) - file system
|
||||
based routing
|
||||
- [`vite-plugin-vue-layouts`](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) -
|
||||
layouts for pages
|
||||
- [Pinia](https://pinia.esm.dev) - Intuitive, type safe, light and flexible Store for Vue
|
||||
using the composition api
|
||||
- [`unplugin-vue-components`](https://github.com/antfu/unplugin-vue-components) -
|
||||
components auto import
|
||||
- [`unplugin-auto-import`](https://github.com/antfu/unplugin-auto-import) - Directly use
|
||||
Vue Composition API and others without importing
|
||||
- [`vite-plugin-pwa`](https://github.com/antfu/vite-plugin-pwa) - PWA
|
||||
- [`vite-plugin-windicss`](https://github.com/antfu/vite-plugin-windicss) - Windi CSS
|
||||
Integration
|
||||
- [`vite-plugin-md`](https://github.com/antfu/vite-plugin-md) - Markdown as components /
|
||||
components in Markdown
|
||||
- [`markdown-it-prism`](https://github.com/jGleitz/markdown-it-prism) -
|
||||
[Prism](https://prismjs.com/) for syntax highlighting
|
||||
- [`prism-theme-vars`](https://github.com/antfu/prism-theme-vars) - customizable
|
||||
Prism.js theme using CSS variables
|
||||
- [Vue I18n](https://github.com/intlify/vue-i18n-next) - Internationalization
|
||||
- [`vite-plugin-vue-i18n`](https://github.com/intlify/vite-plugin-vue-i18n) - Vite
|
||||
plugin for Vue I18n
|
||||
- [VueUse](https://github.com/antfu/vueuse) - collection of useful composition APIs
|
||||
- [`@vueuse/head`](https://github.com/vueuse/head) - manipulate document head reactively
|
||||
|
||||
### Coding Style
|
||||
|
||||
- Use Composition API with
|
||||
[`<script setup>` SFC syntax](https://github.com/vuejs/rfcs/pull/227)
|
||||
- [ESLint](https://eslint.org/) with
|
||||
[@antfu/eslint-config](https://github.com/antfu/eslint-config), single quotes, no semi.
|
||||
|
||||
### Dev tools
|
||||
|
||||
- [TypeScript](https://www.typescriptlang.org/)
|
||||
- [Vitest](https://github.com/vitest-dev/vitest) - Unit testing powered by Vite
|
||||
- [Cypress](https://cypress.io/) - E2E testing
|
||||
- [pnpm](https://pnpm.js.org/) - fast, disk space efficient package manager
|
||||
- [`vite-ssg`](https://github.com/antfu/vite-ssg) - Server-side generation
|
||||
- [critters](https://github.com/GoogleChromeLabs/critters) - Critical CSS
|
||||
- [Netlify](https://www.netlify.com/) - zero-config deployment
|
||||
- [VS Code Extensions](./.vscode/extensions.json)
|
||||
- [Vite](https://marketplace.visualstudio.com/items?itemName=antfu.vite) - Fire up Vite
|
||||
server automatically
|
||||
- [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) - Vue
|
||||
3 `<script setup>` IDE support
|
||||
- [Iconify IntelliSense](https://marketplace.visualstudio.com/items?itemName=antfu.iconify) -
|
||||
Icon inline display and autocomplete
|
||||
- [i18n Ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) -
|
||||
All in one i18n support
|
||||
- [Windi CSS Intellisense](https://marketplace.visualstudio.com/items?itemName=voorjaar.windicss-intellisense) -
|
||||
IDE support for Windi CSS
|
||||
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
|
||||
|
||||
## Try it now!
|
||||
|
||||
> Requires Node >=14
|
||||
|
||||
### GitHub Template
|
||||
|
||||
[Create a repo from this template on GitHub](https://github.com/byoungd/modern-vue-template/generate).
|
||||
|
||||
### Clone to local
|
||||
|
||||
If you prefer to do it manually with the cleaner git history
|
||||
|
||||
## Usage
|
||||
|
||||
### Development
|
||||
|
||||
```
|
||||
pnpm i
|
||||
|
||||
pnpm dev
|
||||
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
To build the App, run
|
||||
|
||||
```bash
|
||||
|
||||
pnpm build
|
||||
|
||||
```
|
||||
|
||||
With Env:
|
||||
|
||||
```bash
|
||||
|
||||
pnpm build:test
|
||||
|
||||
```
|
||||
|
||||
And you will see the generated file in `dist` that ready to be served.
|
||||
|
||||
## Thanks
|
||||
|
||||
- [Vitesse](https://github.com/antfu/vitesse)
|
||||
|
||||
## Final
|
||||
|
||||
enjoy :)
|
|
@ -0,0 +1,178 @@
|
|||
## Modern Vue
|
||||
|
||||
[现代化的 Vue 技术栈](https://github.com/byoungd/modern-vue-template) 支持 **Micro front
|
||||
end** 和 **Monorepo** 🎉。
|
||||
|
||||
即刻享受令人愉悦的开发体验 😄。
|
||||
|
||||
`main` 分支将保持极简以快速创建 vue3 应用。
|
||||
|
||||
`monorepo`架构的项目请查
|
||||
看[monorepo 分支](https://github.com/byoungd/modern-vue-template/tree/monorepo)
|
||||
|
||||
## 特点
|
||||
|
||||
- ⚡️ [Vue 3](https://github.com/vuejs/vue-next),
|
||||
[Vite 2](https://github.com/vitejs/vite), [pnpm](https://pnpm.js.org/),
|
||||
[ESBuild](https://github.com/evanw/esbuild) - 生而为快
|
||||
|
||||
- ⚡️ 打包时进行压缩优化
|
||||
|
||||
- ⚡️ 上传静态资源至 OSS 以支持 CDN
|
||||
|
||||
- 🦾 通过 .env 文件进行环境隔离 轻松使用多套环境以应对开发和生产环境
|
||||
|
||||
- 🦾 使用 `Rush`搭建 `Monorepo`
|
||||
|
||||
- 🦾 使用 `micro-app` 作为微前端方案 配合 monorepo 轻松接入多个不同技术栈的项目
|
||||
|
||||
- 🎨 支持 `Commitlint` 以规范代码提交
|
||||
|
||||
- 🎨 使用 prettier 和 pretty-quick 进行代码自动格式化
|
||||
|
||||
- 🗂 基于文件结构的路由系统
|
||||
|
||||
- 📦 自动引入组件
|
||||
|
||||
- 🍍 [使用 Pinia 进行状态管理](https://pinia.esm.dev/)
|
||||
|
||||
- 📑 [布局系统](./src/layouts)
|
||||
|
||||
- [扩展 Script Setup 提供组件名称 以更好的配合 Vue Devtools](https://github.com/vbenjs/vite-plugin-vue-setup-extend)
|
||||
|
||||
- 📲 [PWA](https://github.com/antfu/vite-plugin-pwa)
|
||||
|
||||
- 🎨 [Windi CSS](https://github.com/windicss/windicss) - next generation utility-first CSS
|
||||
framework
|
||||
|
||||
- 😃 [无妥协使用任意 icons](https://github.com/antfu/unplugin-icons)
|
||||
|
||||
- 🌍 [I18n](./locales)
|
||||
|
||||
- 🗒 [支持 Markdown](https://github.com/antfu/vite-plugin-md)
|
||||
|
||||
- 🔥 使用 [ `<script setup>` 写法进行高效开发](https://github.com/vuejs/rfcs/pull/227)
|
||||
|
||||
- 📥 [APIs 自动引入](https://github.com/antfu/unplugin-auto-import) - use Composition API
|
||||
and others directly
|
||||
|
||||
- 🖨 服务端页面生成 (SSG) 通过 [vite-ssg](https://github.com/antfu/vite-ssg)
|
||||
|
||||
- 🦔 规范的 CSS [critters](https://github.com/GoogleChromeLabs/critters)
|
||||
|
||||
- 🦾 全面支持 TypeScript
|
||||
|
||||
- ⚙️ 使用 [Vitest] 单元测试 (https://github.com/vitest-dev/vitest), E2E Testing with
|
||||
[Cypress](https://cypress.io/) on [GitHub Actions](https://github.com/features/actions)
|
||||
|
||||
- ☁️ 零配置部署至 Netlify
|
||||
|
||||
<br>
|
||||
|
||||
## 使用到的库
|
||||
|
||||
### UI Frameworks
|
||||
|
||||
- [Windi CSS](https://github.com/windicss/windicss) (On-demand
|
||||
[TailwindCSS](https://tailwindcss.com/)) - lighter and faster, with a bunch of
|
||||
additional features!
|
||||
- [Windi CSS Typography](https://windicss.org/plugins/official/typography.html)
|
||||
|
||||
### 图标
|
||||
|
||||
- [Iconify](https://iconify.design) - use icons from any icon sets
|
||||
[🔍Icônes](https://icones.netlify.app/)
|
||||
- [`unplugin-icons`](https://github.com/antfu/unplugin-icons) - icons as Vue components
|
||||
|
||||
### 插件
|
||||
|
||||
- [Vue Router](https://github.com/vuejs/vue-router)
|
||||
- [`vite-plugin-pages`](https://github.com/hannoeru/vite-plugin-pages) - file system
|
||||
based routing
|
||||
- [`vite-plugin-vue-layouts`](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) -
|
||||
layouts for pages
|
||||
- [Pinia](https://pinia.esm.dev) - Intuitive, type safe, light and flexible Store for Vue
|
||||
using the composition api
|
||||
- [`unplugin-vue-components`](https://github.com/antfu/unplugin-vue-components) -
|
||||
components auto import
|
||||
- [`unplugin-auto-import`](https://github.com/antfu/unplugin-auto-import) - Directly use
|
||||
Vue Composition API and others without importing
|
||||
- [`vite-plugin-pwa`](https://github.com/antfu/vite-plugin-pwa) - PWA
|
||||
- [`vite-plugin-windicss`](https://github.com/antfu/vite-plugin-windicss) - Windi CSS
|
||||
Integration
|
||||
- [`vite-plugin-md`](https://github.com/antfu/vite-plugin-md) - Markdown as components /
|
||||
components in Markdown
|
||||
- [`markdown-it-prism`](https://github.com/jGleitz/markdown-it-prism) -
|
||||
[Prism](https://prismjs.com/) for syntax highlighting
|
||||
- [`prism-theme-vars`](https://github.com/antfu/prism-theme-vars) - customizable
|
||||
Prism.js theme using CSS variables
|
||||
- [Vue I18n](https://github.com/intlify/vue-i18n-next) - Internationalization
|
||||
- [`vite-plugin-vue-i18n`](https://github.com/intlify/vite-plugin-vue-i18n) - Vite
|
||||
plugin for Vue I18n
|
||||
- [VueUse](https://github.com/antfu/vueuse) - collection of useful composition APIs
|
||||
- [`@vueuse/head`](https://github.com/vueuse/head) - manipulate document head reactively
|
||||
|
||||
### 编码风格
|
||||
|
||||
- Use Composition API with
|
||||
[`<script setup>` SFC syntax](https://github.com/vuejs/rfcs/pull/227)
|
||||
|
||||
### Dev tools
|
||||
|
||||
- [TypeScript](https://www.typescriptlang.org/)
|
||||
- [Vitest](https://github.com/vitest-dev/vitest) - Unit testing powered by Vite
|
||||
- [Cypress](https://cypress.io/) - E2E testing
|
||||
- [pnpm](https://pnpm.js.org/) - fast, disk space efficient package manager
|
||||
- [`vite-ssg`](https://github.com/antfu/vite-ssg) - Server-side generation
|
||||
- [critters](https://github.com/GoogleChromeLabs/critters) - Critical CSS
|
||||
- [Netlify](https://www.netlify.com/) - zero-config deployment
|
||||
- [VS Code Extensions](./.vscode/extensions.json)
|
||||
- [Vite](https://marketplace.visualstudio.com/items?itemName=antfu.vite) - Fire up Vite
|
||||
server automatically
|
||||
- [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) - Vue
|
||||
3 `<script setup>` IDE support
|
||||
- [Iconify IntelliSense](https://marketplace.visualstudio.com/items?itemName=antfu.iconify) -
|
||||
Icon inline display and autocomplete
|
||||
- [i18n Ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) -
|
||||
All in one i18n support
|
||||
- [Windi CSS Intellisense](https://marketplace.visualstudio.com/items?itemName=voorjaar.windicss-intellisense) -
|
||||
IDE support for Windi CSS
|
||||
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
|
||||
|
||||
## 特别鸣谢
|
||||
|
||||
- [Vitesse](https://github.com/antfu/vitesse)
|
||||
|
||||
## 运行环境
|
||||
|
||||
> Node >=14
|
||||
|
||||
### GitHub Template
|
||||
|
||||
[Create a repo from this template on GitHub](https://github.com/byoungd/modern-vue-template/generate).
|
||||
|
||||
## 如何使用
|
||||
|
||||
### 本地开发
|
||||
|
||||
```
|
||||
pnpm i
|
||||
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### 打包
|
||||
|
||||
打包项目命令为:
|
||||
|
||||
```bash
|
||||
|
||||
pnpm build
|
||||
|
||||
```
|
||||
|
||||
使用 Env 环境:
|
||||
|
||||
```bash
|
||||
pnpm build:test
|
||||
```
|
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"users": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "韩寒"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "王奕博"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "马包裹"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "吴一帆"
|
||||
}
|
||||
],
|
||||
"projects": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "律所管理",
|
||||
"personId": 1,
|
||||
"organization": "南京法奔律师事务所",
|
||||
"created": 1604989757139
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "APP研发",
|
||||
"personId": 2,
|
||||
"organization": "研发组",
|
||||
"created": 1604989757139
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "产品管理",
|
||||
"personId": 2,
|
||||
"organization": "产品部门",
|
||||
"created": 1546300800000
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "总部管理",
|
||||
"personId": 3,
|
||||
"organization": "总部",
|
||||
"created": 1604980000011
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "销售管理",
|
||||
"personId": 4,
|
||||
"organization": "销售组",
|
||||
"created": 1546900800000
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
module.exports = {
|
||||
presets: ['@vue/cli-plugin-babel/preset'],
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* Used to package and output gzip. Note that this does not work properly in Vite, the specific reason is still being investigated
|
||||
* https://github.com/anncwb/vite-plugin-compression
|
||||
*/
|
||||
import type { Plugin } from 'vite'
|
||||
import compressPlugin from 'vite-plugin-compression'
|
||||
|
||||
export function configCompressPlugin(
|
||||
compress: 'gzip' | 'brotli' | 'none',
|
||||
deleteOriginFile = false
|
||||
): Plugin | Plugin[] {
|
||||
const compressList = compress.split(',')
|
||||
|
||||
const plugins: Plugin[] = []
|
||||
|
||||
if (compressList.includes('gzip')) {
|
||||
plugins.push(
|
||||
compressPlugin({
|
||||
ext: '.gz',
|
||||
deleteOriginFile,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (compressList.includes('brotli')) {
|
||||
plugins.push(
|
||||
compressPlugin({
|
||||
ext: '.br',
|
||||
algorithm: 'brotliCompress',
|
||||
deleteOriginFile,
|
||||
})
|
||||
)
|
||||
}
|
||||
return plugins
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import * as fs from 'fs'
|
||||
|
||||
// calc cdn path suffix
|
||||
export const calcCdnPathSuffix = (mode = 'development') => {
|
||||
const v = fs.readFileSync('./config/version.txt', 'utf-8')
|
||||
|
||||
const prefix = `desktop/${mode}`
|
||||
|
||||
let suffix = `${prefix}/latest`
|
||||
|
||||
if (v) {
|
||||
suffix = `${prefix}/${v}`
|
||||
}
|
||||
return suffix
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// build dir
|
||||
export const buildDir = 'build'
|
||||
|
||||
// public file
|
||||
export const publicDir = 'public'
|
||||
|
||||
// pages dir
|
||||
export const pagesDir = 'src/pages'
|
||||
|
||||
// sub apps fir
|
||||
export const subAppsDir = 'src/children'
|
||||
|
||||
// layout dir
|
||||
export const layoutsDir = 'src/layouts'
|
||||
|
||||
// cdn switcher
|
||||
export const useCdn = false
|
||||
|
||||
// OSS BASE
|
||||
export const ossBase = 'https://cdn.xxx.com'
|
||||
|
||||
// OSS Bucket
|
||||
export const ossBucket = 'static'
|
||||
|
||||
// chunk path
|
||||
export const chunkPath = 'cdn/chunk'
|
|
@ -0,0 +1,42 @@
|
|||
import * as dotenv from 'dotenv'
|
||||
|
||||
export interface ViteEnv {
|
||||
VITE_URL: string
|
||||
VITE_BASE_URL: string
|
||||
VITE_ZIP_NAME: string
|
||||
VITE_BASE_API: string
|
||||
VITE_BUILD_DROP_CONSOLE: string
|
||||
VITE_CDN_BASE: string
|
||||
}
|
||||
|
||||
// 通过dotenv配置 需要加载指定.env文件 这样process.env打印到得就是对应得文件了
|
||||
// 这里的mode是我们启动时候的参数 npm run dev:prc 得到的mode就是prc
|
||||
export function loadEnv(mode: string): ViteEnv {
|
||||
const ret: any = {}
|
||||
// 在使用之前我们先指定加载哪个环境变量
|
||||
dotenv.config({
|
||||
path: `.env.${mode}`, // .env.prc
|
||||
})
|
||||
|
||||
for (const envName of Object.keys(process.env)) {
|
||||
const realName = (process.env as any)[envName].replace(/\\n/g, '\n')
|
||||
ret[envName] = realName
|
||||
// inject VITE_XXX into process.env
|
||||
process.env[envName] = realName
|
||||
}
|
||||
return ret
|
||||
}
|
||||
const regExps = (value: string, reg: string): string => {
|
||||
return value.replace(new RegExp(reg, 'g'), '')
|
||||
}
|
||||
|
||||
// proxy
|
||||
export function createProxy(targetProxyUrl: string, baseUrl: string) {
|
||||
return {
|
||||
[`${baseUrl}`]: {
|
||||
target: targetProxyUrl,
|
||||
changeOrigin: true,
|
||||
rewrite: (path: string) => regExps(path, `${baseUrl}`),
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
0.0.1
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"baseUrl": "http://localhost:3333",
|
||||
"chromeWebSecurity": false
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
context('Basic', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/')
|
||||
})
|
||||
|
||||
it('basic nav', () => {
|
||||
cy.url().should('eq', 'http://localhost:3333/')
|
||||
|
||||
cy.contains('[Home Layout]').should('exist')
|
||||
|
||||
cy.get('#input')
|
||||
.type('Vitesse{Enter}')
|
||||
.url()
|
||||
.should('eq', 'http://localhost:3333/hi/Vitesse')
|
||||
|
||||
cy.contains('[Default Layout]').should('exist')
|
||||
|
||||
cy.get('.btn').click().url().should('eq', 'http://localhost:3333/')
|
||||
})
|
||||
|
||||
it('markdown', () => {
|
||||
cy.get('[title="About"]').click().url().should('eq', 'http://localhost:3333/about')
|
||||
|
||||
cy.get('pre.language-js').should('exist')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,25 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="apple-touch-icon" href="/pwa-192x192.png" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#00aba9" />
|
||||
<meta name="msapplication-TileColor" content="#00aba9" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<script>
|
||||
;(function () {
|
||||
const prefersDark =
|
||||
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
const setting = localStorage.getItem('vueuse-color-scheme') || 'auto'
|
||||
if (setting === 'dark' || (prefersDark && setting !== 'light'))
|
||||
document.documentElement.classList.toggle('dark', true)
|
||||
})()
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,9 @@
|
|||
## i18n
|
||||
|
||||
This directory is to serve your locale translation files. YAML under this folder would be
|
||||
loaded automatically and register with their filenames as locale code.
|
||||
|
||||
Check out [`vue-i18n`](https://github.com/intlify/vue-i18n-next) for more details.
|
||||
|
||||
If you are using VS Code, [`i18n Ally`](https://github.com/lokalise/i18n-ally) is
|
||||
recommended to make the i18n experience better.
|
|
@ -0,0 +1,14 @@
|
|||
button:
|
||||
about: Über
|
||||
back: Zurück
|
||||
go: Los
|
||||
home: Startseite
|
||||
toggle_dark: Dunkelmodus umschalten
|
||||
toggle_langs: Sprachen ändern
|
||||
intro:
|
||||
desc: Vite Startvorlage mit Vorlieben
|
||||
dynamic-route: Demo einer dynamischen Route
|
||||
hi: Hi, {name}!
|
||||
aka: Auch bekannt als
|
||||
whats-your-name: Wie heißt du?
|
||||
not-found: Nicht gefunden
|
|
@ -0,0 +1,14 @@
|
|||
button:
|
||||
about: About
|
||||
back: Back
|
||||
go: GO
|
||||
home: Home
|
||||
toggle_dark: Toggle dark mode
|
||||
toggle_langs: Change languages
|
||||
intro:
|
||||
desc: Opinionated Vite Starter Template
|
||||
dynamic-route: Demo of dynamic route
|
||||
hi: Hi, {name}!
|
||||
aka: Also known as
|
||||
whats-your-name: What's your name?
|
||||
not-found: Not found
|
|
@ -0,0 +1,14 @@
|
|||
button:
|
||||
about: Acerca de
|
||||
back: Atrás
|
||||
go: Ir
|
||||
home: Inicio
|
||||
toggle_dark: Alternar modo oscuro
|
||||
toggle_langs: Cambiar idiomas
|
||||
intro:
|
||||
desc: Plantilla de Inicio de Vite Dogmática
|
||||
dynamic-route: Demo de ruta dinámica
|
||||
hi: ¡Hola, {name}!
|
||||
aka: También conocido como
|
||||
whats-your-name: ¿Cómo te llamas?
|
||||
not-found: No se ha encontrado
|
|
@ -0,0 +1,14 @@
|
|||
button:
|
||||
about: À propos de
|
||||
back: Retour
|
||||
go: Essayer
|
||||
home: Accueil
|
||||
toggle_dark: Basculer en mode sombre
|
||||
toggle_langs: Changer de langue
|
||||
intro:
|
||||
desc: Exemple d'application Vite
|
||||
dynamic-route: Démo de route dynamique
|
||||
hi: Salut, {name}!
|
||||
aka: Aussi connu sous le nom de
|
||||
whats-your-name: Comment t'appelles-tu ?
|
||||
not-found: Page non trouvée
|
|
@ -0,0 +1,14 @@
|
|||
button:
|
||||
about: Tentang
|
||||
back: Kembali
|
||||
go: Pergi
|
||||
home: Beranda
|
||||
toggle_dark: Ubah ke mode gelap
|
||||
toggle_langs: Ubah bahasa
|
||||
intro:
|
||||
desc: Template awal vite
|
||||
dynamic-route: Contoh rute dinamik
|
||||
hi: Halo, {name}!
|
||||
aka: Juga diketahui sebagai
|
||||
whats-your-name: Siapa nama anda?
|
||||
not-found: Tidak ditemukan
|
|
@ -0,0 +1,13 @@
|
|||
button:
|
||||
about: Su di me
|
||||
back: Indietro
|
||||
go: Vai
|
||||
home: Home
|
||||
toggle_dark: Attiva/disattiva modalità scura
|
||||
toggle_langs: Cambia lingua
|
||||
intro:
|
||||
desc: Modello per una Applicazione Vite
|
||||
dynamic-route: Demo di rotta dinamica
|
||||
hi: Ciao, {name}!
|
||||
whats-your-name: Come ti chiami?
|
||||
not-found: Non trovato
|
|
@ -0,0 +1,13 @@
|
|||
button:
|
||||
about: これは?
|
||||
back: 戻る
|
||||
go: 進む
|
||||
home: ホーム
|
||||
toggle_dark: ダークモード切り替え
|
||||
toggle_langs: 言語切り替え
|
||||
intro:
|
||||
desc: 固執された Vite スターターテンプレート
|
||||
dynamic-route: 動的ルートのデモ
|
||||
hi: こんにちは、{name}!
|
||||
whats-your-name: 君の名は。
|
||||
not-found: 見つかりませんでした
|
|
@ -0,0 +1,13 @@
|
|||
button:
|
||||
about: 소개
|
||||
back: 뒤로가기
|
||||
go: 이동
|
||||
home: 홈
|
||||
toggle_dark: 다크모드 토글
|
||||
toggle_langs: 언어 변경
|
||||
intro:
|
||||
desc: Vite 애플리케이션 템플릿
|
||||
dynamic-route: 다이나믹 라우트 데모
|
||||
hi: 안녕, {name}!
|
||||
whats-your-name: 이름이 뭐예요?
|
||||
not-found: 찾을 수 없습니다
|
|
@ -0,0 +1,14 @@
|
|||
button:
|
||||
about: O nas
|
||||
back: Wróć
|
||||
go: WEJDŹ
|
||||
home: Strona główna
|
||||
toggle_dark: Ustaw tryb nocny
|
||||
toggle_langs: Zmień język
|
||||
intro:
|
||||
desc: Opiniowany szablon startowy Vite
|
||||
dynamic-route: Demonstracja dynamicznego route
|
||||
hi: Cześć, {name}!
|
||||
aka: Znany też jako
|
||||
whats-your-name: Jak masz na imię?
|
||||
not-found: Nie znaleziono
|
|
@ -0,0 +1,14 @@
|
|||
button:
|
||||
about: Sobre
|
||||
back: Voltar
|
||||
go: Ir
|
||||
home: Início
|
||||
toggle_dark: Alternar modo escuro
|
||||
toggle_langs: Mudar de idioma
|
||||
intro:
|
||||
desc: Modelo Opinativo de Partida de Vite
|
||||
dynamic-route: Demonstração de rota dinâmica
|
||||
hi: Olá, {name}!
|
||||
aka: Também conhecido como
|
||||
whats-your-name: Qual é o seu nome?
|
||||
not-found: Não encontrado
|
|
@ -0,0 +1,13 @@
|
|||
button:
|
||||
about: О шаблоне
|
||||
back: Назад
|
||||
go: Перейти
|
||||
home: Главная
|
||||
toggle_dark: Включить темный режим
|
||||
toggle_langs: Сменить язык
|
||||
intro:
|
||||
desc: Самостоятельный начальный шаблон Vite
|
||||
dynamic-route: Демо динамического маршрута
|
||||
hi: Привет, {name}!
|
||||
whats-your-name: Как тебя зовут?
|
||||
not-found: Не найден
|
|
@ -0,0 +1,14 @@
|
|||
button:
|
||||
about: Hakkımda
|
||||
back: Geri
|
||||
go: İLERİ
|
||||
home: Anasayfa
|
||||
toggle_dark: Karanlık modu değiştir
|
||||
toggle_langs: Dilleri değiştir
|
||||
intro:
|
||||
desc: Görüşlü Vite Başlangıç Şablonu
|
||||
dynamic-route: Dinamik rota demosu
|
||||
hi: Merhaba, {name}!
|
||||
aka: Ayrıca şöyle bilinir
|
||||
whats-your-name: Adınız nedir?
|
||||
not-found: Bulunamadı
|
|
@ -0,0 +1,13 @@
|
|||
button:
|
||||
about: Về
|
||||
back: Quay lại
|
||||
go: Đi
|
||||
home: Khởi đầu
|
||||
toggle_dark: Chuyển đổi chế độ tối
|
||||
toggle_langs: Thay đổi ngôn ngữ
|
||||
intro:
|
||||
desc: Ý kiến cá nhân Vite Template để bắt đầu
|
||||
dynamic-route: Bản giới thiệu về dynamic route
|
||||
hi: Hi, {name}!
|
||||
whats-your-name: Tên bạn là gì?
|
||||
not-found: Không tìm thấy
|
|
@ -0,0 +1,14 @@
|
|||
button:
|
||||
about: 关于
|
||||
back: 返回
|
||||
go: 确定
|
||||
home: 首页
|
||||
toggle_dark: 切换深色模式
|
||||
toggle_langs: 切换语言
|
||||
intro:
|
||||
desc: 固执己见的 Vite 项目模板
|
||||
dynamic-route: 动态路由演示
|
||||
hi: 你好,{name}
|
||||
aka: 也叫
|
||||
whats-your-name: 输入你的名字
|
||||
not-found: 未找到页面
|
|
@ -0,0 +1,18 @@
|
|||
[build.environment]
|
||||
# bypass npm auto install
|
||||
NPM_FLAGS = "--version"
|
||||
NODE_VERSION = "16"
|
||||
|
||||
[build]
|
||||
publish = "dist"
|
||||
command = "npx pnpm i --store=node_modules/.pnpm-store && npx pnpm run build"
|
||||
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
||||
|
||||
[[headers]]
|
||||
for = "/manifest.webmanifest"
|
||||
[headers.values]
|
||||
Content-Type = "application/manifest+json"
|
|
@ -0,0 +1,3 @@
|
|||
export const excludeDeps = ['vue-demi']
|
||||
|
||||
export const includeDeps = ['vue', 'vue-router', '@vueuse/core', '@vueuse/head']
|
|
@ -1,13 +1,42 @@
|
|||
{
|
||||
"name": "demo-vue",
|
||||
"version": "0.1.0",
|
||||
"name": "modern-vue",
|
||||
"version": "0.0.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
"build:ssg": "vite-ssg build",
|
||||
"build": "cross-env NODE_OPTIONS=--max-old-space-size=6000 vite build --mode development",
|
||||
"build:dev": "cross-env NODE_OPTIONS=--max-old-space-size=6000 vite build --mode development",
|
||||
"build:test": "cross-env NODE_OPTIONS=--max-old-space-size=6000 SERVER_ENV=prod NODE_ENV=production vite build --mode test && cross-env MODE=test node ./scripts/upToCdnProd.js",
|
||||
"build:prod": "cross-env NODE_OPTIONS=--max-old-space-size=6000 SERVER_ENV=prod NODE_ENV=production vite build --mode production && cross-env MODE=production node ./scripts/upToCdnProd.js",
|
||||
"dev": "vite --port 3333 --open --mode development",
|
||||
"lint": "eslint \"**/*.{vue,ts,js}\"",
|
||||
"preview": "vite preview",
|
||||
"preview-https": "serve dist",
|
||||
"test": "vitest",
|
||||
"test:e2e": "cypress open",
|
||||
"test:unit": "vitest",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"format": "prettier --write",
|
||||
"format:staged": "pretty-quick --staged --write",
|
||||
"mock:demo": "json-server api/__mock__/db.json --watch --port 3003",
|
||||
"mock": "json-server mock/db.json --watch --port 3003",
|
||||
"commit": "git cz",
|
||||
"git": "git add . && git cz && git push"
|
||||
},
|
||||
"dependencies": {
|
||||
"@micro-zoe/micro-app": "^0.8.3",
|
||||
"@vueuse/core": "^7.5.4",
|
||||
"@vueuse/head": "^0.7.5",
|
||||
"dotenv": "^14.3.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.0.9",
|
||||
"prism-theme-vars": "^0.2.2",
|
||||
"vite-plugin-compression": "^0.4.0",
|
||||
"vite-plugin-vue-setup-extend": "^0.3.0",
|
||||
"vue": "^3.2.29",
|
||||
"vue-demi": "^0.12.1",
|
||||
"vue-i18n": "^9.1.9",
|
||||
"vue-router": "^4.0.12",
|
||||
"@aomao/engine": "^2.6.16",
|
||||
"@aomao/plugin-alignment": "^2.6.16",
|
||||
"@aomao/plugin-backcolor": "^2.6.16",
|
||||
|
@ -49,19 +78,64 @@
|
|||
"core-js": "^3.6.5",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"sharedb": "^1.9.2",
|
||||
"vue": "^3.2.9",
|
||||
"vue-class-component": "^8.0.0-0",
|
||||
"vue-router": "^4.0.0-0"
|
||||
"lodash-es": "^4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/sharedb": "^1.0.14",
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-router": "~4.5.0",
|
||||
"@vue/cli-plugin-typescript": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/compiler-sfc": "^3.2.9",
|
||||
"@commitlint/cli": "^15.0.0",
|
||||
"@commitlint/config-conventional": "^15.0.0",
|
||||
"@iconify-json/carbon": "^1.0.14",
|
||||
"@iconify-json/el": "^1.0.1",
|
||||
"@iconify-json/mi": "^1.0.1",
|
||||
"@intlify/vite-plugin-vue-i18n": "^3.2.1",
|
||||
"@types/glob": "^7.2.0",
|
||||
"@types/markdown-it-link-attributes": "^3.0.1",
|
||||
"@types/node": "^17.0.11",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.18.0",
|
||||
"@typescript-eslint/parser": "^4.18.0",
|
||||
"@vitejs/plugin-vue": "^2.1.0",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^10.0.0",
|
||||
"@vue/test-utils": "^2.0.0-rc.18",
|
||||
"commitlint-config-cz": "^0.13.3",
|
||||
"critters": "^0.0.16",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^9.3.1",
|
||||
"cz-conventional-changelog": "^2.1.0",
|
||||
"cz-customizable": "^6.3.0",
|
||||
"esdk-obs-nodejs": "^3.21.6",
|
||||
"eslint": "7",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"eslint-plugin-vue": "^8.0.1",
|
||||
"glob": "^7.2.0",
|
||||
"https-localhost": "^4.7.0",
|
||||
"husky": "^7.0.1",
|
||||
"json-server": "^0.16.3",
|
||||
"lint-staged": "^11.1.2",
|
||||
"markdown-it-link-attributes": "^4.0.0",
|
||||
"markdown-it-prism": "^2.2.2",
|
||||
"pnpm": "^6.28.0",
|
||||
"prettier": "^2.3.2",
|
||||
"prettier-eslint-cli": "^5.0.1",
|
||||
"pretty-quick": "^3.1.1",
|
||||
"ts-node": "^10.4.0",
|
||||
"typescript": "^4.5.5",
|
||||
"unplugin-auto-import": "^0.5.11",
|
||||
"unplugin-icons": "^0.13.0",
|
||||
"unplugin-vue-components": "^0.17.14",
|
||||
"vite": "^2.7.13",
|
||||
"vite-plugin-inspect": "^0.3.13",
|
||||
"vite-plugin-md": "^0.11.7",
|
||||
"vite-plugin-pages": "^0.20.0",
|
||||
"vite-plugin-pwa": "^0.11.13",
|
||||
"vite-plugin-vue-layouts": "^0.5.0",
|
||||
"vite-plugin-windicss": "^1.6.3",
|
||||
"vite-ssg": "^0.17.7",
|
||||
"vitest": "0.1.27",
|
||||
"vue-tsc": "^0.31.1",
|
||||
"workbox-window": "^6.4.2",
|
||||
"less": "^3.0.4",
|
||||
"less-loader": "6.0.0",
|
||||
"typescript": "~4.1.5"
|
||||
"less-loader": "6.0.0"
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,3 @@
|
|||
/assets/*
|
||||
cache-control: max-age=31536000
|
||||
cache-control: immutable
|
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB |
|
@ -0,0 +1,9 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<style>
|
||||
path { fill: #222; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { fill: #ffffff; }
|
||||
}
|
||||
</style>
|
||||
<path d="M27.562 26L17.17 8.928l2.366-3.888L17.828 4L16 7.005L14.17 4l-1.708 1.04l2.366 3.888L4.438 26H2v2h28v-2zM16 10.85L25.22 26H17v-8h-2v8H6.78z" />
|
||||
</svg>
|
After Width: | Height: | Size: 347 B |
|
@ -1,17 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Allow: /
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M2916 6015 c-93 -57 -173 -108 -178 -113 -6 -6 7 -36 33 -78 23 -38
|
||||
86 -141 139 -229 54 -88 135 -221 180 -295 46 -74 94 -155 108 -180 14 -25 29
|
||||
-52 35 -60 7 -12 -9 -45 -62 -130 -39 -63 -85 -140 -103 -170 -18 -30 -117
|
||||
-194 -222 -365 -104 -170 -199 -326 -210 -346 -12 -19 -61 -102 -111 -183 -49
|
||||
-81 -101 -166 -115 -189 -14 -23 -39 -64 -55 -90 -17 -27 -77 -126 -134 -220
|
||||
-57 -95 -127 -210 -156 -257 -194 -315 -325 -533 -325 -541 0 -5 -4 -9 -10 -9
|
||||
-5 0 -10 -4 -10 -9 0 -5 -55 -98 -121 -207 -247 -404 -403 -660 -416 -684 -8
|
||||
-14 -58 -97 -112 -185 l-98 -160 -189 -2 c-104 -1 -225 -2 -269 -2 l-80 -1 1
|
||||
-210 c0 -116 4 -213 8 -218 11 -11 6107 -9 6114 2 8 13 8 406 0 419 -4 7 -88
|
||||
10 -265 9 l-259 -2 -50 77 c-27 43 -54 87 -60 98 -6 11 -62 103 -124 205 -62
|
||||
102 -120 197 -129 212 -9 16 -85 142 -170 280 -85 139 -160 262 -165 273 -6
|
||||
11 -13 22 -16 25 -3 3 -30 46 -59 95 -30 50 -102 169 -161 265 -59 96 -240
|
||||
393 -402 660 -163 267 -371 609 -463 760 -92 151 -194 318 -225 370 -31 52
|
||||
-101 167 -155 255 l-97 160 27 50 c16 27 32 55 36 61 5 5 38 59 74 120 36 60
|
||||
69 116 74 124 5 8 75 122 155 253 81 131 144 242 141 247 -4 7 -114 76 -183
|
||||
115 -10 6 -52 32 -95 58 -42 27 -81 46 -87 42 -8 -5 -94 -140 -140 -219 -19
|
||||
-33 -221 -365 -246 -404 -15 -22 -18 -18 -111 135 -52 87 -123 203 -157 258
|
||||
-67 108 -67 110 -111 184 -16 28 -34 51 -40 50 -5 0 -86 -47 -179 -104z m739
|
||||
-1642 c319 -526 519 -854 637 -1046 43 -70 78 -130 78 -133 0 -2 5 -10 10 -17
|
||||
6 -7 69 -109 140 -227 72 -118 134 -222 139 -230 5 -8 55 -89 111 -180 56 -91
|
||||
105 -172 110 -180 9 -14 52 -84 270 -445 54 -88 135 -221 180 -295 46 -74 91
|
||||
-148 100 -165 9 -16 31 -53 48 -81 18 -28 32 -54 32 -57 0 -3 -403 -6 -895 -5
|
||||
l-895 0 0 81 c-1 45 -1 439 -1 875 l0 792 -37 1 c-57 1 -344 1 -374 0 l-27 -1
|
||||
0 -832 c0 -458 0 -852 0 -875 l-1 -42 -895 1 c-492 0 -895 3 -895 5 0 9 115
|
||||
198 122 201 5 2 8 7 8 12 0 5 23 46 51 92 28 46 78 128 112 183 33 55 70 116
|
||||
82 135 12 19 132 215 265 435 133 220 266 438 295 485 65 105 206 338 220 362
|
||||
6 10 172 284 370 608 198 325 387 635 420 690 33 55 62 100 65 100 3 0 73
|
||||
-111 155 -247z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
|
@ -0,0 +1,20 @@
|
|||
const fs = require('fs')
|
||||
const { upToHWOSS } = require('./upToHW')
|
||||
|
||||
// 得出cdn路径前缀
|
||||
const calcCdnPathSuffix = (mode) => {
|
||||
const v = fs.readFileSync('./config/version.txt', 'utf-8')
|
||||
|
||||
const prefix = `desktop/${mode}`
|
||||
|
||||
let suffix = `${prefix}/latest`
|
||||
|
||||
if (v) {
|
||||
suffix = `${prefix}/${v}`
|
||||
}
|
||||
return suffix
|
||||
}
|
||||
|
||||
const mode = process.env.MODE || 'development'
|
||||
|
||||
upToHWOSS(calcCdnPathSuffix(mode))
|
|
@ -0,0 +1,73 @@
|
|||
const buildDir = 'build'
|
||||
const ossBucket = 'mobile-static'
|
||||
|
||||
function upToHWOSS(suffix) {
|
||||
const glob = require('glob')
|
||||
const path = require('path')
|
||||
const ObsClient = require('esdk-obs-nodejs')
|
||||
|
||||
const isWindow = /^win/.test(process.platform)
|
||||
|
||||
let pre = path.resolve(__dirname, `../${buildDir}/`) + (isWindow ? '\\' : '')
|
||||
|
||||
const files = glob.sync(
|
||||
`${path.join(
|
||||
__dirname,
|
||||
`../${buildDir}/**/*.?(js|json|ico|css|png|jpg|svg|woff|woff2|ttf|eot|gz)`
|
||||
)}`
|
||||
)
|
||||
pre = pre.replace(/\\/g, '/')
|
||||
// ObsClient fill your own
|
||||
const obsClient = new ObsClient({
|
||||
access_key_id: '',
|
||||
secret_access_key: '',
|
||||
server: '',
|
||||
})
|
||||
async function uploadFileCDN(files) {
|
||||
files.map(async (file) => {
|
||||
let key = getFileKey(pre, file)
|
||||
key = suffix + '/' + key
|
||||
try {
|
||||
await uploadFIle(key, file)
|
||||
console.log(`upload key: ${key}`)
|
||||
} catch (err) {
|
||||
console.log('error', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
async function uploadFIle(key, localFile) {
|
||||
return new Promise((resolve, reject) => {
|
||||
obsClient.putObject(
|
||||
{
|
||||
Bucket: ossBucket,
|
||||
Key: key,
|
||||
SourceFile: localFile,
|
||||
},
|
||||
(err, result) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(result)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
function getFileKey(pre, file) {
|
||||
if (file.indexOf(pre) > -1) {
|
||||
const key = file.split(pre)[1]
|
||||
return key.startsWith('/') ? key.substring(1) : key
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
console.time('uploading to oss...')
|
||||
await uploadFileCDN(files)
|
||||
console.timeEnd('upload end')
|
||||
})()
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
upToHWOSS,
|
||||
}
|
|
@ -1,29 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
// https://github.com/vueuse/head
|
||||
// you can use this to manipulate the document head in any components,
|
||||
// they will be rendered correctly in the html results with vite-ssg
|
||||
useHead({
|
||||
title: 'Modern Vue',
|
||||
meta: [{ name: 'description', content: 'Opinionated Vite Starter Template' }],
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="nav">
|
||||
<router-link to="/">Home</router-link> |
|
||||
<router-link to="/about">About</router-link>
|
||||
</div>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
#app{
|
||||
padding-top: 42px;
|
||||
}
|
||||
#nav {
|
||||
padding: 10px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
z-index: 999;
|
||||
a {
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
|
||||
&.router-link-exact-active {
|
||||
color: #42b983;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 6.7 KiB |
|
@ -0,0 +1,8 @@
|
|||
import type { App } from 'vue'
|
||||
import Comp from './src/main.vue'
|
||||
|
||||
Comp.install = function (Vue: App) {
|
||||
Vue.component(Comp.name, Comp)
|
||||
}
|
||||
|
||||
export default Comp
|
|
@ -0,0 +1,287 @@
|
|||
import { PluginEntry, CardEntry, PluginOptions, NodeInterface } from '@aomao/engine'
|
||||
//引入插件 begin
|
||||
import Redo from '@aomao/plugin-redo'
|
||||
import Undo from '@aomao/plugin-undo'
|
||||
import Bold from '@aomao/plugin-bold'
|
||||
import Code from '@aomao/plugin-code'
|
||||
import Backcolor from '@aomao/plugin-backcolor'
|
||||
import Fontcolor from '@aomao/plugin-fontcolor'
|
||||
import Fontsize from '@aomao/plugin-fontsize'
|
||||
import Italic from '@aomao/plugin-italic'
|
||||
import Underline from '@aomao/plugin-underline'
|
||||
import Hr, { HrComponent } from '@aomao/plugin-hr'
|
||||
import Tasklist, { CheckboxComponent } from '@aomao/plugin-tasklist'
|
||||
import Orderedlist from '@aomao/plugin-orderedlist'
|
||||
import Unorderedlist from '@aomao/plugin-unorderedlist'
|
||||
import Indent from '@aomao/plugin-indent'
|
||||
import Heading from '@aomao/plugin-heading'
|
||||
import Strikethrough from '@aomao/plugin-strikethrough'
|
||||
import Sub from '@aomao/plugin-sub'
|
||||
import Sup from '@aomao/plugin-sup'
|
||||
import Alignment from '@aomao/plugin-alignment'
|
||||
import Mark from '@aomao/plugin-mark'
|
||||
import Quote from '@aomao/plugin-quote'
|
||||
import PaintFormat from '@aomao/plugin-paintformat'
|
||||
import RemoveFormat from '@aomao/plugin-removeformat'
|
||||
import SelectAll from '@aomao/plugin-selectall'
|
||||
import Link from '@aomao/plugin-link-vue'
|
||||
import Codeblock, { CodeBlockComponent } from '@aomao/plugin-codeblock-vue'
|
||||
import Image, { ImageComponent, ImageUploader } from '@aomao/plugin-image'
|
||||
import Table, { TableComponent } from '@aomao/plugin-table'
|
||||
import File, { FileComponent, FileUploader } from '@aomao/plugin-file'
|
||||
import Video, { VideoComponent, VideoUploader } from '@aomao/plugin-video'
|
||||
import Math, { MathComponent } from '@aomao/plugin-math'
|
||||
import Fontfamily from '@aomao/plugin-fontfamily'
|
||||
import Status, { StatusComponent } from '@aomao/plugin-status'
|
||||
import LineHeight from '@aomao/plugin-line-height'
|
||||
import Mention, { MentionComponent } from '@aomao/plugin-mention'
|
||||
import {
|
||||
ToolbarPlugin,
|
||||
ToolbarComponent,
|
||||
fontFamilyDefaultData,
|
||||
} from '@aomao/toolbar-vue'
|
||||
|
||||
import Empty from 'ant-design-vue/es/empty'
|
||||
import 'ant-design-vue/es/empty/style'
|
||||
import { createApp } from 'vue'
|
||||
import Loading from '../src/loading.vue'
|
||||
import MentionPopover from '../src/mention.vue'
|
||||
|
||||
const DOMAIN = 'https://editor.aomao.com/api'
|
||||
|
||||
export const plugins: Array<PluginEntry> = [
|
||||
Redo,
|
||||
Undo,
|
||||
Bold,
|
||||
Code,
|
||||
Backcolor,
|
||||
Fontcolor,
|
||||
Fontsize,
|
||||
Italic,
|
||||
Underline,
|
||||
Hr,
|
||||
Tasklist,
|
||||
Orderedlist,
|
||||
Unorderedlist,
|
||||
Indent,
|
||||
Heading,
|
||||
Strikethrough,
|
||||
Sub,
|
||||
Sup,
|
||||
Alignment,
|
||||
Mark,
|
||||
Quote,
|
||||
PaintFormat,
|
||||
RemoveFormat,
|
||||
SelectAll,
|
||||
Link,
|
||||
Codeblock,
|
||||
Image,
|
||||
ImageUploader,
|
||||
Table,
|
||||
File,
|
||||
FileUploader,
|
||||
Video,
|
||||
VideoUploader,
|
||||
Math,
|
||||
ToolbarPlugin,
|
||||
Fontfamily,
|
||||
Status,
|
||||
LineHeight,
|
||||
Mention,
|
||||
]
|
||||
|
||||
export const cards: Array<CardEntry> = [
|
||||
HrComponent,
|
||||
CheckboxComponent,
|
||||
CodeBlockComponent,
|
||||
ImageComponent,
|
||||
TableComponent,
|
||||
FileComponent,
|
||||
VideoComponent,
|
||||
MathComponent,
|
||||
ToolbarComponent,
|
||||
StatusComponent,
|
||||
MentionComponent,
|
||||
]
|
||||
|
||||
export const pluginConfig: { [key: string]: PluginOptions } = {
|
||||
[ToolbarPlugin.pluginName]: {
|
||||
popup: {
|
||||
items: [
|
||||
['bold', 'strikethrough', 'fontcolor'],
|
||||
{
|
||||
icon: 'text',
|
||||
items: ['italic', 'underline', 'backcolor', 'moremark'],
|
||||
},
|
||||
[
|
||||
{
|
||||
type: 'button',
|
||||
name: 'image-uploader',
|
||||
icon: 'image',
|
||||
},
|
||||
'link',
|
||||
'tasklist',
|
||||
'heading',
|
||||
],
|
||||
{
|
||||
icon: 'more',
|
||||
items: [
|
||||
{
|
||||
type: 'button',
|
||||
name: 'video-uploader',
|
||||
icon: 'video',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'file-uploader',
|
||||
icon: 'attachment',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'table',
|
||||
icon: 'table',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'math',
|
||||
icon: 'math',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'codeblock',
|
||||
icon: 'codeblock',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'orderedlist',
|
||||
icon: 'ordered-list',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'unordered-list',
|
||||
icon: 'unordered-list',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'hr',
|
||||
icon: 'hr',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'quote',
|
||||
icon: 'quote',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
[Italic.pluginName]: {
|
||||
// 默认为 _ 下划线,这里修改为单个 * 号
|
||||
markdown: '*',
|
||||
},
|
||||
[Image.pluginName]: {
|
||||
onBeforeRender: (status: string, url: string) => {
|
||||
if (url.startsWith('data:image/')) return url
|
||||
return url + `?token=12323`
|
||||
},
|
||||
},
|
||||
[ImageUploader.pluginName]: {
|
||||
file: {
|
||||
action: `${DOMAIN}/upload/image`,
|
||||
headers: { Authorization: 213434 },
|
||||
},
|
||||
remote: {
|
||||
action: `${DOMAIN}/upload/image`,
|
||||
},
|
||||
isRemote: (src: string) => src.indexOf(DOMAIN) < 0,
|
||||
},
|
||||
[FileUploader.pluginName]: {
|
||||
action: `${DOMAIN}/upload/file`,
|
||||
},
|
||||
[VideoUploader.pluginName]: {
|
||||
action: `${DOMAIN}/upload/video`,
|
||||
limitSize: 1024 * 1024 * 50,
|
||||
},
|
||||
[Video.pluginName]: {
|
||||
onBeforeRender: (status: string, url: string) => {
|
||||
return url + `?token=12323`
|
||||
},
|
||||
},
|
||||
[Math.pluginName]: {
|
||||
action: `https://g.aomao.com/latex`,
|
||||
parse: (res: any) => {
|
||||
if (res.success) return { result: true, data: res.svg }
|
||||
return { result: false }
|
||||
},
|
||||
},
|
||||
[Mention.pluginName]: {
|
||||
action: `${DOMAIN}/user/search`,
|
||||
onLoading: (root: NodeInterface) => {
|
||||
const vm = createApp(Loading)
|
||||
vm.mount(root.get<HTMLElement>()!)
|
||||
},
|
||||
onEmpty: (root: NodeInterface) => {
|
||||
const vm = createApp(Empty)
|
||||
vm.mount(root.get<HTMLElement>()!)
|
||||
},
|
||||
onClick: (root: NodeInterface, { key, name }: { key: string; name: string }) => {
|
||||
console.log('mention click:', key, '-', name)
|
||||
},
|
||||
onMouseEnter: (layout: NodeInterface, { name }: { key: string; name: string }) => {
|
||||
const vm = createApp(MentionPopover, {
|
||||
name,
|
||||
})
|
||||
vm.mount(layout.get<HTMLElement>()!)
|
||||
},
|
||||
},
|
||||
[Fontsize.pluginName]: {
|
||||
//配置粘贴后需要过滤的字体大小
|
||||
filter: (fontSize: string) => {
|
||||
return (
|
||||
[
|
||||
'12px',
|
||||
'13px',
|
||||
'14px',
|
||||
'15px',
|
||||
'16px',
|
||||
'19px',
|
||||
'22px',
|
||||
'24px',
|
||||
'29px',
|
||||
'32px',
|
||||
'40px',
|
||||
'48px',
|
||||
].indexOf(fontSize) > -1
|
||||
)
|
||||
},
|
||||
},
|
||||
[Fontfamily.pluginName]: {
|
||||
//配置粘贴后需要过滤的字体
|
||||
filter: (fontfamily: string) => {
|
||||
const item = fontFamilyDefaultData.find((item) =>
|
||||
fontfamily
|
||||
.split(',')
|
||||
.some(
|
||||
(name) =>
|
||||
item.value.toLowerCase().indexOf(name.replace(/"/, '').toLowerCase()) > -1
|
||||
)
|
||||
)
|
||||
return item ? item.value : false
|
||||
},
|
||||
},
|
||||
[LineHeight.pluginName]: {
|
||||
//配置粘贴后需要过滤的行高
|
||||
filter: (lineHeight: string) => {
|
||||
if (lineHeight === '14px') return '1'
|
||||
if (lineHeight === '16px') return '1.15'
|
||||
if (lineHeight === '21px') return '1.5'
|
||||
if (lineHeight === '28px') return '2'
|
||||
if (lineHeight === '35px') return '2.5'
|
||||
if (lineHeight === '42px') return '3'
|
||||
// 不满足条件就移除掉
|
||||
return ['1', '1.15', '1.5', '2', '2.5', '3'].indexOf(lineHeight) > -1
|
||||
},
|
||||
},
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export const demoContent =
|
||||
'<h1 style="text-align:center;">民事起诉状</h1><br><h3>原告:<span style="color: #d9d9d9">原某某</span></h3><!--[--><p style=";line-height:1.5; font-family:宋体 ;font-size:12pt;text-indent:2em;"><span><span style="color: #d9d9d9">性别</span></span> , <span><span style="color: #d9d9d9">汉族</span></span> , <span><span style="color: #d9d9d9">出生日期</span></span><span>出生</span></p><p style=";line-height:1.5; font-family:宋体 ;font-size:12pt; ;text-indent:2em;"> 地址: <span><span style="color: #d9d9d9">身份证上的住址</span></span></p><p style=";line-height:1.5; font-family:宋体 ;font-size:12pt; ;text-indent:2em;"> 公民身份号码: <span><span style="color: #d9d9d9">1234567890123</span></span></p><p style=";line-height:1.5; font-family:宋体 ;font-size:12pt; ;text-indent:2em;"> 联系电话: <span><span style="color: #d9d9d9">18877777777</span></span></p><!--]--><!----><!----><br><!--]--><!--[--><h3>被告:<span style="color: #d9d9d9">被某某</span></h3><!--[--><p style=";line-height:1.5; font-family:宋体 ;font-size:12pt; ;text-indent:2em;"><span><span style="color: #d9d9d9">性别</span></span> , <span><span style="color: #d9d9d9">汉族</span></span> , <span><span style="color: #d9d9d9">出生日期</span></span><span>出生</span></p><p style=";line-height:1.5; font-family:宋体 ;font-size:12pt; ;text-indent:2em;"> 地址: <span><span style="color: #d9d9d9">身份证上的住址</span></span></p><p style=";line-height:1.5; font-family:宋体 ;font-size:12pt; ;text-indent:2em;"> 公民身份号码: <span><span style="color: #d9d9d9">1234567890123</span></span></p><p style=";line-height:1.5; font-family:宋体 ;font-size:12pt; ;text-indent:2em;"> 联系电话: <span><span style="color: #d9d9d9">18877777777</span></span></p><!--]--><!----><!----><br><!--]--><!--]--><br><h3>诉讼请求:</h3><!--[--><p style=";line-height:1.5; font-family:宋体 ;font-size:12pt; ;text-indent:2em;"><span><span style="color: #d9d9d9">诉讼请求</span></span></p><!--]--><br><br><h3>事实与理由:</h3><!--[--><p style=";line-height:1.5; font-family:宋体 ;font-size:12pt; ;text-indent:2em;"><span style="color: #d9d9d9">围绕诉求,将与诉求有关的事实于理由写明,注意要简明扼要,抓住要点。</span></p><!--]--><br><br><p style=";text-indent:2em; font-family:宋体">此 致</p><p style=";text-align:left; font-family:宋体">南京市雨花台区人民法院</p><br><br><br><br><p style=";line-height:1.5; font-family:宋体 ;font-size:12pt; ;text-indent:2em; ;text-align:right;"> 具状人:<span style=";line-height:1.5; font-family:宋体 ;font-size:12pt; ;color:#d9d9d9;"></span></p><p style=";line-height:1.5; font-family:宋体 ;font-size:12pt; ;text-indent:2em; ;text-align:right;">2022年2月8日</p>'
|
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<a-spin class="loading" :tip="text" :spinning="loading">
|
||||
<slot></slot>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import ASpin from 'ant-design-vue/es/spin'
|
||||
import 'ant-design-vue/es/spin/style'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AmLoading',
|
||||
components: {
|
||||
ASpin,
|
||||
},
|
||||
props: {
|
||||
text: String,
|
||||
loading: Boolean,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<style css>
|
||||
.loading {
|
||||
padding: 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ant-spin-nested-loading {
|
||||
position: inherit;
|
||||
}
|
||||
.ant-spin-nested-loading .ant-spin-container {
|
||||
position: inherit;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,248 @@
|
|||
<template>
|
||||
<AmLoading :loading="loading">
|
||||
<AmToolbar v-if="engine" :engine="engine" :items="items" />
|
||||
<div :class="['editor-wrapper', { 'editor-mobile': isMobile }]">
|
||||
<div class="editor-container text-left">
|
||||
<div class="editor-content">
|
||||
<div ref="container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AmLoading>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import Engine, { $, EngineInterface, isMobile } from '@aomao/engine'
|
||||
import AmToolbar, { GroupItemProps } from '@aomao/toolbar-vue'
|
||||
import AmLoading from './loading.vue'
|
||||
import { cards, plugins, pluginConfig } from './config'
|
||||
import 'ant-design-vue/es/style'
|
||||
import { demoContent } from './demoData'
|
||||
|
||||
// toolbar
|
||||
const items = isMobile
|
||||
? ref<GroupItemProps[]>([
|
||||
['undo', 'redo'],
|
||||
{
|
||||
icon: 'text',
|
||||
items: ['bold', 'italic', 'strikethrough', 'underline', 'moremark'],
|
||||
},
|
||||
[
|
||||
{
|
||||
type: 'button',
|
||||
name: 'image-uploader',
|
||||
icon: 'image',
|
||||
},
|
||||
'link',
|
||||
'tasklist',
|
||||
'heading',
|
||||
],
|
||||
{
|
||||
icon: 'more',
|
||||
items: [
|
||||
{
|
||||
type: 'button',
|
||||
name: 'video-uploader',
|
||||
icon: 'video',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'file-uploader',
|
||||
icon: 'attachment',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'table',
|
||||
icon: 'table',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'math',
|
||||
icon: 'math',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'codeblock',
|
||||
icon: 'codeblock',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'orderedlist',
|
||||
icon: 'orderedlist',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'unorderedlist',
|
||||
icon: 'unorderedlist',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'hr',
|
||||
icon: 'hr',
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
: ref<GroupItemProps[]>([
|
||||
['collapse'],
|
||||
['undo', 'redo', 'paintformat', 'removeformat'],
|
||||
['heading', 'fontfamily', 'fontsize'],
|
||||
['bold', 'italic', 'strikethrough', 'underline', 'moremark'],
|
||||
['fontcolor', 'backcolor'],
|
||||
['alignment'],
|
||||
['unorderedlist', 'orderedlist', 'tasklist', 'indent', 'line-height'],
|
||||
['link', 'quote', 'hr'],
|
||||
])
|
||||
|
||||
// 编辑器容器
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
// 编辑器引擎
|
||||
const engine = ref<EngineInterface | null>(null)
|
||||
// 默认设置为当前在加载中
|
||||
const loading = ref(true)
|
||||
onMounted(() => {
|
||||
// 容器加载后实例化编辑器引擎
|
||||
if (container.value) {
|
||||
//实例化引擎
|
||||
const engineInstance = new Engine(container.value, {
|
||||
// 启用的插件
|
||||
plugins,
|
||||
// 启用的卡片
|
||||
cards,
|
||||
// 所有的卡片配置
|
||||
config: pluginConfig,
|
||||
})
|
||||
// 设置显示成功消息UI,默认使用 console.log
|
||||
engineInstance.messageSuccess = (msg: string) => {
|
||||
message.success(msg)
|
||||
}
|
||||
// 设置显示错误消息UI,默认使用 console.error
|
||||
engineInstance.messageError = (error: string) => {
|
||||
message.error(error)
|
||||
}
|
||||
// 设置显示确认消息UI,默认无
|
||||
engineInstance.messageConfirm = (msg: string) => {
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
Modal.confirm({
|
||||
content: msg,
|
||||
onOk: () => resolve(true),
|
||||
onCancel: () => reject(),
|
||||
})
|
||||
})
|
||||
}
|
||||
//卡片最大化时设置编辑页面样式
|
||||
engineInstance.on('card:maximize', () => {
|
||||
$('.editor-toolbar').css('z-index', '9999').css('top', '55px')
|
||||
})
|
||||
engineInstance.on('card:minimize', () => {
|
||||
$('.editor-toolbar').css('z-index', '').css('top', '')
|
||||
})
|
||||
// 默认编辑器值,为了演示,这里初始化值写死,正式环境可以请求api加载
|
||||
|
||||
const value = demoContent
|
||||
// 使用协同编辑,需要安装 mongodb 数据库,并且配置 ot-server/client 中的数据库连接,最后 yarn start 启动 ot-server 服务
|
||||
|
||||
// 非协同编辑,设置编辑器值,异步渲染后回调
|
||||
engineInstance.setValue(value, () => {
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
// 监听编辑器值改变事件
|
||||
engineInstance.on('change', () => {
|
||||
console.log('value', engineInstance.getJsonValue())
|
||||
console.log('html:', engineInstance.getHtml())
|
||||
})
|
||||
|
||||
engine.value = engineInstance
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (engine.value) engine.value.destroy()
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
#app {
|
||||
padding: 0;
|
||||
}
|
||||
#nav {
|
||||
position: relative;
|
||||
}
|
||||
.editor-ot-users {
|
||||
font-size: 12px;
|
||||
background: #ffffff;
|
||||
padding: 0px 0 8px 266px;
|
||||
z-index: 999;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.editor-ot-users-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.editor-ot-users .ant-avatar {
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.editor-toolbar {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.02);
|
||||
z-index: 1000;
|
||||
}
|
||||
.editor-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: 1440px;
|
||||
}
|
||||
|
||||
.editor-wrapper.editor-mobile {
|
||||
min-width: auto;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
background: #fafafa;
|
||||
background-color: #fafafa;
|
||||
padding: 62px 0 64px;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.editor-mobile .editor-container {
|
||||
padding: 0;
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor-content {
|
||||
position: relative;
|
||||
width: 812px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
overflow: hidden;
|
||||
min-height: 800px;
|
||||
}
|
||||
|
||||
.editor-mobile .editor-content {
|
||||
width: auto;
|
||||
min-height: calc(100vh - 68px);
|
||||
border: 0 none;
|
||||
}
|
||||
|
||||
.editor-content .am-engine {
|
||||
padding: 40px 60px 60px;
|
||||
}
|
||||
|
||||
.editor-mobile .editor-content .am-engine {
|
||||
padding: 18px 0 0 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,27 @@
|
|||
<template>
|
||||
<div class="mention-container">
|
||||
<p>This is name: {{ name }}</p>
|
||||
<p>配置 mention 插件的 onMouseEnter 方法</p>
|
||||
<p>此处使用 createApp().mount 自定义渲染</p>
|
||||
<p>Use createApp().mount to customize rendering here</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AmMention',
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<style css>
|
||||
.mention-container {
|
||||
padding: 5px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
initial: number
|
||||
}>()
|
||||
|
||||
const { count, inc, dec } = useCounter(props.initial)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
{{ count }}
|
||||
<button class="inc" @click="inc()">+</button>
|
||||
<button class="dec" @click="dec()">-</button>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,46 @@
|
|||
<script setup lang="ts">
|
||||
import { isDark, toggleDark } from '~/composables'
|
||||
|
||||
const { t, availableLocales, locale } = useI18n()
|
||||
|
||||
const toggleLocales = () => {
|
||||
// change to some real logic
|
||||
const locales = availableLocales
|
||||
locale.value = locales[(locales.indexOf(locale.value) + 1) % locales.length]
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="text-xl mt-6">
|
||||
<router-link class="icon-btn mx-2" to="/" :title="t('button.home')">
|
||||
<carbon-campsite />
|
||||
</router-link>
|
||||
|
||||
<button
|
||||
class="icon-btn mx-2 !outline-none"
|
||||
:title="t('button.toggle_dark')"
|
||||
@click="toggleDark()"
|
||||
>
|
||||
<carbon-moon v-if="isDark" />
|
||||
<carbon-sun v-else />
|
||||
</button>
|
||||
|
||||
<a class="icon-btn mx-2" :title="t('button.toggle_langs')" @click="toggleLocales">
|
||||
<carbon-language />
|
||||
</a>
|
||||
|
||||
<router-link class="icon-btn mx-2" to="/about" :title="t('button.about')">
|
||||
<carbon-dicom-overlay />
|
||||
</router-link>
|
||||
|
||||
<a
|
||||
class="icon-btn mx-2"
|
||||
rel="noreferrer"
|
||||
href="https://github.com/antfu/vitesse"
|
||||
target="_blank"
|
||||
title="GitHub"
|
||||
>
|
||||
<carbon-logo-github />
|
||||
</a>
|
||||
</nav>
|
||||
</template>
|
|
@ -0,0 +1,12 @@
|
|||
## Components
|
||||
|
||||
Components in this dir will be auto-registered and on-demand, powered by
|
||||
[`unplugin-vue-components`](https://github.com/antfu/unplugin-vue-components).
|
||||
|
||||
### Icons
|
||||
|
||||
You can use icons from almost any icon sets by the power of
|
||||
[Iconify](https://iconify.design/).
|
||||
|
||||
It will only bundle the icons you use. Check out
|
||||
[`unplugin-icons`](https://github.com/antfu/unplugin-icons) for more details.
|
|
@ -1,303 +0,0 @@
|
|||
import {
|
||||
PluginEntry,
|
||||
CardEntry,
|
||||
PluginOptions,
|
||||
NodeInterface,
|
||||
} from '@aomao/engine';
|
||||
//引入插件 begin
|
||||
import Redo from '@aomao/plugin-redo';
|
||||
import Undo from '@aomao/plugin-undo';
|
||||
import Bold from '@aomao/plugin-bold';
|
||||
import Code from '@aomao/plugin-code';
|
||||
import Backcolor from '@aomao/plugin-backcolor';
|
||||
import Fontcolor from '@aomao/plugin-fontcolor';
|
||||
import Fontsize from '@aomao/plugin-fontsize';
|
||||
import Italic from '@aomao/plugin-italic';
|
||||
import Underline from '@aomao/plugin-underline';
|
||||
import Hr, { HrComponent } from '@aomao/plugin-hr';
|
||||
import Tasklist, { CheckboxComponent } from '@aomao/plugin-tasklist';
|
||||
import Orderedlist from '@aomao/plugin-orderedlist';
|
||||
import Unorderedlist from '@aomao/plugin-unorderedlist';
|
||||
import Indent from '@aomao/plugin-indent';
|
||||
import Heading from '@aomao/plugin-heading';
|
||||
import Strikethrough from '@aomao/plugin-strikethrough';
|
||||
import Sub from '@aomao/plugin-sub';
|
||||
import Sup from '@aomao/plugin-sup';
|
||||
import Alignment from '@aomao/plugin-alignment';
|
||||
import Mark from '@aomao/plugin-mark';
|
||||
import Quote from '@aomao/plugin-quote';
|
||||
import PaintFormat from '@aomao/plugin-paintformat';
|
||||
import RemoveFormat from '@aomao/plugin-removeformat';
|
||||
import SelectAll from '@aomao/plugin-selectall';
|
||||
import Link from '@aomao/plugin-link-vue';
|
||||
import Codeblock, { CodeBlockComponent } from '@aomao/plugin-codeblock-vue';
|
||||
import Image, { ImageComponent, ImageUploader } from '@aomao/plugin-image';
|
||||
import Table, { TableComponent } from '@aomao/plugin-table';
|
||||
import File, { FileComponent, FileUploader } from '@aomao/plugin-file';
|
||||
import Video, { VideoComponent, VideoUploader } from '@aomao/plugin-video';
|
||||
import Math, { MathComponent } from '@aomao/plugin-math';
|
||||
import Fontfamily from '@aomao/plugin-fontfamily';
|
||||
import Status, { StatusComponent } from '@aomao/plugin-status';
|
||||
import LineHeight from '@aomao/plugin-line-height';
|
||||
import Mention, { MentionComponent } from '@aomao/plugin-mention';
|
||||
import {
|
||||
ToolbarPlugin,
|
||||
ToolbarComponent,
|
||||
fontFamilyDefaultData,
|
||||
} from '@aomao/toolbar-vue';
|
||||
|
||||
import Empty from 'ant-design-vue/es/empty';
|
||||
import 'ant-design-vue/es/empty/style';
|
||||
import { createApp } from 'vue';
|
||||
import Loading from './loading.vue';
|
||||
import MentionPopover from './mention.vue';
|
||||
|
||||
const DOMAIN = 'https://editor.aomao.com/api';
|
||||
|
||||
export const plugins: Array<PluginEntry> = [
|
||||
Redo,
|
||||
Undo,
|
||||
Bold,
|
||||
Code,
|
||||
Backcolor,
|
||||
Fontcolor,
|
||||
Fontsize,
|
||||
Italic,
|
||||
Underline,
|
||||
Hr,
|
||||
Tasklist,
|
||||
Orderedlist,
|
||||
Unorderedlist,
|
||||
Indent,
|
||||
Heading,
|
||||
Strikethrough,
|
||||
Sub,
|
||||
Sup,
|
||||
Alignment,
|
||||
Mark,
|
||||
Quote,
|
||||
PaintFormat,
|
||||
RemoveFormat,
|
||||
SelectAll,
|
||||
Link,
|
||||
Codeblock,
|
||||
Image,
|
||||
ImageUploader,
|
||||
Table,
|
||||
File,
|
||||
FileUploader,
|
||||
Video,
|
||||
VideoUploader,
|
||||
Math,
|
||||
ToolbarPlugin,
|
||||
Fontfamily,
|
||||
Status,
|
||||
LineHeight,
|
||||
Mention,
|
||||
];
|
||||
|
||||
export const cards: Array<CardEntry> = [
|
||||
HrComponent,
|
||||
CheckboxComponent,
|
||||
CodeBlockComponent,
|
||||
ImageComponent,
|
||||
TableComponent,
|
||||
FileComponent,
|
||||
VideoComponent,
|
||||
MathComponent,
|
||||
ToolbarComponent,
|
||||
StatusComponent,
|
||||
MentionComponent,
|
||||
];
|
||||
|
||||
export const pluginConfig: { [key: string]: PluginOptions } = {
|
||||
[ToolbarPlugin.pluginName]: {
|
||||
popup: {
|
||||
items: [
|
||||
['bold', 'strikethrough', 'fontcolor'],
|
||||
{
|
||||
icon: 'text',
|
||||
items: ['italic', 'underline', 'backcolor', 'moremark'],
|
||||
},
|
||||
[
|
||||
{
|
||||
type: 'button',
|
||||
name: 'image-uploader',
|
||||
icon: 'image',
|
||||
},
|
||||
'link',
|
||||
'tasklist',
|
||||
'heading',
|
||||
],
|
||||
{
|
||||
icon: 'more',
|
||||
items: [
|
||||
{
|
||||
type: 'button',
|
||||
name: 'video-uploader',
|
||||
icon: 'video',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'file-uploader',
|
||||
icon: 'attachment',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'table',
|
||||
icon: 'table',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'math',
|
||||
icon: 'math',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'codeblock',
|
||||
icon: 'codeblock',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'orderedlist',
|
||||
icon: 'ordered-list',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'unordered-list',
|
||||
icon: 'unordered-list',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'hr',
|
||||
icon: 'hr',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'quote',
|
||||
icon: 'quote',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
[Italic.pluginName]: {
|
||||
// 默认为 _ 下划线,这里修改为单个 * 号
|
||||
markdown: '*',
|
||||
},
|
||||
[Image.pluginName]: {
|
||||
onBeforeRender: (status: string, url: string) => {
|
||||
if (url.startsWith('data:image/')) return url;
|
||||
return url + `?token=12323`;
|
||||
},
|
||||
},
|
||||
[ImageUploader.pluginName]: {
|
||||
file: {
|
||||
action: `${DOMAIN}/upload/image`,
|
||||
headers: { Authorization: 213434 },
|
||||
},
|
||||
remote: {
|
||||
action: `${DOMAIN}/upload/image`,
|
||||
},
|
||||
isRemote: (src: string) => src.indexOf(DOMAIN) < 0,
|
||||
},
|
||||
[FileUploader.pluginName]: {
|
||||
action: `${DOMAIN}/upload/file`,
|
||||
},
|
||||
[VideoUploader.pluginName]: {
|
||||
action: `${DOMAIN}/upload/video`,
|
||||
limitSize: 1024 * 1024 * 50,
|
||||
},
|
||||
[Video.pluginName]: {
|
||||
onBeforeRender: (status: string, url: string) => {
|
||||
return url + `?token=12323`;
|
||||
},
|
||||
},
|
||||
[Math.pluginName]: {
|
||||
action: `https://g.aomao.com/latex`,
|
||||
parse: (res: any) => {
|
||||
if (res.success) return { result: true, data: res.svg };
|
||||
return { result: false };
|
||||
},
|
||||
},
|
||||
[Mention.pluginName]: {
|
||||
action: `${DOMAIN}/user/search`,
|
||||
onLoading: (root: NodeInterface) => {
|
||||
const vm = createApp(Loading);
|
||||
vm.mount(root.get<HTMLElement>()!);
|
||||
},
|
||||
onEmpty: (root: NodeInterface) => {
|
||||
const vm = createApp(Empty);
|
||||
vm.mount(root.get<HTMLElement>()!);
|
||||
},
|
||||
onClick: (
|
||||
root: NodeInterface,
|
||||
{ key, name }: { key: string; name: string },
|
||||
) => {
|
||||
console.log('mention click:', key, '-', name);
|
||||
},
|
||||
onMouseEnter: (
|
||||
layout: NodeInterface,
|
||||
{ name }: { key: string; name: string },
|
||||
) => {
|
||||
const vm = createApp(MentionPopover, {
|
||||
name,
|
||||
});
|
||||
vm.mount(layout.get<HTMLElement>()!);
|
||||
},
|
||||
},
|
||||
[Fontsize.pluginName]: {
|
||||
//配置粘贴后需要过滤的字体大小
|
||||
filter: (fontSize: string) => {
|
||||
return (
|
||||
[
|
||||
'12px',
|
||||
'13px',
|
||||
'14px',
|
||||
'15px',
|
||||
'16px',
|
||||
'19px',
|
||||
'22px',
|
||||
'24px',
|
||||
'29px',
|
||||
'32px',
|
||||
'40px',
|
||||
'48px',
|
||||
].indexOf(fontSize) > -1
|
||||
);
|
||||
},
|
||||
},
|
||||
[Fontfamily.pluginName]: {
|
||||
//配置粘贴后需要过滤的字体
|
||||
filter: (fontfamily: string) => {
|
||||
const item = fontFamilyDefaultData.find((item) =>
|
||||
fontfamily
|
||||
.split(',')
|
||||
.some(
|
||||
(name) =>
|
||||
item.value
|
||||
.toLowerCase()
|
||||
.indexOf(name.replace(/"/, '').toLowerCase()) >
|
||||
-1,
|
||||
),
|
||||
);
|
||||
return item ? item.value : false;
|
||||
},
|
||||
},
|
||||
[LineHeight.pluginName]: {
|
||||
//配置粘贴后需要过滤的行高
|
||||
filter: (lineHeight: string) => {
|
||||
if (lineHeight === '14px') return '1';
|
||||
if (lineHeight === '16px') return '1.15';
|
||||
if (lineHeight === '21px') return '1.5';
|
||||
if (lineHeight === '28px') return '2';
|
||||
if (lineHeight === '35px') return '2.5';
|
||||
if (lineHeight === '42px') return '3';
|
||||
// 不满足条件就移除掉
|
||||
return (
|
||||
['1', '1.15', '1.5', '2', '2.5', '3'].indexOf(lineHeight) > -1
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,315 +0,0 @@
|
|||
<template>
|
||||
<am-loading :loading="loading">
|
||||
<div class="editor-ot-users">
|
||||
<space class="editor-ot-users-content" size="small">
|
||||
<span v-if="!isMobile" style="color: '#888888'">
|
||||
当前在线<strong>{{members.length}}</strong>人
|
||||
</span>
|
||||
<avatar
|
||||
v-for="member in members"
|
||||
:key="member['id']"
|
||||
size="30"
|
||||
:style="{backgroundColor:member['color']}"
|
||||
>
|
||||
{{member['name']}}
|
||||
</avatar>
|
||||
</space>
|
||||
</div>
|
||||
<am-toolbar v-if="engine" :engine="engine" :items="items" />
|
||||
<div :class="['editor-wrapper',{'editor-mobile': isMobile}]">
|
||||
<div class="editor-container">
|
||||
<div class="editor-content">
|
||||
<div ref="container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</am-loading>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { Avatar, message, Modal, Space } from 'ant-design-vue'
|
||||
import Engine, { $, EngineInterface, isMobile } from "@aomao/engine"
|
||||
import AmToolbar from '@aomao/toolbar-vue'
|
||||
import AmLoading from './loading.vue'
|
||||
import { cards, plugins, pluginConfig} from './config'
|
||||
import OTClient from './ot-client'
|
||||
import 'ant-design-vue/es/style'
|
||||
|
||||
export default defineComponent({
|
||||
name:"engine-demo",
|
||||
components:{
|
||||
Avatar,
|
||||
Space,
|
||||
AmLoading,
|
||||
AmToolbar
|
||||
},
|
||||
data(){
|
||||
// toolbar 配置项
|
||||
return {
|
||||
items:isMobile ? [
|
||||
['undo', 'redo'],
|
||||
{
|
||||
icon:"text",
|
||||
items:[
|
||||
'bold',
|
||||
'italic',
|
||||
'strikethrough',
|
||||
'underline',
|
||||
'moremark',
|
||||
]
|
||||
},
|
||||
[
|
||||
{
|
||||
type: "button",
|
||||
name: 'image-uploader',
|
||||
icon: "image"
|
||||
},
|
||||
"link",
|
||||
"tasklist",
|
||||
"heading"
|
||||
],
|
||||
{
|
||||
icon: "more",
|
||||
items: [
|
||||
{
|
||||
type: "button",
|
||||
name: 'video-uploader',
|
||||
icon: "video"
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
name: 'file-uploader',
|
||||
icon: "attachment"
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
name: 'table',
|
||||
icon: "table"
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
name: 'math',
|
||||
icon: "math"
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
name: 'codeblock',
|
||||
icon: "codeblock"
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
name: "orderedlist",
|
||||
icon: "orderedlist"
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
name: "unorderedlist",
|
||||
icon: "unorderedlist"
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
name: "hr",
|
||||
icon: "hr"
|
||||
},
|
||||
]
|
||||
}
|
||||
]:[['collapse'],
|
||||
['undo', 'redo', 'paintformat', 'removeformat'],
|
||||
['heading', 'fontfamily', 'fontsize'],
|
||||
[
|
||||
'bold',
|
||||
'italic',
|
||||
'strikethrough',
|
||||
'underline',
|
||||
'moremark',
|
||||
],
|
||||
['fontcolor', 'backcolor'],
|
||||
['alignment'],
|
||||
['unorderedlist', 'orderedlist', 'tasklist', 'indent', 'line-height'],
|
||||
['link', 'quote', 'hr']]
|
||||
}
|
||||
},
|
||||
setup(){
|
||||
// 编辑器容器
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
// 编辑器引擎
|
||||
const engine = ref<EngineInterface | null>(null)
|
||||
// 当前所有协作用户
|
||||
const members = ref([])
|
||||
// 默认设置为当前在加载中
|
||||
const loading = ref(true)
|
||||
onMounted(() => {
|
||||
// 容器加载后实例化编辑器引擎
|
||||
if(container.value){
|
||||
//实例化引擎
|
||||
const engineInstance = new Engine(container.value,{
|
||||
// 启用的插件
|
||||
plugins,
|
||||
// 启用的卡片
|
||||
cards,
|
||||
// 所有的卡片配置
|
||||
config: pluginConfig,
|
||||
});
|
||||
// 设置显示成功消息UI,默认使用 console.log
|
||||
engineInstance.messageSuccess = (msg: string) => {
|
||||
message.success(msg);
|
||||
};
|
||||
// 设置显示错误消息UI,默认使用 console.error
|
||||
engineInstance.messageError = (error: string) => {
|
||||
message.error(error);
|
||||
};
|
||||
// 设置显示确认消息UI,默认无
|
||||
engineInstance.messageConfirm = (msg: string) => {
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
Modal.confirm({
|
||||
content: msg,
|
||||
onOk: () => resolve(true),
|
||||
onCancel: () => reject(),
|
||||
});
|
||||
});
|
||||
};
|
||||
//卡片最大化时设置编辑页面样式
|
||||
engineInstance.on('card:maximize', () => {
|
||||
$('.editor-toolbar').css('z-index', '9999').css('top', '55px');
|
||||
});
|
||||
engineInstance.on('card:minimize', () => {
|
||||
$('.editor-toolbar').css('z-index', '').css('top', '');
|
||||
});
|
||||
// 默认编辑器值,为了演示,这里初始化值写死,正式环境可以请求api加载
|
||||
const value = '<strong>Hello</strong>,<span style="color:red">am-editor</span>'
|
||||
// 使用协同编辑,需要安装 mongodb 数据库,并且配置 ot-server/client 中的数据库连接,最后 yarn start 启动 ot-server 服务
|
||||
let isOt = false
|
||||
if (isOt) {
|
||||
// 实例化协作编辑客户端
|
||||
const ot = new OTClient(engineInstance);
|
||||
// 获取当前用户,正式环境可以传Token到 ot-server 中验证
|
||||
const memberData = localStorage.getItem('member');
|
||||
const currentMember = !!memberData ? JSON.parse(memberData) : null;
|
||||
// 连接协同服务端,如果服务端没有对应docId的文档,将使用 value 初始化
|
||||
ot.connect(`ws://127.0.0.1:8080${currentMember ? '?uid=' + currentMember.id : ''}`, 'demo', value);
|
||||
ot.on('ready', member => {
|
||||
// 为了演示,保存当前会员信息
|
||||
if (member) localStorage.setItem('member', JSON.stringify(member));
|
||||
loading.value = false
|
||||
});
|
||||
//用户加入或退出改变
|
||||
ot.on('membersChange', currentMembers => {
|
||||
members.value = currentMembers;
|
||||
});
|
||||
} else {
|
||||
// 非协同编辑,设置编辑器值,异步渲染后回调
|
||||
engineInstance.setValue(value, () => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 监听编辑器值改变事件
|
||||
engineInstance.on('change', () => {
|
||||
console.log('value', engineInstance.getValue());
|
||||
console.log('html:', engineInstance.getHtml());
|
||||
});
|
||||
|
||||
engine.value = engineInstance
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if(engine.value) engine.value.destroy()
|
||||
})
|
||||
|
||||
return {
|
||||
loading,
|
||||
isMobile,
|
||||
container,
|
||||
engine,
|
||||
members
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
#app {
|
||||
padding:0
|
||||
}
|
||||
#nav {
|
||||
position: relative
|
||||
}
|
||||
.editor-ot-users {
|
||||
font-size: 12px;
|
||||
background: #ffffff;
|
||||
padding: 0px 0 8px 266px;
|
||||
z-index: 999;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.editor-ot-users-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.editor-ot-users .ant-avatar {
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.editor-toolbar {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.02);
|
||||
z-index: 1000;
|
||||
}
|
||||
.editor-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: 1440px;
|
||||
}
|
||||
|
||||
.editor-wrapper.editor-mobile {
|
||||
min-width: auto;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
background: #fafafa;
|
||||
background-color: #fafafa;
|
||||
padding: 62px 0 64px;
|
||||
height: calc(100vh - 68px);
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.editor-mobile .editor-container {
|
||||
padding: 0;
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor-content {
|
||||
position: relative;
|
||||
width: 812px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
overflow: hidden;
|
||||
min-height: 800px;
|
||||
|
||||
}
|
||||
|
||||
.editor-mobile .editor-content {
|
||||
width: auto;
|
||||
min-height:calc(100vh - 68px);
|
||||
border: 0 none;
|
||||
}
|
||||
|
||||
.editor-content .am-engine {
|
||||
padding: 40px 60px 60px;
|
||||
}
|
||||
|
||||
.editor-mobile .editor-content .am-engine {
|
||||
padding:18px 0 0 0;
|
||||
}
|
||||
|
||||
</style>
|
|
@ -1,35 +0,0 @@
|
|||
<template>
|
||||
<a-spin class="loading" :tip="text" :spinning="loading">
|
||||
<slot></slot>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue"
|
||||
import ASpin from 'ant-design-vue/es/spin'
|
||||
import 'ant-design-vue/es/spin/style';
|
||||
|
||||
export default defineComponent({
|
||||
name:"am-loading",
|
||||
components:{
|
||||
ASpin
|
||||
},
|
||||
props:{
|
||||
text: String,
|
||||
loading: Boolean
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<style css>
|
||||
.loading {
|
||||
padding: 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ant-spin-nested-loading {
|
||||
position: inherit;
|
||||
}
|
||||
.ant-spin-nested-loading .ant-spin-container {
|
||||
position: inherit;
|
||||
}
|
||||
</style>
|
|
@ -1,24 +0,0 @@
|
|||
<template>
|
||||
<div class="mention-container">
|
||||
<p>This is name: {{name}}</p>
|
||||
<p>配置 mention 插件的 onMouseEnter 方法</p>
|
||||
<p>此处使用 createApp().mount 自定义渲染</p>
|
||||
<p>Use createApp().mount to customize rendering here</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue"
|
||||
|
||||
export default defineComponent({
|
||||
name:"am-mention",
|
||||
props:{
|
||||
name: String
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<style css>
|
||||
.mention-container {
|
||||
padding: 5px;
|
||||
}
|
||||
</style>
|
|
@ -1,406 +0,0 @@
|
|||
import { EventEmitter } from 'events';
|
||||
import { EngineInterface } from '@aomao/engine';
|
||||
import ReconnectingWebSocket, { ErrorEvent } from 'reconnecting-websocket';
|
||||
import { Doc } from 'sharedb';
|
||||
import sharedb from 'sharedb/lib/client';
|
||||
import { Socket } from 'sharedb/lib/sharedb';
|
||||
|
||||
export type Member = {
|
||||
avatar: string;
|
||||
name: string;
|
||||
uuid: string;
|
||||
color?: string;
|
||||
};
|
||||
export const STATUS = {
|
||||
init: 'init',
|
||||
loaded: 'loaded',
|
||||
active: 'active',
|
||||
exit: 'exit',
|
||||
error: 'error',
|
||||
};
|
||||
|
||||
export const EVENT = {
|
||||
inactive: 'inactive',
|
||||
error: 'error',
|
||||
membersChange: 'membersChange',
|
||||
statusChange: 'statusChange',
|
||||
message: 'message',
|
||||
};
|
||||
|
||||
export type ERROR = {
|
||||
code: string;
|
||||
level: string;
|
||||
message: string;
|
||||
error?: ErrorEvent;
|
||||
};
|
||||
|
||||
export const ERROR_CODE = {
|
||||
INIT_FAILED: 'INIT_FAILED',
|
||||
SAVE_FAILED: 'SAVE_FAILED',
|
||||
PUBLISH_FAILED: 'PUBLISH_FAILED',
|
||||
DISCONNECTED: 'DISCONNECTED',
|
||||
STATUS_CODE: {
|
||||
TIMEOUT: 4001,
|
||||
FORCE_DISCONNECTED: 4002,
|
||||
},
|
||||
CONNECTION_ERROR: 'CONNECTION_ERROR',
|
||||
COLLAB_DOC_ERROR: 'COLLAB_DOC_ERROR',
|
||||
};
|
||||
|
||||
export const ERROR_LEVEL = {
|
||||
FATAL: 'FATAL',
|
||||
WARNING: 'WARNING',
|
||||
NOTICE: 'NOTICE',
|
||||
};
|
||||
/**
|
||||
* 协同客户端
|
||||
*/
|
||||
class OTClient extends EventEmitter {
|
||||
// 编辑器引擎
|
||||
protected engine: EngineInterface;
|
||||
// ws 连接实例
|
||||
protected socket?: WebSocket;
|
||||
// 当前协同的所有用户
|
||||
protected members: Array<Member> = [];
|
||||
// 当前用户
|
||||
protected current?: Member;
|
||||
// 当前状态
|
||||
protected status?: string;
|
||||
// 协作的文档对象
|
||||
protected doc?: Doc;
|
||||
// 当前 ws 是否关闭
|
||||
protected isClosed: boolean = true;
|
||||
// 心跳检测对象
|
||||
protected heartbeat?: {
|
||||
timeout: NodeJS.Timeout;
|
||||
datetime: Date;
|
||||
};
|
||||
|
||||
constructor(engine: EngineInterface) {
|
||||
super();
|
||||
this.engine = engine;
|
||||
}
|
||||
/**
|
||||
* 每隔指定毫秒发送心跳检测
|
||||
* @param {number} millisecond 毫秒 默认 30000
|
||||
* @return {void}
|
||||
*/
|
||||
checkHeartbeat(millisecond: number = 30000): void {
|
||||
if (!this.socket) return;
|
||||
if (this.heartbeat?.timeout) clearTimeout(this.heartbeat.timeout);
|
||||
const timeout = setTimeout(() => {
|
||||
const now = new Date();
|
||||
|
||||
if (
|
||||
!this.isClosed &&
|
||||
(!this.heartbeat ||
|
||||
now.getTime() - this.heartbeat.datetime.getTime() >=
|
||||
millisecond)
|
||||
) {
|
||||
this.sendMessage('heartbeat', { time: now.getTime() });
|
||||
this.heartbeat = {
|
||||
timeout,
|
||||
datetime: now,
|
||||
};
|
||||
} else if (this.heartbeat) {
|
||||
this.heartbeat.timeout = timeout;
|
||||
}
|
||||
this.checkHeartbeat(millisecond);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到协作文档
|
||||
* @param url 协同服务地址
|
||||
* @param docID 文档唯一ID
|
||||
* @param defautlValue 如果协作服务端没有创建的文档,将作为协同文档的初始值
|
||||
* @param collectionName 协作服务名称,与协同服务端相对应
|
||||
*/
|
||||
connect(
|
||||
url: string,
|
||||
docID: string,
|
||||
defautlValue?: string,
|
||||
collectionName: string = 'yanmao',
|
||||
) {
|
||||
if (this.socket) this.socket.close();
|
||||
// 实例化一个可以自动重连的 ws
|
||||
const socket = new ReconnectingWebSocket(
|
||||
async () => {
|
||||
const token = await new Promise<string>((resolve) => {
|
||||
// 这里可以异步获取一个Token,如果有的话
|
||||
resolve('');
|
||||
});
|
||||
// 组合ws链接
|
||||
const uri = new URL(url);
|
||||
uri.searchParams.set('id', docID);
|
||||
uri.searchParams.set('token', token);
|
||||
return uri.toString();
|
||||
},
|
||||
[],
|
||||
{
|
||||
maxReconnectionDelay: 30000,
|
||||
minReconnectionDelay: 10000,
|
||||
reconnectionDelayGrowFactor: 10000,
|
||||
maxRetries: 10,
|
||||
},
|
||||
);
|
||||
|
||||
// ws 已链接
|
||||
socket.addEventListener('open', () => {
|
||||
this.socket = socket as WebSocket;
|
||||
// 加载编辑器内部的协同服务
|
||||
this.load(socket, docID, collectionName, defautlValue);
|
||||
// 标记关闭状态为false
|
||||
this.isClosed = false;
|
||||
// 监听协同服务端自定义消息
|
||||
this.socket.addEventListener('message', (event) => {
|
||||
const { data, action } = JSON.parse(event.data);
|
||||
// 当前所有的协作用户
|
||||
if ('members' === action) {
|
||||
this.addMembers(data);
|
||||
this.engine.ot.setMembers(data);
|
||||
return;
|
||||
}
|
||||
// 有新的协作者加入了
|
||||
if ('join' === action) {
|
||||
this.addMembers([data]);
|
||||
this.engine.ot.addMember(data);
|
||||
return;
|
||||
}
|
||||
// 有协作者离开了
|
||||
if ('leave' === action) {
|
||||
this.engine.ot.removeMember(data);
|
||||
this.removeMember(data);
|
||||
return;
|
||||
}
|
||||
// 协作服务端准备好了,可以实例化编辑器内部的协同服务了
|
||||
if ('ready' === action) {
|
||||
// 当前协作者用户
|
||||
this.current = data as Member;
|
||||
this.engine.ot.setCurrentMember(data);
|
||||
this.emit('ready', this.engine.ot.getCurrentMember());
|
||||
this.emit(EVENT.membersChange, this.normalizeMembers());
|
||||
this.transmit(STATUS.active);
|
||||
}
|
||||
// 广播信息,一个协作用户发送给全部协作者的广播
|
||||
if ('broadcast' === action) {
|
||||
const { uuid, body, type } = data;
|
||||
// 如果接收者和发送者不是同一人就触发一个message事件,外部可以监听这个事件并作出响应
|
||||
if (uuid !== this.current?.uuid) {
|
||||
this.emit(EVENT.message, {
|
||||
type,
|
||||
body,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
// 开始检测心跳
|
||||
this.checkHeartbeat();
|
||||
});
|
||||
// 监听ws关闭事件
|
||||
socket.addEventListener('close', () => {
|
||||
// 如果不是主动退出的关闭,就显示错误信息
|
||||
if (this.status !== STATUS.exit) {
|
||||
this.onError({
|
||||
code: ERROR_CODE.DISCONNECTED,
|
||||
level: ERROR_LEVEL.FATAL,
|
||||
message:
|
||||
'网络连接异常,无法继续编辑!正在为您重新连接中...',
|
||||
});
|
||||
}
|
||||
});
|
||||
// 监听ws错误消息
|
||||
socket.addEventListener('error', (error) => {
|
||||
this.onError({
|
||||
code: ERROR_CODE.CONNECTION_ERROR,
|
||||
level: ERROR_LEVEL.FATAL,
|
||||
message: '协作服务异常,无法继续编辑!正在为您重新连接中...',
|
||||
error,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载编辑器内部协同服务
|
||||
* @param docId 文档唯一ID
|
||||
* @param collectionName 协作服务名称
|
||||
* @param defaultValue 如果服务端没有对应docId的文档,就用这个值初始化
|
||||
*/
|
||||
load(
|
||||
socket: ReconnectingWebSocket,
|
||||
docId: string,
|
||||
collectionName: string,
|
||||
defaultValue?: string,
|
||||
) {
|
||||
// 实例化一个协同客户端的连接
|
||||
const connection = new sharedb.Connection(socket as Socket);
|
||||
// 获取文档对象
|
||||
const doc = connection.get(collectionName, docId);
|
||||
this.doc = doc;
|
||||
// 订阅
|
||||
doc.subscribe((error) => {
|
||||
console.log('subscribe');
|
||||
if (error) {
|
||||
console.log('collab doc subscribe error', error);
|
||||
} else {
|
||||
try {
|
||||
// 实例化编辑器内部协同服务
|
||||
this.engine.ot.initRemote(doc as any, defaultValue);
|
||||
// 聚焦到编辑器
|
||||
this.engine.focus();
|
||||
} catch (err) {
|
||||
console.log('am-engine init failed:', err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
doc.on('create', () => {
|
||||
console.log('collab doc create');
|
||||
});
|
||||
|
||||
doc.on('load', () => {
|
||||
console.log('collab doc loaded');
|
||||
this.sendMessage('ready');
|
||||
});
|
||||
|
||||
doc.on('op', (op, type) => {
|
||||
console.log('op', op, type ? 'local' : 'server');
|
||||
});
|
||||
|
||||
doc.on('del', (t, n) => {
|
||||
console.log('collab doc deleted', t, n);
|
||||
});
|
||||
|
||||
doc.on('error', (error) => {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播一个消息
|
||||
* @param type 消息类型
|
||||
* @param body 消息内容
|
||||
*/
|
||||
broadcast(type: string, body: any = {}) {
|
||||
this.sendMessage('broadcast', { type, body });
|
||||
}
|
||||
|
||||
/**
|
||||
* 给服务端发送一个消息
|
||||
* @param action 消息类型
|
||||
* @param data 消息数据
|
||||
*/
|
||||
sendMessage(action: string, data?: any) {
|
||||
this.socket?.send(
|
||||
JSON.stringify({
|
||||
action,
|
||||
data: {
|
||||
...data,
|
||||
doc_id: this.doc?.id,
|
||||
uuid: this.current?.uuid,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
addMembers(memberList: Array<Member>) {
|
||||
memberList.forEach((member) => {
|
||||
if (!this.members.find((m) => member.uuid === m.uuid)) {
|
||||
this.members.push(member);
|
||||
}
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.emit(EVENT.membersChange, this.normalizeMembers());
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
removeMember(member: Member) {
|
||||
this.members = this.members.filter((user) => {
|
||||
return user.uuid !== member.uuid;
|
||||
});
|
||||
this.emit(EVENT.membersChange, this.normalizeMembers());
|
||||
}
|
||||
|
||||
normalizeMembers() {
|
||||
const members = [];
|
||||
const colorMap: any = {};
|
||||
const users = this.engine.ot.getMembers();
|
||||
users.forEach((user) => {
|
||||
colorMap[user.uuid] = user.color;
|
||||
});
|
||||
const memberMap: any = {};
|
||||
for (let i = this.members.length; i > 0; i--) {
|
||||
const member = this.members[i - 1];
|
||||
if (!memberMap[member.uuid]) {
|
||||
const cloneMember = { ...member };
|
||||
cloneMember.color = colorMap[member.uuid];
|
||||
memberMap[member.uuid] = member;
|
||||
members.push(cloneMember);
|
||||
}
|
||||
}
|
||||
return members;
|
||||
}
|
||||
|
||||
transmit(status: string) {
|
||||
const prevStatus = this.status;
|
||||
this.status = status;
|
||||
this.emit(EVENT.statusChange, {
|
||||
form: prevStatus,
|
||||
to: status,
|
||||
});
|
||||
}
|
||||
|
||||
onError(error: ERROR) {
|
||||
this.emit(EVENT.error, error);
|
||||
this.status = STATUS.error;
|
||||
}
|
||||
|
||||
isActive() {
|
||||
return this.status === STATUS.active;
|
||||
}
|
||||
|
||||
exit() {
|
||||
if (this.status !== STATUS.exit) {
|
||||
this.transmit(STATUS.exit);
|
||||
this.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.socket) {
|
||||
try {
|
||||
this.socket.close(
|
||||
ERROR_CODE.STATUS_CODE.FORCE_DISCONNECTED,
|
||||
'FORCE_DISCONNECTED',
|
||||
);
|
||||
if (this.heartbeat?.timeout) {
|
||||
clearTimeout(this.heartbeat!.timeout);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
window.addEventListener('beforeunload', () => this.exit());
|
||||
window.addEventListener('visibilitychange', () => {
|
||||
if ('hidden' === document.visibilityState) {
|
||||
this.emit(EVENT.inactive);
|
||||
}
|
||||
});
|
||||
window.addEventListener('pagehide', () => this.exit());
|
||||
}
|
||||
|
||||
unbindEvents() {
|
||||
window.removeEventListener('beforeunload', () => this.exit());
|
||||
window.removeEventListener('visibilitychange', () => {
|
||||
if ('hidden' === document.visibilityState) {
|
||||
this.emit(EVENT.inactive);
|
||||
}
|
||||
});
|
||||
window.removeEventListener('pagehide', () => this.exit());
|
||||
}
|
||||
}
|
||||
|
||||
export default OTClient;
|
|
@ -0,0 +1,3 @@
|
|||
// these APIs are auto-imported from @vueuse/core
|
||||
export const isDark = useDark()
|
||||
export const toggleDark = useToggle(isDark)
|
|
@ -0,0 +1 @@
|
|||
export * from './dark'
|
|
@ -0,0 +1,20 @@
|
|||
<script setup lang="ts">
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="px-4 py-10 text-center text-teal-700 dark:text-gray-200">
|
||||
<div>
|
||||
<p class="text-4xl">
|
||||
<carbon-warning class="inline-block" />
|
||||
</p>
|
||||
</div>
|
||||
<router-view />
|
||||
<div>
|
||||
<button class="btn m-3 text-sm mt-8" @click="router.back()">
|
||||
{{ t('button.back') }}
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
|
@ -0,0 +1,14 @@
|
|||
## Layouts
|
||||
|
||||
Vue components in this dir are used as layouts.
|
||||
|
||||
By default, `default.vue` will be used unless an alternative is specified in the route
|
||||
meta.
|
||||
|
||||
With [`vite-plugin-pages`](https://github.com/hannoeru/vite-plugin-pages) and
|
||||
[`vite-plugin-vue-layouts`](https://github.com/JohnCampionJr/vite-plugin-vue-layouts), you
|
||||
can specify the layout in the page's SFCs like this:
|
||||
|
||||
```html
|
||||
<route lang="yaml"> meta: layout: home </route>
|
||||
```
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<main class="text-gray-700 dark:text-gray-200">
|
||||
<router-view />
|
||||
</main>
|
||||
</template>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<main class="px-4 py-10 text-center text-gray-700 dark:text-gray-200">
|
||||
<router-view />
|
||||
<Footer />
|
||||
<div class="mt-5 mx-auto text-center opacity-25 text-sm">[Home Layout]</div>
|
||||
</main>
|
||||
</template>
|
|
@ -1,5 +1,68 @@
|
|||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
// windicss layers
|
||||
import 'virtual:windi-base.css'
|
||||
import 'virtual:windi-components.css'
|
||||
// your custom styles here
|
||||
import './styles/main.css'
|
||||
// windicss utilities should be the last style import
|
||||
import 'virtual:windi-utilities.css'
|
||||
// windicss devtools support (dev only)
|
||||
import 'virtual:windi-devtools'
|
||||
|
||||
createApp(App).use(router).mount('#app');
|
||||
import App from './App.vue'
|
||||
// register vue composition api globally
|
||||
import { ViteSSG } from 'vite-ssg'
|
||||
import generatedRoutes from 'virtual:generated-pages'
|
||||
import microApp from '@micro-zoe/micro-app'
|
||||
import { setupLayouts } from 'virtual:generated-layouts'
|
||||
|
||||
const routes = setupLayouts(generatedRoutes)
|
||||
// @ts-ignore
|
||||
microApp.start({
|
||||
lifeCycles: {
|
||||
created() {
|
||||
console.log('created 全局监听')
|
||||
},
|
||||
beforemount() {
|
||||
console.log('beforemount 全局监听')
|
||||
},
|
||||
mounted() {
|
||||
console.log('mounted 全局监听')
|
||||
},
|
||||
unmount() {
|
||||
console.log('unmount 全局监听')
|
||||
},
|
||||
error() {
|
||||
console.log('error 全局监听')
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
modules: {
|
||||
react: [
|
||||
{
|
||||
loader(code: string, url: string) {
|
||||
if (
|
||||
process.env.NODE_ENV === 'development' &&
|
||||
code.indexOf('sockjs-node') > -1
|
||||
) {
|
||||
console.log('react17插件', url)
|
||||
code = code.replace('window.location.port', '3002')
|
||||
}
|
||||
return code
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// https://github.com/antfu/vite-ssg
|
||||
export const createApp = ViteSSG(
|
||||
App,
|
||||
{ routes, base: import.meta.env.BASE_URL },
|
||||
(ctx) => {
|
||||
// install all modules under `modules/`
|
||||
Object.values(import.meta.globEager('./modules/*.ts')).forEach((i) =>
|
||||
i.install?.(ctx)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
## Modules
|
||||
|
||||
A custom user module system. Place a `.ts` file with the following template, it will be
|
||||
installed automatically.
|
||||
|
||||
```ts
|
||||
import { UserModule } from '~/types'
|
||||
|
||||
export const install: UserModule = ({ app, router, isClient }) => {
|
||||
// do something
|
||||
}
|
||||
```
|
|
@ -0,0 +1,23 @@
|
|||
import { createI18n } from 'vue-i18n'
|
||||
import type { UserModule } from '~/types'
|
||||
|
||||
// Import i18n resources
|
||||
// https://vitejs.dev/guide/features.html#glob-import
|
||||
//
|
||||
// Don't need this? Try vitesse-lite: https://github.com/antfu/vitesse-lite
|
||||
const messages = Object.fromEntries(
|
||||
Object.entries(import.meta.globEager('../../locales/*.y(a)?ml')).map(([key, value]) => {
|
||||
const yaml = key.endsWith('.yaml')
|
||||
return [key.slice(14, yaml ? -5 : -4), value.default]
|
||||
})
|
||||
)
|
||||
|
||||
export const install: UserModule = ({ app }) => {
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages,
|
||||
})
|
||||
|
||||
app.use(i18n)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { App } from 'vue'
|
||||
import type { UserModule } from '~/types'
|
||||
|
||||
// inject into Vue
|
||||
export const injectToVue = (app: App, name: string, target: unknown) => {
|
||||
app.config.globalProperties[`$${name}`] = target
|
||||
}
|
||||
|
||||
export const install: UserModule = ({ app }) => {
|
||||
const inject = (name: string, target: unknown) => {
|
||||
injectToVue(app, name, target)
|
||||
}
|
||||
// demo
|
||||
inject('name', 'Modern Vue Stack')
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import NProgress from 'nprogress'
|
||||
import type { UserModule } from '~/types'
|
||||
|
||||
export const install: UserModule = ({ isClient, router }) => {
|
||||
if (isClient) {
|
||||
router.beforeEach(() => {
|
||||
NProgress.start()
|
||||
})
|
||||
router.afterEach(() => {
|
||||
NProgress.done()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { createPinia } from 'pinia'
|
||||
import type { UserModule } from '~/types'
|
||||
|
||||
// Setup Pinia
|
||||
// https://pinia.esm.dev/
|
||||
export const install: UserModule = ({ isClient, initialState, app }) => {
|
||||
const pinia = createPinia()
|
||||
app.use(pinia)
|
||||
// Refer to
|
||||
// https://github.com/antfu/vite-ssg/blob/main/README.md#state-serialization
|
||||
// for other serialization strategies.
|
||||
if (isClient) pinia.state.value = initialState.pinia || {}
|
||||
else initialState.pinia = pinia.state.value
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import type { UserModule } from '~/types'
|
||||
|
||||
// https://github.com/antfu/vite-plugin-pwa#automatic-reload-when-new-content-available
|
||||
export const install: UserModule = ({ isClient, router }) => {
|
||||
if (!isClient) return
|
||||
|
||||
router.isReady().then(async () => {
|
||||
const { registerSW } = await import('virtual:pwa-register')
|
||||
registerSW({ immediate: true })
|
||||
})
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
## File-based Routing
|
||||
|
||||
Routes will be auto-generated for Vue files in this dir with the same file structure.
|
||||
Check out [`vite-plugin-pages`](https://github.com/hannoeru/vite-plugin-pages) for more
|
||||
details.
|
||||
|
||||
### Path Aliasing
|
||||
|
||||
`~/` is aliased to `./src/` folder.
|
||||
|
||||
For example, instead of having
|
||||
|
||||
```ts
|
||||
import { isDark } from '../../../../composables'
|
||||
```
|
||||
|
||||
now, you can use
|
||||
|
||||
```ts
|
||||
import { isDark } from '~/composables'
|
||||
```
|
|
@ -0,0 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
{{ t('not-found') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: 404
|
||||
</route>
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
title: About
|
||||
---
|
||||
|
||||
<div class="text-center">
|
||||
<!-- You can use Vue components inside markdown -->
|
||||
<carbon-dicom-overlay class="text-4xl -mb-6 m-auto" />
|
||||
<h3>About</h3>
|
||||
</div>
|
||||
|
||||
[Vitesse](https://github.com/antfu/vitesse) is an opinionated
|
||||
[Vite](https://github.com/vitejs/vite) starter template made by
|
||||
[@antfu](https://github.com/antfu) for mocking apps swiftly. With **file-based routing**,
|
||||
**components auto importing**, **markdown support**, I18n, PWA and uses **WindiCSS** for
|
||||
UI.
|
||||
|
||||
```js
|
||||
// syntax highlighting example
|
||||
function vitesse() {
|
||||
const foo = 'bar'
|
||||
console.log(foo)
|
||||
}
|
||||
```
|
||||
|
||||
Check out the [GitHub repo](https://github.com/antfu/vitesse) for more details.
|
|
@ -0,0 +1,49 @@
|
|||
<script setup lang="ts">
|
||||
import { useUserStore } from '~/stores/user'
|
||||
|
||||
const props = defineProps<{ name: string }>()
|
||||
const router = useRouter()
|
||||
const user = useUserStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
watchEffect(() => {
|
||||
user.setNewName(props.name)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p class="text-4xl">
|
||||
<carbon-pedestrian class="inline-block" />
|
||||
</p>
|
||||
<p>
|
||||
{{ t('intro.hi', { name: props.name }) }}
|
||||
</p>
|
||||
|
||||
<p class="text-sm opacity-50">
|
||||
<em>{{ t('intro.dynamic-route') }}</em>
|
||||
</p>
|
||||
|
||||
<template v-if="user.otherNames.length">
|
||||
<p class="text-sm mt-4">
|
||||
<span class="opacity-75">{{ t('intro.aka') }}:</span>
|
||||
<ul>
|
||||
<li v-for="otherName in user.otherNames" :key="otherName">
|
||||
<router-link :to="`/hi/${otherName}`" replace>
|
||||
{{ otherName }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<button
|
||||
class="btn m-3 text-sm mt-6"
|
||||
@click="router.back()"
|
||||
>
|
||||
{{ t('button.back') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,7 @@
|
|||
<script setup lang="ts" name="IndexPage">
|
||||
import AmEditor from '~/components/AmEditor/'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AmEditor />
|
||||
</template>
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<div class="red">
|
||||
<micro-app name="red" url="http://localhost:3002/" :data="data" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const data = ref({ from: '来自基座的初始化数据' })
|
||||
</script>
|
|
@ -1,26 +0,0 @@
|
|||
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
|
||||
import Home from '../views/Home.vue';
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Home,
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "about" */ '../views/About.vue'),
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(process.env.BASE_URL),
|
||||
routes,
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -1,6 +0,0 @@
|
|||
/* eslint-disable */
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent } from 'vue';
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
declare interface Window {
|
||||
// extend the window
|
||||
}
|
||||
|
||||
// with vite-plugin-md, markdowns can be treat as Vue components
|
||||
declare module '*.md' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import { acceptHMRUpdate, defineStore } from 'pinia'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
/**
|
||||
* Current named of the user.
|
||||
*/
|
||||
const savedName = ref('')
|
||||
const previousNames = ref(new Set<string>())
|
||||
|
||||
const usedNames = computed(() => Array.from(previousNames.value))
|
||||
const otherNames = computed(() => usedNames.value.filter(name => name !== savedName.value))
|
||||
|
||||
/**
|
||||
* Changes the current name of the user and saves the one that was used
|
||||
* before.
|
||||
*
|
||||
* @param name - new name to set
|
||||
*/
|
||||
function setNewName(name: string) {
|
||||
if (savedName.value)
|
||||
previousNames.value.add(savedName.value)
|
||||
|
||||
savedName.value = name
|
||||
}
|
||||
|
||||
return {
|
||||
setNewName,
|
||||
otherNames,
|
||||
savedName,
|
||||
}
|
||||
})
|
||||
|
||||
if (import.meta.hot)
|
||||
import.meta.hot.accept(acceptHMRUpdate(useUserStore, import.meta.hot))
|
|
@ -0,0 +1,43 @@
|
|||
@import './markdown.css';
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
background: #121212;
|
||||
}
|
||||
|
||||
#nprogress {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#nprogress .bar {
|
||||
@apply bg-teal-600 opacity-75;
|
||||
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply px-4 py-1 rounded inline-block
|
||||
bg-teal-600 text-white cursor-pointer
|
||||
hover:bg-teal-700
|
||||
disabled:cursor-default disabled:bg-gray-600 disabled:opacity-50;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
@apply inline-block cursor-pointer select-none
|
||||
opacity-75 transition duration-200 ease-in-out
|
||||
hover:opacity-100 hover:text-teal-600;
|
||||
font-size: 0.9em;
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/* https://github.com/antfu/prism-theme-vars */
|
||||
@import 'prism-theme-vars/base.css';
|
||||
|
||||
.prose {
|
||||
--prism-font-family: 'Input Mono', monospace;
|
||||
}
|
||||
|
||||
.prose img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
html:not(.dark) .prose {
|
||||
--prism-foreground: #393a34;
|
||||
--prism-background: #fbfbfb;
|
||||
--prism-comment: #a0ada0;
|
||||
--prism-string: #b56959;
|
||||
--prism-literal: #2f8a89;
|
||||
--prism-number: #296aa3;
|
||||
--prism-keyword: #1c6b48;
|
||||
--prism-function: #6c7834;
|
||||
--prism-boolean: #1c6b48;
|
||||
--prism-constant: #a65e2b;
|
||||
--prism-deleted: #a14f55;
|
||||
--prism-class: #2993a3;
|
||||
--prism-builtin: #ab5959;
|
||||
--prism-property: #b58451;
|
||||
--prism-namespace: #b05a78;
|
||||
--prism-punctuation: #8e8f8b;
|
||||
--prism-decorator: #bd8f8f;
|
||||
--prism-regex: #ab5e3f;
|
||||
--prism-json-property: #698c96;
|
||||
}
|
||||
|
||||
html.dark .prose {
|
||||
--prism-foreground: #d4cfbf;
|
||||
--prism-background: #151515;
|
||||
--prism-comment: #758575;
|
||||
--prism-string: #d48372;
|
||||
--prism-literal: #429988;
|
||||
--prism-keyword: #4d9375;
|
||||
--prism-boolean: #1c6b48;
|
||||
--prism-number: #6394bf;
|
||||
--prism-variable: #c2b36e;
|
||||
--prism-function: #a1b567;
|
||||
--prism-deleted: #a14f55;
|
||||
--prism-class: #54b1bf;
|
||||
--prism-builtin: #e0a569;
|
||||
--prism-property: #dd8e6e;
|
||||
--prism-namespace: #db889a;
|
||||
--prism-punctuation: #858585;
|
||||
--prism-decorator: #bd8f8f;
|
||||
--prism-regex: #ab5e3f;
|
||||
--prism-json-property: #6b8b9e;
|
||||
--prism-line-number: #888888;
|
||||
--prism-line-number-gutter: #eeeeee;
|
||||
--prism-line-highlight-background: #444444;
|
||||
--prism-selection-background: #444444;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import type { ViteSSGContext } from 'vite-ssg'
|
||||
|
||||
export type UserModule = (ctx: ViteSSGContext) => void
|
|
@ -1,5 +0,0 @@
|
|||
<template>
|
||||
<div class="about">
|
||||
<h1>This is an about page</h1>
|
||||
</div>
|
||||
</template>
|
|
@ -1,19 +0,0 @@
|
|||
<template>
|
||||
<div class="home">
|
||||
<engine-demo />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { Select } from 'ant-design-vue'
|
||||
import EngineDemo from '@/components/demo.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
"a-select":Select,
|
||||
"a-select-option":Select.Option,
|
||||
EngineDemo
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,3 @@
|
|||
// Vitest Snapshot v1
|
||||
|
||||
exports[`Counter.vue > should render 1`] = `"<div>10 <button class=\\"inc\\"> + </button><button class=\\"dec\\"> - </button></div>"`;
|
|
@ -0,0 +1,5 @@
|
|||
describe('tests', () => {
|
||||
it('should works', () => {
|
||||
expect(1 + 1).toEqual(2)
|
||||
})
|
||||
})
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue