Compare commits
34 Commits
d70cd4688c
...
2a27f67b85
| Author | SHA1 | Date |
|---|---|---|
|
|
2a27f67b85 | |
|
|
53ddf5fb20 | |
|
|
4aef16583d | |
|
|
7f9cbe8466 | |
|
|
95021c503e | |
|
|
8120ba3cd7 | |
|
|
652703f371 | |
|
|
877389828a | |
|
|
66dd00b110 | |
|
|
8f6b0159d7 | |
|
|
2bc0a5a7bb | |
|
|
b9c179a95b | |
|
|
40bb7edbd0 | |
|
|
3dd78b1b24 | |
|
|
f4f5529f4c | |
|
|
f768a8eead | |
|
|
e87b3b2cf3 | |
|
|
9e7a635e1b | |
|
|
85dfe6c43f | |
|
|
63746a8f5e | |
|
|
48ef4bf597 | |
|
|
6cbd596f9f | |
|
|
61f5bc6146 | |
|
|
5ffb8737d3 | |
|
|
511982621f | |
|
|
ae304c112b | |
|
|
6d3accc520 | |
|
|
0281782612 | |
|
|
e559091a3d | |
|
|
b3f9c11f26 | |
|
|
13cabad76a | |
|
|
d8230ad3b9 | |
|
|
46c6c9a3a7 | |
|
|
cdf9e3643a |
|
|
@ -0,0 +1,4 @@
|
|||
# 参见 .env
|
||||
|
||||
VITE_BASE = ./
|
||||
VITE_HISTORY = hash
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -28,4 +28,28 @@ jobs:
|
|||
git init
|
||||
git add -A
|
||||
git commit -m "Build through github action"
|
||||
git push -f "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" master:gh-pages
|
||||
git push -f "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" master:gh-pages
|
||||
|
||||
send:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 克隆代码
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: 构建消息
|
||||
id: exec_cmd
|
||||
run: |
|
||||
MSG=$(git log --format=%B -n 1 ${{ github.sha }})
|
||||
echo "msg=$MSG" >> "$GITHUB_OUTPUT"
|
||||
HASH=$(git log -1 --pretty=format:%h)
|
||||
echo "hash1=$HASH" >> "$GITHUB_ENV"
|
||||
shell: bash
|
||||
|
||||
- name: 推送到企微
|
||||
uses: appnify/action-wechat-work@master
|
||||
env:
|
||||
WECHAT_WORK_BOT_WEBHOOK: ${{secrets.WECHAT_WORK_BOT_WEBHOOK}}
|
||||
with:
|
||||
msgtype: text
|
||||
content: "推送通知\n\n仓库名称:${{ github.repository }}\n提交用户:${{ github.actor }}\n提交消息:${{ steps.exec_cmd.outputs.msg }}\n提交哈希:${{ env.hash1 }}\n\n提醒:已有提交推送到仓库,请留意构建结果。"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"tabWidth": 2,
|
||||
"printWidth": 120,
|
||||
"printWidth": 180,
|
||||
"bracketSpacing": true,
|
||||
"singleQuote": true,
|
||||
"arrowParens": "avoid"
|
||||
|
|
|
|||
|
|
@ -18,5 +18,7 @@ COPY --from=builder /app/dist /usr/share/nginx/html
|
|||
# 复制nginx配置
|
||||
COPY --from=builder /app/.github/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# 显式暴露端口
|
||||
EXPOSE 80
|
||||
# 启动,关闭后台运行启动前台运行,不然 docker 会结束运行
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
144
README.md
144
README.md
|
|
@ -15,6 +15,7 @@
|
|||
- 遵循 Conventional Changelog 规范, 自动生成版本记录文档
|
||||
- 内置常用 VsCode 代码片段和推荐扩展,提升开发效率
|
||||
- 支持路由动态打包、路由权限、路由缓存和动态首页
|
||||
- 支持 Docker 部署,包含优化过的 Dockerfile 配置
|
||||
|
||||
## 快速开始
|
||||
|
||||
|
|
@ -53,20 +54,153 @@ pnpm dev
|
|||
|
||||
根据 src/pages 目录生成路由数组,包含以下以下规则:
|
||||
|
||||
- 以文件夹为路由,读取该文件夹下 index.vue 的信息作为路由信息,其他文件会跳过,可以包含子文件夹
|
||||
- 以文件夹为路由,读取该文件夹下 index.vue 的信息作为路由信息,其他文件会跳过,可以包含子文件夹作为嵌套路由
|
||||
- 在 src/pages 的文件夹层级,作为菜单层级,路由层级最终只有 2 层(配合 keep-alive 缓存)
|
||||
- 在 src/pages 目录下,以 _ 开头的文件夹作为 1 级路由,其他作为 2 级路由,也就是应用路由
|
||||
- 子文件夹下,只有 index.vue 文件有效,其他文件会忽略,这些文件可以作为子组件使用
|
||||
- components 目录会被忽视
|
||||
- xxx.xx.xx 文件会被忽视,例如 index.my.vue
|
||||
- components 目录会被忽视。
|
||||
- xxx.xx.xx 文件会被忽视,例如 index.my.vue 文件。
|
||||
|
||||
对应目录下的 index.vue 文件中定义如下路由配置:
|
||||
|
||||
```jsonc
|
||||
<route lang="json">
|
||||
{
|
||||
"parentMeta": {
|
||||
// 具体属性查阅 src/types/vue-router.d.ts
|
||||
// 其他 Route 参数
|
||||
"meta": {
|
||||
// 请看下面
|
||||
}
|
||||
}
|
||||
</route>
|
||||
```
|
||||
|
||||
目前支持的参数,如下:
|
||||
|
||||
```ts
|
||||
interface RouteMeta {
|
||||
/**
|
||||
* 页面标题
|
||||
* @description
|
||||
* 菜单和导航面包屑等地方会用到
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* 页面图标
|
||||
* @description
|
||||
* 使用 icon-park-outline 图标集的图标类名
|
||||
*/
|
||||
icon?: string;
|
||||
/**
|
||||
* 显示顺序
|
||||
* @description
|
||||
* 在菜单中的显示顺序,越小越靠前
|
||||
*/
|
||||
sort?: number;
|
||||
/**
|
||||
* 是否隐藏
|
||||
* @description
|
||||
* - false // 不隐藏(默认)
|
||||
* - true // 在路由和菜单中隐藏,即忽略且不打包
|
||||
* - 'menu' // 在菜单中隐藏,通过其他方式访问
|
||||
* - 'prod' // 在生产环境下隐藏
|
||||
*/
|
||||
hide?: boolean | 'menu' | 'prod';
|
||||
/**
|
||||
* 所需权限
|
||||
* @example
|
||||
* ```js
|
||||
* ['system:user']
|
||||
* ```
|
||||
*/
|
||||
auth?: string[];
|
||||
/**
|
||||
* 是否缓存
|
||||
* @description
|
||||
* 是否使用 keep-alive 缓存
|
||||
*/
|
||||
cache?: boolean;
|
||||
/**
|
||||
* 组件名字
|
||||
* @description
|
||||
* 组件名字,当 cache为true 时必须
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* 是否显示loading
|
||||
* @description
|
||||
* 可以自定义 loading 文本
|
||||
*/
|
||||
loading?: boolean | string;
|
||||
/**
|
||||
* 链接
|
||||
* @description
|
||||
* ```js
|
||||
* 'https://juetan.cn'
|
||||
* ```
|
||||
*/
|
||||
link?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 嵌套布局
|
||||
|
||||
默认情况下,嵌套路由会使用父级 index.vue 作为布局文件,如果不需要布局,只需在父级路由指定 component 为 null 即可,如下:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"component": null,
|
||||
"meta": {}
|
||||
}
|
||||
```
|
||||
|
||||
这样,其层级仅作为菜单层级,在路由上表现为扁平。
|
||||
|
||||
### 路由权限
|
||||
|
||||
在每个路由的 index.vue 文件中,通过 meta.auth 字段指定访问该路由所需的权限,示例如下:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"meta": {
|
||||
"auth": ["system:user", "system:menu"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
默认全部需要登陆才可访问,其中有 2 个比较特殊的权限:
|
||||
|
||||
- `*` 表示无需登陆即可访问,适合挂一些比较通用的页面。
|
||||
- `unlogin` 表示未登录才可以访问。例如登录页,登陆后访问该页面会被拒绝。
|
||||
|
||||
用户登陆后获取的权限,应存储在 userStore.auth 字段中,在路由的 beforeEach 守卫中,会比较两个是否匹配,匹配上则继续,否则会显示如下 403 页面:
|
||||
|
||||

|
||||
|
||||
### 动态路由
|
||||
|
||||
相比于比较流行的加法挂载,我更倾向于减法挂载。即默认加载完所有路由,在 beforeEach 钩子根据权限移除不必要的路由。
|
||||
|
||||
### 动态首页
|
||||
|
||||
在作为首页路由的 index.vue 文件中,指定 alias 为 '/' 即可,默认是 home/index.vue 文件。如需动态更新首页,在 beforeEach 获取完菜单信息,通过 removeRoute 移除旧的首页路由,通过 addRoute 添加新的首页路由即可。
|
||||
|
||||
### 路由缓存
|
||||
|
||||
在路由的 index.vue 文件,首先指定好组件的名字,再通过 cache 字段开启缓存,示例如下:
|
||||
|
||||
```html
|
||||
<script>
|
||||
defineOptions({
|
||||
name: "MyPage"
|
||||
})
|
||||
</script>
|
||||
<route>
|
||||
{
|
||||
"meta": {
|
||||
// 组件名字
|
||||
"name": "MyPage",
|
||||
// 开启缓存
|
||||
"cache": true
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
|
|
|||
81
index.html
81
index.html
|
|
@ -9,16 +9,13 @@
|
|||
</head>
|
||||
<body>
|
||||
<div id="app" class="dark:bg-slate-900 dark:text-slate-200">
|
||||
<div class="loading">
|
||||
<img
|
||||
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHN0eWxlPSJtYXJnaW46IGF1dG87IGJhY2tncm91bmQ6IHJnYigyNTUsIDI1NSwgMjU1KTsgZGlzcGxheTogYmxvY2s7IHNoYXBlLXJlbmRlcmluZzogYXV0bzsiIHdpZHRoPSIyMDBweCIgaGVpZ2h0PSIyMDBweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIj48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1MCA1MCkiPjxnPjxhbmltYXRlVHJhbnNmb3JtIGF0dHJpYnV0ZU5hbWU9InRyYW5zZm9ybSIgdHlwZT0icm90YXRlIiB2YWx1ZXM9IjA7NDUiIGtleVRpbWVzPSIwOzEiIGR1cj0iMC4ycyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiPjwvYW5pbWF0ZVRyYW5zZm9ybT48cGF0aCBkPSJNMjkuNDkxNTI0MjA2MTE3MjU1IC01LjUgTDM3LjQ5MTUyNDIwNjExNzI1NSAtNS41IEwzNy40OTE1MjQyMDYxMTcyNTUgNS41IEwyOS40OTE1MjQyMDYxMTcyNTUgNS41IEEzMCAzMCAwIDAgMSAyNC43NDI3NDQwNTAxOTg3MzggMTYuOTY0NTY5NDU3MTQ2NzEyIEwyNC43NDI3NDQwNTAxOTg3MzggMTYuOTY0NTY5NDU3MTQ2NzEyIEwzMC4zOTk1OTgyOTk2OTExMTcgMjIuNjIxNDIzNzA2NjM5MDkyIEwyMi42MjE0MjM3MDY2MzkwOTYgMzAuMzk5NTk4Mjk5NjkxMTE0IEwxNi45NjQ1Njk0NTcxNDY3MTYgMjQuNzQyNzQ0MDUwMTk4NzM0IEEzMCAzMCAwIDAgMSA1LjUgMjkuNDkxNTI0MjA2MTE3MjU1IEw1LjUgMjkuNDkxNTI0MjA2MTE3MjU1IEw1LjUgMzcuNDkxNTI0MjA2MTE3MjU1IEwtNS40OTk5OTk5OTk5OTk5OTcgMzcuNDkxNTI0MjA2MTE3MjU1IEwtNS40OTk5OTk5OTk5OTk5OTcgMjkuNDkxNTI0MjA2MTE3MjU1IEEzMCAzMCAwIDAgMSAtMTYuOTY0NTY5NDU3MTQ2NzA1IDI0Ljc0Mjc0NDA1MDE5ODczOCBMLTE2Ljk2NDU2OTQ1NzE0NjcwNSAyNC43NDI3NDQwNTAxOTg3MzggTC0yMi42MjE0MjM3MDY2MzkwODUgMzAuMzk5NTk4Mjk5NjkxMTE3IEwtMzAuMzk5NTk4Mjk5NjkxMTE3IDIyLjYyMTQyMzcwNjYzOTA5MiBMLTI0Ljc0Mjc0NDA1MDE5ODczOCAxNi45NjQ1Njk0NTcxNDY3MTIgQTMwIDMwIDAgMCAxIC0yOS40OTE1MjQyMDYxMTcyNTUgNS41MDAwMDAwMDAwMDAwMDkgTC0yOS40OTE1MjQyMDYxMTcyNTUgNS41MDAwMDAwMDAwMDAwMDkgTC0zNy40OTE1MjQyMDYxMTcyNTUgNS41MDAwMDAwMDAwMDAwMSBMLTM3LjQ5MTUyNDIwNjExNzI1NSAtNS41MDAwMDAwMDAwMDAwMDEgTC0yOS40OTE1MjQyMDYxMTcyNTUgLTUuNTAwMDAwMDAwMDAwMDAyIEEzMCAzMCAwIDAgMSAtMjQuNzQyNzQ0MDUwMTk4NzM4IC0xNi45NjQ1Njk0NTcxNDY3MDUgTC0yNC43NDI3NDQwNTAxOTg3MzggLTE2Ljk2NDU2OTQ1NzE0NjcwNSBMLTMwLjM5OTU5ODI5OTY5MTExNyAtMjIuNjIxNDIzNzA2NjM5MDg1IEwtMjIuNjIxNDIzNzA2NjM5MDkyIC0zMC4zOTk1OTgyOTk2OTExMTcgTC0xNi45NjQ1Njk0NTcxNDY3MTIgLTI0Ljc0Mjc0NDA1MDE5ODczOCBBMzAgMzAgMCAwIDEgLTUuNTAwMDAwMDAwMDAwMDExIC0yOS40OTE1MjQyMDYxMTcyNTUgTC01LjUwMDAwMDAwMDAwMDAxMSAtMjkuNDkxNTI0MjA2MTE3MjU1IEwtNS41MDAwMDAwMDAwMDAwMTIgLTM3LjQ5MTUyNDIwNjExNzI1NSBMNS40OTk5OTk5OTk5OTk5OTggLTM3LjQ5MTUyNDIwNjExNzI1NSBMNS41IC0yOS40OTE1MjQyMDYxMTcyNTUgQTMwIDMwIDAgMCAxIDE2Ljk2NDU2OTQ1NzE0NjcwMiAtMjQuNzQyNzQ0MDUwMTk4NzQgTDE2Ljk2NDU2OTQ1NzE0NjcwMiAtMjQuNzQyNzQ0MDUwMTk4NzQgTDIyLjYyMTQyMzcwNjYzOTA4IC0zMC4zOTk1OTgyOTk2OTExMiBMMzAuMzk5NTk4Mjk5NjkxMTE3IC0yMi42MjE0MjM3MDY2MzkxIEwyNC43NDI3NDQwNTAxOTg3MzggLTE2Ljk2NDU2OTQ1NzE0NjcxNiBBMzAgMzAgMCAwIDEgMjkuNDkxNTI0MjA2MTE3MjU1IC01LjUwMDAwMDAwMDAwMDAxMyBNMCAtMjBBMjAgMjAgMCAxIDAgMCAyMCBBMjAgMjAgMCAxIDAgMCAtMjAiIGZpbGw9IiMwOWYiPjwvcGF0aD48L2c+PC9nPjwvc3ZnPgo="
|
||||
alt="loading"
|
||||
class="loading-image"
|
||||
width="64"
|
||||
height="64"
|
||||
/>
|
||||
<h1 class="loading-title">欢迎访问%VITE_TITLE%</h1>
|
||||
<div class="loading-tip">资源加载中, 请稍等...</div>
|
||||
<div class="cube">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
<style>
|
||||
html,
|
||||
|
|
@ -29,8 +26,7 @@
|
|||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
font-family: Inter, '-apple-system', BlinkMacSystemFont, 'PingFang SC', 'Hiragino Sans GB', 'noto sans',
|
||||
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-family: Inter, '-apple-system', BlinkMacSystemFont, 'PingFang SC', 'Hiragino Sans GB', 'noto sans', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
#app {
|
||||
width: 100%;
|
||||
|
|
@ -38,28 +34,53 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
}
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@keyframes cube {
|
||||
0% {
|
||||
transform: rotate(45deg) rotateX(-25deg) rotateY(25deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(45deg) rotateX(-385deg) rotateY(25deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(45deg) rotateX(-385deg) rotateY(385deg);
|
||||
}
|
||||
}
|
||||
.loading-image {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
.cube {
|
||||
animation: cube 2s infinite ease;
|
||||
height: 40px;
|
||||
transform-style: preserve-3d;
|
||||
width: 40px;
|
||||
}
|
||||
.loading-title {
|
||||
margin: 0;
|
||||
margin-top: 20px;
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
.cube div {
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
border: 2px solid #000;
|
||||
}
|
||||
.loading-tip {
|
||||
margin-top: 12px;
|
||||
line-height: 1;
|
||||
color: #889;
|
||||
.cube div:nth-of-type(1) {
|
||||
transform: translateZ(-20px) rotateY(180deg);
|
||||
}
|
||||
.cube div:nth-of-type(2) {
|
||||
transform: rotateY(-270deg) translateX(50%);
|
||||
transform-origin: top right;
|
||||
}
|
||||
.cube div:nth-of-type(3) {
|
||||
transform: rotateY(270deg) translateX(-50%);
|
||||
transform-origin: center left;
|
||||
}
|
||||
.cube div:nth-of-type(4) {
|
||||
transform: rotateX(90deg) translateY(-50%);
|
||||
transform-origin: top center;
|
||||
}
|
||||
.cube div:nth-of-type(5) {
|
||||
transform: rotateX(-90deg) translateY(50%);
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
.cube div:nth-of-type(6) {
|
||||
transform: translateZ(20px);
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "starter-vue",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: rgb(255, 255, 255); display: block; shape-rendering: auto;" width="200px" height="200px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid"><g transform="translate(50 50)"><g><animateTransform attributeName="transform" type="rotate" values="0;45" keyTimes="0;1" dur="0.2s" repeatCount="indefinite"></animateTransform><path d="M29.491524206117255 -5.5 L37.491524206117255 -5.5 L37.491524206117255 5.5 L29.491524206117255 5.5 A30 30 0 0 1 24.742744050198738 16.964569457146712 L24.742744050198738 16.964569457146712 L30.399598299691117 22.621423706639092 L22.621423706639096 30.399598299691114 L16.964569457146716 24.742744050198734 A30 30 0 0 1 5.5 29.491524206117255 L5.5 29.491524206117255 L5.5 37.491524206117255 L-5.499999999999997 37.491524206117255 L-5.499999999999997 29.491524206117255 A30 30 0 0 1 -16.964569457146705 24.742744050198738 L-16.964569457146705 24.742744050198738 L-22.621423706639085 30.399598299691117 L-30.399598299691117 22.621423706639092 L-24.742744050198738 16.964569457146712 A30 30 0 0 1 -29.491524206117255 5.500000000000009 L-29.491524206117255 5.500000000000009 L-37.491524206117255 5.50000000000001 L-37.491524206117255 -5.500000000000001 L-29.491524206117255 -5.500000000000002 A30 30 0 0 1 -24.742744050198738 -16.964569457146705 L-24.742744050198738 -16.964569457146705 L-30.399598299691117 -22.621423706639085 L-22.621423706639092 -30.399598299691117 L-16.964569457146712 -24.742744050198738 A30 30 0 0 1 -5.500000000000011 -29.491524206117255 L-5.500000000000011 -29.491524206117255 L-5.500000000000012 -37.491524206117255 L5.499999999999998 -37.491524206117255 L5.5 -29.491524206117255 A30 30 0 0 1 16.964569457146702 -24.74274405019874 L16.964569457146702 -24.74274405019874 L22.62142370663908 -30.39959829969112 L30.399598299691117 -22.6214237066391 L24.742744050198738 -16.964569457146716 A30 30 0 0 1 29.491524206117255 -5.500000000000013 M0 -20A20 20 0 1 0 0 20 A20 20 0 1 0 0 -20" fill="#09f"></path></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
|
|
@ -4,7 +4,7 @@ const arcoLevels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
|||
export const arcoToUnoColor = (arcoColorName: string): { [id: string]: string } => {
|
||||
const colors = {};
|
||||
for (let i = 0; i < 10; i++) {
|
||||
colors[unoLevels[i]] = `rgb(var(--${arcoColorName}-${arcoLevels[i]}))`;
|
||||
colors[unoLevels[i]] = `rgba(var(--${arcoColorName}-${arcoLevels[i]}), 1)`;
|
||||
}
|
||||
return colors;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { spawn } from 'child_process';
|
||||
import { Plugin, ResolvedConfig } from 'vite';
|
||||
import { Plugin } from 'vite';
|
||||
import pkg from '../../package.json';
|
||||
|
||||
/**
|
||||
|
|
@ -46,8 +46,11 @@ const getBuildInfo = async () => {
|
|||
const version = commits ? `${latestTag}.${commits}` : `v${pkg.version}`;
|
||||
const content = `欢迎访问!版本: ${version} 标识: ${hash} 构建: ${time}`;
|
||||
const style = `"color: #09f; font-weight: 900;", "font-size: 12px; color: #09f; font-family: ''"`;
|
||||
const script = `console.log(\`%c${LOGO} \n%c${content}\n\`, ${style});\n`;
|
||||
return script;
|
||||
const vString = `var __APP_VERSION__ = '${version}';\n`;
|
||||
const hString = `var __APP_HASH__ = '${hash}';\n`;
|
||||
const dString = `var __APP_DATE__ = '${time}';\n`;
|
||||
const lString = `console.log(\`%c${LOGO} \n%c${content}\n\`, ${style});\n`;
|
||||
return vString + hString + dString + lString;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -55,16 +58,9 @@ const getBuildInfo = async () => {
|
|||
* @returns Plugin
|
||||
*/
|
||||
export default function plugin(): Plugin {
|
||||
let config: ResolvedConfig;
|
||||
|
||||
return {
|
||||
name: 'vite:info',
|
||||
enforce: 'pre',
|
||||
|
||||
configResolved(resolvedConfig) {
|
||||
config = resolvedConfig;
|
||||
},
|
||||
|
||||
async transformIndexHtml() {
|
||||
const script = await getBuildInfo();
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -18,8 +18,10 @@ const userStore = useUserStore();
|
|||
const menuStore = useMenuStore();
|
||||
|
||||
const hasAuth = computed(() => {
|
||||
if (!route.name.startsWith('_')) {
|
||||
return true;
|
||||
}
|
||||
return route.matched.every(item => {
|
||||
console.log('i', item);
|
||||
const needAuth = item.meta.auth;
|
||||
const userAuth = userStore.auth;
|
||||
if (needAuth?.includes('*')) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,28 @@
|
|||
import { Service } from './service';
|
||||
import { addToastInterceptor } from '../interceptors/toast';
|
||||
import { addAuthInterceptor } from '../interceptors/auth';
|
||||
import { addExceptionInterceptor } from '../interceptors/exception';
|
||||
import { env } from '@/config/env';
|
||||
import { App } from 'vue';
|
||||
import { Api } from '../generated/Api';
|
||||
|
||||
/**
|
||||
* 扩展生成的API类
|
||||
*/
|
||||
export class Service extends Api<unknown> {
|
||||
/**
|
||||
* 作为VUE插件进行初始化
|
||||
* @param app
|
||||
*/
|
||||
install(app: App) {
|
||||
app.config.globalProperties.$api = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登陆过期处理函数
|
||||
* @description 勿动
|
||||
*/
|
||||
expireHandler: () => void = () => {};
|
||||
}
|
||||
|
||||
/**
|
||||
* API 接口实例
|
||||
|
|
@ -22,8 +42,8 @@ addToastInterceptor(api.instance);
|
|||
* 添加异常处理拦截器
|
||||
*/
|
||||
addExceptionInterceptor(api.instance, () => api.expireHandler?.());
|
||||
|
||||
/**
|
||||
* 添加登陆令牌拦截器
|
||||
*/
|
||||
addAuthInterceptor(api.instance);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
import { Api } from '../generated/Api';
|
||||
|
||||
/**
|
||||
* 扩展生成的API类
|
||||
*/
|
||||
export class Service extends Api<unknown> {
|
||||
/**
|
||||
* 登陆过期处理函数
|
||||
* @description 勿动
|
||||
*/
|
||||
expireHandler: () => void = () => {};
|
||||
}
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
import { store, useUserStore } from "@/store";
|
||||
import { AxiosInstance } from "axios";
|
||||
import { store } from '@/store';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { AxiosInstance } from 'axios';
|
||||
|
||||
/**
|
||||
* 登陆令牌拦截器
|
||||
* @param axios Axios实例
|
||||
*/
|
||||
export function addAuthInterceptor(axios: AxiosInstance) {
|
||||
axios.interceptors.request.use((config) => {
|
||||
axios.interceptors.request.use(config => {
|
||||
const userStore = useUserStore(store);
|
||||
if (userStore.accessToken) {
|
||||
config.headers.Authorization = `Bearer ${userStore.accessToken}`;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,10 @@
|
|||
<template>
|
||||
<div class="h-full overflow-hidden grid grid-rows-[auto_1fr]">
|
||||
<div class="bg-white px-4 py-2 border-b border-gray-200">
|
||||
<div class="bg-white px-4 py-2">
|
||||
<div class="flex justify-between gap-4">
|
||||
<BreadCrumb></BreadCrumb>
|
||||
<div>
|
||||
<a-link>需要帮助?</a-link>
|
||||
<a-button size="mini" @click="router.push({ path: route.path, query: { s: Math.random() }, force: true })">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-refresh"></i>
|
||||
</template>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { InjectionKey } from 'vue';
|
||||
import { Block, Blocker, Container } from '../core';
|
||||
import { useTextBlock } from './text';
|
||||
|
||||
const blockers: Record<string, Blocker> = import.meta.glob(['./*/index.ts', '!./font/*'], {
|
||||
eager: true,
|
||||
|
|
@ -25,47 +24,3 @@ const getIcon = (type: string) => {
|
|||
};
|
||||
|
||||
export { BlockerMap, getBlockerRender, getIcon, getTypeName };
|
||||
|
||||
export const BlockerManagerKey = Symbol('k') as InjectionKey<ReturnType<typeof useBlockerManage>>
|
||||
|
||||
export function useBlockerManage() {
|
||||
const blockers: Blocker[] = [useTextBlock()];
|
||||
const leftPanels: any[] = [];
|
||||
|
||||
for (const blocker of blockers) {
|
||||
const panel = blocker.addLeftTab?.();
|
||||
if (panel) {
|
||||
leftPanels.push(leftPanels);
|
||||
}
|
||||
}
|
||||
|
||||
const callInitHook = (container: Container) => {
|
||||
for (const blocker of blockers) {
|
||||
container = blocker.onLoadContainer?.(container) || container;
|
||||
}
|
||||
return container;
|
||||
};
|
||||
|
||||
const callLoadHook = (data: any): Blocker => {
|
||||
for (const blocker of blockers) {
|
||||
data = blocker.onLoadBlock?.(data) || data;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const callSaveHook = (block: Block) => {
|
||||
let data = block;
|
||||
for (const blocker of blockers) {
|
||||
data = blocker.onSaveBlock?.(data) || data;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
return {
|
||||
blockers,
|
||||
leftPanels,
|
||||
callInitHook,
|
||||
callLoadHook,
|
||||
callSaveHook,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,87 +0,0 @@
|
|||
import { Block, Blocker, defineBlocker } from '../../core';
|
||||
import { font } from '../font';
|
||||
import { Text } from './interface';
|
||||
import Option from './option.vue';
|
||||
import Render from './render.vue';
|
||||
|
||||
export default defineBlocker<Text>({
|
||||
type: 'text',
|
||||
icon: 'icon-park-outline-text',
|
||||
title: '文本组件',
|
||||
description: '文字',
|
||||
render: Render,
|
||||
option: Option,
|
||||
initial: {
|
||||
id: '',
|
||||
type: 'text',
|
||||
title: '',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 300,
|
||||
h: 100,
|
||||
xFixed: false,
|
||||
yFixed: false,
|
||||
bgImage: '',
|
||||
bgColor: '',
|
||||
meta: {},
|
||||
actived: false,
|
||||
resizable: true,
|
||||
draggable: true,
|
||||
params: {
|
||||
marquee: false,
|
||||
speed: 100,
|
||||
direction: 'left',
|
||||
fontCh: {
|
||||
...font,
|
||||
content:
|
||||
'温馨提示:乘客您好,进站检票时,持票卡的乘客请在右侧闸机上方感应区内验票,扫码过闸的乘客请将乘车码对准闸机扫码口,扇门打开后依次进闸。乘车过程中请妥善保管好车票,以免丢失。',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function useTextBlock(): Blocker<Text> {
|
||||
const initialData: Text = {
|
||||
id: '',
|
||||
type: 'text',
|
||||
title: '',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 300,
|
||||
h: 100,
|
||||
xFixed: false,
|
||||
yFixed: false,
|
||||
bgImage: '',
|
||||
bgColor: '',
|
||||
meta: {},
|
||||
actived: false,
|
||||
resizable: true,
|
||||
draggable: true,
|
||||
params: {
|
||||
marquee: false,
|
||||
speed: 100,
|
||||
direction: 'left',
|
||||
fontCh: {
|
||||
...font,
|
||||
content:
|
||||
'温馨提示:乘客您好,进站检票时,持票卡的乘客请在右侧闸机上方感应区内验票,扫码过闸的乘客请将乘车码对准闸机扫码口,扇门打开后依次进闸。乘车过程中请妥善保管好车票,以免丢失。',
|
||||
},
|
||||
},
|
||||
};
|
||||
return {
|
||||
type: 'text',
|
||||
icon: 'icon-park-outline-text',
|
||||
title: '文本组件',
|
||||
description: '文字',
|
||||
render: Render,
|
||||
option: Option,
|
||||
initial: initialData,
|
||||
addLeftTab() {
|
||||
return {
|
||||
title: '文本测试',
|
||||
icon: 'icon-park-outline-user',
|
||||
component: () => h('div', null, 'TODO')
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
import { merge } from 'lodash-es';
|
||||
import { Block, Blocker, defineBlocker } from '../../core';
|
||||
import { BlockItem, Plugin } from '../../core/plugin';
|
||||
import { font } from '../font';
|
||||
import { Text } from './interface';
|
||||
import Option from './option.vue';
|
||||
import Render from './render.vue';
|
||||
import { Button } from '@arco-design/web-vue';
|
||||
|
||||
export default defineBlocker<Text>({
|
||||
type: 'text',
|
||||
icon: 'icon-park-outline-text',
|
||||
title: '文本组件',
|
||||
description: '文字',
|
||||
render: Render,
|
||||
option: Option,
|
||||
initial: {
|
||||
id: '',
|
||||
type: 'text',
|
||||
title: '',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 300,
|
||||
h: 100,
|
||||
xFixed: false,
|
||||
yFixed: false,
|
||||
bgImage: '',
|
||||
bgColor: '',
|
||||
meta: {},
|
||||
actived: false,
|
||||
resizable: true,
|
||||
draggable: true,
|
||||
params: {
|
||||
marquee: false,
|
||||
speed: 100,
|
||||
direction: 'left',
|
||||
fontCh: {
|
||||
...font,
|
||||
content:
|
||||
'温馨提示:乘客您好,进站检票时,持票卡的乘客请在右侧闸机上方感应区内验票,扫码过闸的乘客请将乘车码对准闸机扫码口,扇门打开后依次进闸。乘车过程中请妥善保管好车票,以免丢失。',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const defaults: Text = {
|
||||
id: '',
|
||||
type: 'text',
|
||||
title: '',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 300,
|
||||
h: 100,
|
||||
xFixed: false,
|
||||
yFixed: false,
|
||||
bgImage: '',
|
||||
bgColor: '',
|
||||
meta: {},
|
||||
actived: false,
|
||||
resizable: true,
|
||||
draggable: true,
|
||||
params: {
|
||||
marquee: false,
|
||||
speed: 100,
|
||||
direction: 'left',
|
||||
fontCh: {
|
||||
...font,
|
||||
content: '温馨提示:乘客您好',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const item: BlockItem = {
|
||||
type: 'text',
|
||||
icon: 'icon-park-outline-text',
|
||||
title: '文本组件',
|
||||
description: '文字',
|
||||
editRender: Option,
|
||||
viewRender: Render,
|
||||
onInit: () => {
|
||||
return merge({}, defaults);
|
||||
},
|
||||
};
|
||||
|
||||
export function TextBlock(): Plugin {
|
||||
const defaults = {
|
||||
id: '',
|
||||
type: 'text',
|
||||
title: '',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 300,
|
||||
h: 100,
|
||||
xFixed: false,
|
||||
yFixed: false,
|
||||
bgImage: '',
|
||||
bgColor: '',
|
||||
meta: {},
|
||||
actived: false,
|
||||
resizable: true,
|
||||
draggable: true,
|
||||
params: {
|
||||
marquee: false,
|
||||
speed: 100,
|
||||
direction: 'left',
|
||||
fontCh: {
|
||||
...font,
|
||||
content: '温馨提示:乘客您好',
|
||||
},
|
||||
},
|
||||
};
|
||||
return {
|
||||
name: 'TextBlockPlugin',
|
||||
hrRender: {
|
||||
name: 'TextDelete',
|
||||
render() {
|
||||
return (
|
||||
<Button>
|
||||
{{
|
||||
icon: <i class="icon-park-outline-delete"></i>,
|
||||
default: '测试',
|
||||
}}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
hlRender: {
|
||||
name: 'tip',
|
||||
render() {
|
||||
return <span class="text-gray-400 text-xs ml-2">测试提示</span>;
|
||||
},
|
||||
},
|
||||
addBlockItem() {
|
||||
return {
|
||||
type: 'text',
|
||||
icon: 'icon-park-outline-text',
|
||||
title: '文本组件',
|
||||
description: '文字',
|
||||
onInit: () => merge({}, defaults),
|
||||
editRender: Option,
|
||||
viewRender: Render,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -2,41 +2,23 @@
|
|||
<a-modal v-model:visible="show" :fullscreen="true" :footer="false" class="an-editor">
|
||||
<div class="w-full h-full bg-slate-100 grid grid-rows-[auto_1fr] select-none">
|
||||
<div class="h-13 bg-white border-b border-slate-200 z-10">
|
||||
<EditorHeader
|
||||
v-model:container="container"
|
||||
:saving="saving"
|
||||
@preview="showPreview = true"
|
||||
@config="showConfig = true"
|
||||
@exit="onExit()"
|
||||
@save="saveData()"
|
||||
></EditorHeader>
|
||||
<EditorHeader v-model:container="container" :saving="saving" @preview="showPreview = true" @config="showConfig = true" @exit="onExit()" @save="saveData()"></EditorHeader>
|
||||
</div>
|
||||
<div class="grid grid-cols-[auto_1fr_auto] overflow-hidden">
|
||||
<div class="h-full overflow-hidden bg-white shadow-[2px_0_6px_rgba(0,0,0,.05)] z-10">
|
||||
<EditorLeft @rm-block="rmBlock" @current-block="setCurrentBlock"></EditorLeft>
|
||||
</div>
|
||||
<div class="w-full h-full">
|
||||
<EditorMain
|
||||
v-model:rightPanelCollapsed="rightPanelCollapsed"
|
||||
@add-block="addBlock"
|
||||
@current-block="setCurrentBlock"
|
||||
@block-menu="onBlockContextMenu"
|
||||
></EditorMain>
|
||||
<EditorMain v-model:rightPanelCollapsed="rightPanelCollapsed" @add-block="addBlock" @current-block="setCurrentBlock" @block-menu="onBlockContextMenu"></EditorMain>
|
||||
</div>
|
||||
<div class="h-full overflow-hidden bg-white shadow-[-2px_0_6px_rgba(0,0,0,.05)]">
|
||||
<EditorRight v-model:collapsed="rightPanelCollapsed" v-model:block="currentBlock"></EditorRight>
|
||||
<EditorRight v-model:collapsed="rightPanelCollapsed" v-model:block="container.current"></EditorRight>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<EditorPreview v-model:visible="showPreview" :container="container" :blocks="blocks"></EditorPreview>
|
||||
<EditorPreview v-model:visible="showPreview" :container="container" :blocks="container.children"></EditorPreview>
|
||||
<EditorSetting v-model:visible="showConfig" v-model="container"></EditorSetting>
|
||||
<ContextMenu
|
||||
v-model:visible="blockMenu.show"
|
||||
:x="blockMenu.x"
|
||||
:y="blockMenu.y"
|
||||
:items="blockMenuItems"
|
||||
@done="blockMenu.show = false"
|
||||
></ContextMenu>
|
||||
<ContextMenu v-model:visible="blockMenu.show" :x="blockMenu.x" :y="blockMenu.y" :items="blockMenuItems" @done="blockMenu.show = false"></ContextMenu>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
|
|
@ -44,7 +26,7 @@
|
|||
import { delConfirm, sleep } from '@/utils';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Block, ContextMenuItem, EditorKey, useEditor } from '../core';
|
||||
import { Block, EditorKey, useEditor } from '../core';
|
||||
import ContextMenu from './ContextMenu.vue';
|
||||
import EditorSetting from './EditorConfig.vue';
|
||||
import EditorHeader from './EditorHeader.vue';
|
||||
|
|
@ -52,6 +34,8 @@ import EditorLeft from './EditorLeft.vue';
|
|||
import EditorMain from './EditorMain.vue';
|
||||
import EditorPreview from './EditorPreview.vue';
|
||||
import EditorRight from './EditorRight.vue';
|
||||
import { ContextKey, usePluginContext } from '../core/plugin';
|
||||
import { TextBlock } from '../blocks/text';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
|
|
@ -67,7 +51,10 @@ const showPreview = ref(false);
|
|||
const showConfig = ref(false);
|
||||
const saving = ref(false);
|
||||
const editor = useEditor();
|
||||
const { container, blocks, currentBlock, addBlock, rmBlock, setCurrentBlock } = editor;
|
||||
const context = usePluginContext([TextBlock()]);
|
||||
const { container, addBlock, rmBlock, setCurrentBlock } = context;
|
||||
|
||||
console.log({context});
|
||||
|
||||
const blockMenu = reactive<{ show: boolean; x: number; y: number; block: Block | null }>({
|
||||
show: false,
|
||||
|
|
@ -76,7 +63,7 @@ const blockMenu = reactive<{ show: boolean; x: number; y: number; block: Block |
|
|||
block: null,
|
||||
});
|
||||
|
||||
const blockMenuItems: ContextMenuItem[] = [
|
||||
const blockMenuItems: any[] = [
|
||||
{
|
||||
name: '删除',
|
||||
icon: 'icon-park-outline-delete',
|
||||
|
|
@ -103,7 +90,7 @@ const onBlockContextMenu = (block: Block, e: MouseEvent) => {
|
|||
const saveData = async () => {
|
||||
const data = {
|
||||
container: container.value,
|
||||
children: blocks.value,
|
||||
children: container.value.children,
|
||||
};
|
||||
saving.value = true;
|
||||
await sleep(3000);
|
||||
|
|
@ -120,7 +107,7 @@ const loadData = async () => {
|
|||
}
|
||||
const data = JSON.parse(str);
|
||||
container.value = data.container;
|
||||
blocks.value = data.children;
|
||||
container.value.children = data.children;
|
||||
};
|
||||
|
||||
const onExit = async () => {
|
||||
|
|
@ -133,6 +120,7 @@ const onExit = async () => {
|
|||
};
|
||||
|
||||
provide(EditorKey, editor);
|
||||
provide(ContextKey, context);
|
||||
onMounted(loadData);
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -46,9 +46,7 @@ const props = defineProps({
|
|||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:visible', 'update:modelValue']);
|
||||
|
||||
const show = useVModel(props, 'visible', emit);
|
||||
const model = useVModel(props, 'modelValue', emit);
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@
|
|||
</a-link>
|
||||
<a-divider :direction="'vertical'" :margin="8"></a-divider>
|
||||
<ani-texter v-model="container.title"></ani-texter>
|
||||
<component v-for="item in HL" :is="item" />
|
||||
<!-- <a-tag :color="container.id ? 'blue' : 'green'" class="mr-2 ml-1">
|
||||
{{ container.id ? '修改' : '新增' }}
|
||||
</a-tag> -->
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<component v-for="item in HR" :key="item.name" :is="item" />
|
||||
<a-button @click="emit('preview')">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-play"></i>
|
||||
|
|
@ -38,6 +40,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { Container } from '../core';
|
||||
import { ContextKey } from '../core/plugin';
|
||||
import AniTexter from './InputTexter.vue';
|
||||
|
||||
defineProps({
|
||||
|
|
@ -48,8 +51,9 @@ defineProps({
|
|||
})
|
||||
|
||||
const emit = defineEmits(['preview', 'config', 'exit', 'save']);
|
||||
|
||||
const container = defineModel<Container>('container', { required: true });
|
||||
|
||||
const { HR, HL } = inject(ContextKey)!
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
<template>
|
||||
<div class="h-full grid grid-cols-[auto_1fr]" :style="{ width: !collapsed ? '248px' : undefined }">
|
||||
<div class="h-full grid grid-rows-[1fr_auto] border-r border-slate-200">
|
||||
<a-menu
|
||||
:collapsed="true"
|
||||
:default-selected-keys="['0_0']"
|
||||
:selected-keys="[key]"
|
||||
@menu-item-click="(k) => (key = k)"
|
||||
>
|
||||
<a-menu :collapsed="true" :default-selected-keys="['0_0']" :selected-keys="[key]" @menu-item-click="k => (key = k)">
|
||||
<a-menu-item key="list">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-all-application"></i>
|
||||
|
|
@ -31,10 +26,7 @@
|
|||
<a-tooltip :content="collapsed ? '展开' : '折叠'" position="right">
|
||||
<a-button type="text" @click="collapsed = !collapsed">
|
||||
<template #icon>
|
||||
<i
|
||||
class="text-lg text-gray-400 hover:text-gray-700"
|
||||
:class="collapsed ? 'icon-park-outline-expand-left' : 'icon-park-outline-expand-right'"
|
||||
></i>
|
||||
<i class="text-lg text-gray-400 hover:text-gray-700" :class="collapsed ? 'icon-park-outline-expand-left' : 'icon-park-outline-expand-right'"></i>
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
|
@ -61,13 +53,13 @@
|
|||
</ul>
|
||||
<ul v-show="key === 'data'" class="list-none px-2 grid gap-2">
|
||||
<li
|
||||
v-for="item in blocks"
|
||||
v-for="item in container.children"
|
||||
:key="item.id"
|
||||
class="group h-8 w-full overflow-hidden grid grid-cols-[auto_1fr_auto] items-center gap-2 bg-gray-100 text-gray-500 px-2 py-1 rounded border border-transparent"
|
||||
:class="{
|
||||
'!bg-brand-50': currentBlock === item,
|
||||
'!text-brand-500': currentBlock === item,
|
||||
'!border-brand-300': currentBlock === item,
|
||||
'!bg-brand-50': container.current === item,
|
||||
'!text-brand-500': container.current === item,
|
||||
'!border-brand-300': container.current === item,
|
||||
}"
|
||||
@click="emit('current-block', item)"
|
||||
>
|
||||
|
|
@ -78,10 +70,7 @@
|
|||
{{ item.title }}
|
||||
</div>
|
||||
<div class="w-4">
|
||||
<i
|
||||
class="!hidden !group-hover:inline-block text-gray-400 hover:text-gray-700 icon-park-outline-delete !text-xs"
|
||||
@click.prevent="emit('rm-block', item)"
|
||||
></i>
|
||||
<i class="!hidden !group-hover:inline-block text-gray-400 hover:text-gray-700 icon-park-outline-delete !text-xs" @click.prevent="emit('rm-block', item)"></i>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -90,24 +79,24 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getIcon } from "../blocks";
|
||||
import { Block, EditorKey } from "../core";
|
||||
import { getIcon } from '../blocks';
|
||||
import { Block, EditorKey } from '../core';
|
||||
|
||||
const { blocks, currentBlock, BlockerMap } = inject(EditorKey)!;
|
||||
const { container, BlockerMap } = inject(EditorKey)!;
|
||||
const blockList = Object.values(BlockerMap);
|
||||
const collapsed = ref(false);
|
||||
const key = ref<"list" | "data">("list");
|
||||
const key = ref<'list' | 'data'>('list');
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "rm-block", block: Block): void;
|
||||
(event: "current-block", block: Block | null): void;
|
||||
(event: 'rm-block', block: Block): void;
|
||||
(event: 'current-block', block: Block | null): void;
|
||||
}>();
|
||||
|
||||
/**
|
||||
* 拖拽开始时设置数据
|
||||
*/
|
||||
const onDragStart = (e: DragEvent) => {
|
||||
e.dataTransfer?.setData("type", (e.target as HTMLElement).dataset.type!);
|
||||
e.dataTransfer?.setData('type', (e.target as HTMLElement).dataset.type!);
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,13 @@
|
|||
<template>
|
||||
<div class="h-full grid grid-rows-[auto_1fr]">
|
||||
<div class="h-10">
|
||||
<EditorMainHeader
|
||||
:container="container"
|
||||
v-model:rightPanelCollapsed="rightPanelCollapsed"
|
||||
@preview="emit('preview')"
|
||||
></EditorMainHeader>
|
||||
<EditorMainHeader :container="container" v-model:rightPanelCollapsed="rightPanelCollapsed" @preview="emit('preview')"></EditorMainHeader>
|
||||
</div>
|
||||
<div class="h-full w-full overflow-hidden p-4">
|
||||
<div
|
||||
class="juetan-editor-container w-full h-full flex items-center justify-center overflow-hidden relative bg-slate-50"
|
||||
>
|
||||
<div
|
||||
class="relative"
|
||||
:style="containerStyle"
|
||||
@dragover.prevent
|
||||
@click="onClick"
|
||||
@drop="onDragDrop"
|
||||
@wheel="onMouseWheel"
|
||||
@mousedown="onMouseDown"
|
||||
>
|
||||
<div class="juetan-editor-container w-full h-full flex items-center justify-center overflow-hidden relative bg-slate-50">
|
||||
<div class="relative" :style="containerStyle" @dragover.prevent @click="onClick" @drop="onDragDrop" @wheel="onMouseWheel" @mousedown="onMouseDown">
|
||||
<EditorMainBlock
|
||||
v-for="block in blocks"
|
||||
v-for="block in container.children"
|
||||
:key="block.id"
|
||||
:data="block"
|
||||
:container="container"
|
||||
|
|
@ -60,12 +46,13 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Block, EditorKey } from '../core';
|
||||
import { Block, EditorKey, formatContainerStyle } from '../core';
|
||||
import { ContextKey } from '../core/plugin';
|
||||
import EditorMainBlock from './EditorMainBlock.vue';
|
||||
import EditorMainHeader from './EditorMainHeader.vue';
|
||||
|
||||
const rightPanelCollapsed = defineModel<boolean>('rightPanelCollapsed');
|
||||
const { blocks, container, refLine, formatContainerStyle, scene } = inject(EditorKey)!;
|
||||
const { container, refLine, scene } = inject(ContextKey)!;
|
||||
const { onMouseDown, onMouseWheel } = scene;
|
||||
const { active, xLines, yLines } = refLine;
|
||||
|
||||
|
|
@ -107,14 +94,7 @@ const onDragDrop = (e: DragEvent) => {
|
|||
<style scoped>
|
||||
.juetan-editor-container {
|
||||
--color: rgba(0, 0, 0, 0.2);
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
var(--color) 25%,
|
||||
transparent 25%,
|
||||
transparent 75%,
|
||||
var(--color) 75%,
|
||||
var(--color) 100%
|
||||
),
|
||||
background: linear-gradient(45deg, var(--color) 25%, transparent 25%, transparent 75%, var(--color) 75%, var(--color) 100%),
|
||||
linear-gradient(45deg, var(--color) 25%, transparent 25%, transparent 75%, var(--color) 75%, var(--color) 100%);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 10px 10px;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@
|
|||
import { PropType } from "vue";
|
||||
import { BlockerMap } from "../blocks";
|
||||
import DragResizer from "./DragResizer.vue";
|
||||
import { Block, Container, EditorKey } from "../core";
|
||||
import { Block, Container } from "../core";
|
||||
import { ContextKey } from "../core/plugin";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
|
|
@ -41,7 +42,7 @@ const props = defineProps({
|
|||
},
|
||||
});
|
||||
|
||||
const { setCurrentBlock, refLine } = inject(EditorKey)!;
|
||||
const { setCurrentBlock, refLine } = inject(ContextKey)!;
|
||||
const { active, recordBlocksXY, updateRefLine } = refLine;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
</span>
|
||||
<span class="text-gray-400 text-xs mr-2">
|
||||
组件:
|
||||
<span class="inline-block w-8 text-gray-700">{{ blocks.length }} 个</span>
|
||||
<span class="inline-block w-8 text-gray-700">{{ container.children.length }} 个</span>
|
||||
</span>
|
||||
<a-tooltip content="自适应比例" position="bottom">
|
||||
<a-button type="text" @click="setContainerOrigin">
|
||||
|
|
@ -62,8 +62,8 @@
|
|||
<script setup lang="ts">
|
||||
import InputTexter from './InputTexter.vue';
|
||||
// import EditorMainConfig from './EditorMainConfig.vue';
|
||||
import { EditorKey } from '../core';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { ContextKey } from '../core/plugin';
|
||||
|
||||
const props = defineProps({
|
||||
rightPanelCollapsed: {
|
||||
|
|
@ -74,7 +74,7 @@ const props = defineProps({
|
|||
|
||||
const emit = defineEmits(['preview', 'update:rightPanelCollapsed']);
|
||||
const collapsed = useVModel(props, 'rightPanelCollapsed', emit);
|
||||
const { container, blocks, setContainerOrigin } = inject(EditorKey)!;
|
||||
const { container, setContainerOrigin } = inject(ContextKey)!;
|
||||
|
||||
const visible = ref(false);
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<template #icon>
|
||||
<i :class="BlockerMap[model.type].icon"></i>
|
||||
</template>
|
||||
{{ BlockerMap[model.type].title }}属性
|
||||
{{ BlockerMap[model.type].title }}
|
||||
</a-tag>
|
||||
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto">
|
||||
<a-form :model="{}" layout="vertical" class="pr-3">
|
||||
|
|
@ -23,12 +23,13 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { BlockerMap } from '../blocks';
|
||||
import { Block, EditorKey } from '../core';
|
||||
import { Block } from '../core';
|
||||
import { ContextKey } from '../core/plugin';
|
||||
import EditorSetting from './EditorSetting.vue';
|
||||
|
||||
const collapsed = defineModel<boolean>('collapsed');
|
||||
const model = defineModel<Block | null>('block');
|
||||
const { container } = inject(EditorKey)!;
|
||||
const { container } = inject(ContextKey)!;
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="p-3">
|
||||
<a-tag class="text-sm! mb-2 w-full" size="large" color="blue" :bordered="false">
|
||||
<a-tag class="text-sm! mb-2 w-full" size="large" color="red" :bordered="false">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-config" ></i>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Component } from "vue";
|
||||
import { Component } from 'vue';
|
||||
|
||||
/**
|
||||
* 组件参数
|
||||
|
|
@ -70,25 +70,11 @@ export interface Block<T = any> {
|
|||
params: T;
|
||||
}
|
||||
|
||||
export interface ContextMenuItem {
|
||||
type?: 'divider' | 'menu'
|
||||
showChildren?: boolean
|
||||
onClick?: (item: ContextMenuItem) => void;
|
||||
icon?: Component | string
|
||||
name: string
|
||||
tip?: string
|
||||
class?: string;
|
||||
children?: ContextMenuItem[]
|
||||
export function formatBlockStyle(block: Block) {
|
||||
const { bgColor, bgImage } = block;
|
||||
return {
|
||||
backgroundColor: bgColor,
|
||||
backgroundImage: bgImage ? `url(${bgImage})` : null,
|
||||
backgroundSize: '100% 100%',
|
||||
};
|
||||
}
|
||||
|
||||
export const useBlockContextMenu = (blocks: Block[]) => {
|
||||
const items: ContextMenuItem[] = [
|
||||
{
|
||||
name: '删除',
|
||||
icon: () => h('i', { class: 'icon-park-outline-delete' }),
|
||||
onClick(item) {
|
||||
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
import { CSSProperties } from 'vue';
|
||||
import { Block } from './block';
|
||||
|
||||
/**
|
||||
* 画布配置
|
||||
*/
|
||||
|
|
@ -42,8 +45,22 @@ export interface Container {
|
|||
* 背景颜色
|
||||
*/
|
||||
bgColor: string;
|
||||
/**
|
||||
* 使用的语言列表
|
||||
*/
|
||||
langList: string[];
|
||||
/**
|
||||
* 语言的切换间隔
|
||||
*/
|
||||
langSwitch: number;
|
||||
/**
|
||||
* 组件列表
|
||||
*/
|
||||
children: Block[];
|
||||
/**
|
||||
* 当前选中的组件
|
||||
*/
|
||||
current: Block | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -51,15 +68,30 @@ export interface Container {
|
|||
*/
|
||||
export const defaultContainer: Container = {
|
||||
id: 11,
|
||||
title: "国庆节喜庆版式设计",
|
||||
description: "适用于国庆节1日-7日间上午9:00-10:00播出的版式设计",
|
||||
title: '国庆节喜庆版式设计',
|
||||
description: '适用于国庆节1日-7日间上午9:00-10:00播出的版式设计',
|
||||
x: 0,
|
||||
y: 0,
|
||||
zoom: 0.7,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
bgImage: "",
|
||||
bgColor: "#ffffff",
|
||||
bgImage: '',
|
||||
bgColor: '#ffffff',
|
||||
langList: ['ch', 'en'],
|
||||
langSwitch: 0
|
||||
langSwitch: 0,
|
||||
children: [],
|
||||
current: null,
|
||||
};
|
||||
|
||||
export function formatContainerStyle(container: Container) {
|
||||
const { width, height, bgColor, bgImage, zoom, x, y } = container;
|
||||
return {
|
||||
position: 'absolute',
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
backgroundColor: bgColor,
|
||||
backgroundImage: bgImage ? `url(${bgImage})` : null,
|
||||
backgroundSize: '100% 100%',
|
||||
transform: `translate3d(${x}px, ${y}px, 0) scale(${zoom})`,
|
||||
} as CSSProperties;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,16 @@
|
|||
import { Container, defaultContainer } from "./container";
|
||||
import { Block } from "./block";
|
||||
import { useReferenceLine } from "./ref-line";
|
||||
import { BlockerMap } from "../blocks";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { CSSProperties, InjectionKey } from "vue";
|
||||
import { useScene } from "./scene";
|
||||
import { Container, defaultContainer } from './container';
|
||||
import { Block } from './block';
|
||||
import { useReferenceLine } from './ref-line';
|
||||
import { BlockerMap } from '../blocks';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { CSSProperties, InjectionKey } from 'vue';
|
||||
import { useScene } from './scene';
|
||||
|
||||
export const useEditor = () => {
|
||||
/**
|
||||
* 画布设置
|
||||
*/
|
||||
const container = ref<Container>({ ...defaultContainer });
|
||||
/**
|
||||
* 组件列表
|
||||
*/
|
||||
const blocks = ref<Block[]>([]);
|
||||
/**
|
||||
* 选中组件
|
||||
*/
|
||||
|
|
@ -22,7 +18,7 @@ export const useEditor = () => {
|
|||
/**
|
||||
* 参考线
|
||||
*/
|
||||
const refLine = useReferenceLine(blocks, currentBlock);
|
||||
const refLine = useReferenceLine(container);
|
||||
/**
|
||||
* 画布移动和缩放
|
||||
*/
|
||||
|
|
@ -43,17 +39,11 @@ export const useEditor = () => {
|
|||
if (!blocker) {
|
||||
return;
|
||||
}
|
||||
const ids = blocks.value.map((i) => Number(i.id));
|
||||
const ids = container.value.children.map(i => Number(i.id));
|
||||
const maxId = ids.length ? Math.max.apply(null, ids) : 0;
|
||||
const id = (maxId + 1).toString();
|
||||
const title = `${blocker.title}${id}`;
|
||||
blocks.value.push({
|
||||
...cloneDeep(blocker.initial),
|
||||
id,
|
||||
x,
|
||||
y,
|
||||
title,
|
||||
});
|
||||
container.value.children.push({ ...cloneDeep(blocker.initial), id, x, y, title });
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -61,9 +51,9 @@ export const useEditor = () => {
|
|||
* @param block 组件
|
||||
*/
|
||||
const rmBlock = (block: Block) => {
|
||||
const index = blocks.value.indexOf(block);
|
||||
const index = container.value.children.indexOf(block);
|
||||
if (index > -1) {
|
||||
blocks.value.splice(index, 1);
|
||||
container.value.children.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -77,7 +67,7 @@ export const useEditor = () => {
|
|||
return {
|
||||
backgroundColor: bgColor,
|
||||
backgroundImage: bgImage ? `url(${bgImage})` : undefined,
|
||||
backgroundSize: "100% 100%",
|
||||
backgroundSize: '100% 100%',
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -89,12 +79,12 @@ export const useEditor = () => {
|
|||
const formatContainerStyle = (container: Container) => {
|
||||
const { width, height, bgColor, bgImage, zoom, x, y } = container;
|
||||
return {
|
||||
position: "absolute",
|
||||
position: 'absolute',
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
backgroundColor: bgColor,
|
||||
backgroundImage: bgImage ? `url(${bgImage})` : undefined,
|
||||
backgroundSize: "100% 100%",
|
||||
backgroundSize: '100% 100%',
|
||||
transform: `translate3d(${x}px, ${y}px, 0) scale(${zoom})`,
|
||||
} as CSSProperties;
|
||||
};
|
||||
|
|
@ -104,7 +94,7 @@ export const useEditor = () => {
|
|||
* @param block 组件
|
||||
*/
|
||||
const setCurrentBlock = (block: Block | null) => {
|
||||
for (const item of blocks.value) {
|
||||
for (const item of container.value.children) {
|
||||
item.actived = false;
|
||||
}
|
||||
if (!block) {
|
||||
|
|
@ -121,7 +111,7 @@ export const useEditor = () => {
|
|||
const setContainerOrigin = () => {
|
||||
container.value.x = 0;
|
||||
container.value.y = 0;
|
||||
const el = document.querySelector(".juetan-editor-container");
|
||||
const el = document.querySelector('.juetan-editor-container');
|
||||
if (el) {
|
||||
const { width, height } = el.getBoundingClientRect();
|
||||
const wZoom = width / container.value.width;
|
||||
|
|
@ -133,7 +123,6 @@ export const useEditor = () => {
|
|||
|
||||
return {
|
||||
container,
|
||||
blocks,
|
||||
currentBlock,
|
||||
refLine,
|
||||
scene,
|
||||
|
|
@ -147,4 +136,4 @@ export const useEditor = () => {
|
|||
};
|
||||
};
|
||||
|
||||
export const EditorKey = Symbol("EditorKey") as InjectionKey<ReturnType<typeof useEditor>>;
|
||||
export const EditorKey = Symbol('EditorKey') as InjectionKey<ReturnType<typeof useEditor>>;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,255 @@
|
|||
import { CSSProperties, Component } from 'vue';
|
||||
import { Block } from './block';
|
||||
import { Container, defaultContainer } from './container';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { useReferenceLine } from './ref-line';
|
||||
import { useScene } from './scene';
|
||||
|
||||
export interface BlockItem {
|
||||
/**
|
||||
* 需唯一
|
||||
*/
|
||||
type: string;
|
||||
/**
|
||||
* 图标
|
||||
*/
|
||||
icon: string;
|
||||
/**
|
||||
* 名字
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* 在组件库时的渲染
|
||||
*/
|
||||
pickRender?: any;
|
||||
/**
|
||||
* 在列表中时的渲染
|
||||
*/
|
||||
listRender?: any;
|
||||
/**
|
||||
* 在编辑中时的渲染
|
||||
*/
|
||||
showRender?: any;
|
||||
/**
|
||||
* 在预览中时的渲染
|
||||
*/
|
||||
viewRender?: any;
|
||||
/**
|
||||
* 编辑属性时的渲染
|
||||
*/
|
||||
editRender?: any;
|
||||
/**
|
||||
* 初始化默认参数
|
||||
*/
|
||||
onInit?: any;
|
||||
/**
|
||||
* 转换数据
|
||||
*/
|
||||
onLoad?: any;
|
||||
/**
|
||||
* 转换数据
|
||||
*/
|
||||
onSave?: any;
|
||||
}
|
||||
|
||||
interface SortableRender {
|
||||
name: string;
|
||||
sort?: number;
|
||||
render: Component;
|
||||
}
|
||||
|
||||
export interface Plugin {
|
||||
/**
|
||||
* 名字需唯一
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* 须有唯一的 name 属性
|
||||
*/
|
||||
hlRender?: SortableRender;
|
||||
/**
|
||||
* 须有唯一的 name 属性
|
||||
*/
|
||||
hrRender?: SortableRender;
|
||||
/**
|
||||
* 须有唯一的 name 属性
|
||||
*/
|
||||
ltRender?: SortableRender;
|
||||
/**
|
||||
* 须有唯一的 name 属性
|
||||
*/
|
||||
lbRender?: SortableRender;
|
||||
/**
|
||||
* 须有唯一的 name 属性
|
||||
*/
|
||||
mlRender?: SortableRender;
|
||||
/**
|
||||
* 须有唯一的 name 属性
|
||||
*/
|
||||
mrRender?: SortableRender;
|
||||
/**
|
||||
* 须有唯一的 name 属性
|
||||
*/
|
||||
rtRender?: SortableRender;
|
||||
rbRender?: () => BlockItem;
|
||||
addBlockItem?: () => BlockItem;
|
||||
blockItems?: BlockItem | BlockItem[];
|
||||
onInit?: (context: any) => void;
|
||||
onSave?: () => void;
|
||||
onLoad?: (data: any) => void;
|
||||
}
|
||||
|
||||
export const usePluginContext = (pluginlist: Plugin[]) => {
|
||||
const container: Ref<Container> = ref(cloneDeep(defaultContainer)) as any;
|
||||
const blocks = computed(() => container.value.children);
|
||||
const blockerMap: Record<string, BlockItem> = {};
|
||||
const refLine = useReferenceLine(container);
|
||||
const scene = useScene(container);
|
||||
|
||||
/** 顶部栏左侧 */
|
||||
const HL: Ref<SortableRender[]> = ref([]);
|
||||
/** 顶部栏右侧 */
|
||||
const HR: Ref<SortableRender[]> = ref([]);
|
||||
/** 左侧栏顶部 */
|
||||
const LC: Ref<SortableRender[]> = ref([]);
|
||||
/** 左侧栏底部 */
|
||||
const LB: Ref<SortableRender[]> = ref([]);
|
||||
/** 中间栏左侧 */
|
||||
const ML: Ref<SortableRender[]> = ref([]);
|
||||
/** 中间栏右侧 */
|
||||
const MR: Ref<SortableRender[]> = ref([]);
|
||||
/** 右侧栏顶部 */
|
||||
const RC: Ref<SortableRender[]> = ref([]);
|
||||
|
||||
function load(data: any) {
|
||||
data.children = data.children.map(item => {
|
||||
return blockerMap[item.type]?.onLoad?.(item) ?? item;
|
||||
});
|
||||
for (const plugin of pluginlist) {
|
||||
data = plugin.onLoad?.(data) ?? data;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function save(container: Container) {}
|
||||
|
||||
function addBlock(type: string, x = 0, y = 0) {
|
||||
const blocker = blockerMap[type];
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
if (!blocker) {
|
||||
return;
|
||||
}
|
||||
const ids = blocks.value.map(i => Number(i.id));
|
||||
const maxId = ids.length ? Math.max.apply(null, ids) : 0;
|
||||
const id = (maxId + 1).toString();
|
||||
const title = `${blocker.title}${id}`;
|
||||
const block = { ...cloneDeep(blocker.onInit?.()), id, x, y, title };
|
||||
blocks.value.push(block);
|
||||
}
|
||||
|
||||
function rmBlock(block: Block) {
|
||||
const index = blocks.value.indexOf(block);
|
||||
if (index > -1) {
|
||||
blocks.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function setCurrentBlock(block: Block | null) {
|
||||
for (const item of container.value.children) {
|
||||
item.actived = false;
|
||||
}
|
||||
if (!block) {
|
||||
container.value.current = null;
|
||||
} else {
|
||||
block.actived = true;
|
||||
container.value.current = block;
|
||||
}
|
||||
}
|
||||
|
||||
function setContainerOrigin() {
|
||||
container.value.x = 0;
|
||||
container.value.y = 0;
|
||||
const el = document.querySelector('.juetan-editor-container');
|
||||
if (el) {
|
||||
const { width, height } = el.getBoundingClientRect();
|
||||
const wZoom = width / container.value.width;
|
||||
const hZoom = height / container.value.width;
|
||||
const zoom = Math.floor((wZoom > hZoom ? wZoom : hZoom) * 10000) / 10000;
|
||||
container.value.zoom = zoom;
|
||||
}
|
||||
}
|
||||
|
||||
const context = {
|
||||
container,
|
||||
blockerMap,
|
||||
refLine,
|
||||
scene,
|
||||
HL,
|
||||
HR,
|
||||
LB,
|
||||
LC,
|
||||
ML,
|
||||
MR,
|
||||
RC,
|
||||
setCurrentBlock,
|
||||
setContainerOrigin,
|
||||
addBlock,
|
||||
rmBlock,
|
||||
};
|
||||
|
||||
function addRender(list: any[], render?: SortableRender) {
|
||||
if (!render) {
|
||||
return;
|
||||
}
|
||||
if (list.some(i => i.name === render.name)) {
|
||||
console.log('name has existed');
|
||||
return;
|
||||
}
|
||||
list.push(render);
|
||||
}
|
||||
|
||||
for (const plugin of pluginlist) {
|
||||
plugin.onInit?.(context);
|
||||
addRender(HL.value, plugin.hlRender);
|
||||
addRender(HR.value, plugin.hrRender);
|
||||
addRender(LC.value, plugin.ltRender);
|
||||
addRender(LB.value, plugin.lbRender);
|
||||
addRender(ML.value, plugin.mlRender);
|
||||
addRender(MR.value, plugin.mrRender);
|
||||
addRender(RC.value, plugin.rtRender);
|
||||
const bi = plugin.addBlockItem?.();
|
||||
if (bi) {
|
||||
blockerMap[bi.type] = bi;
|
||||
}
|
||||
}
|
||||
|
||||
HL.value.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
|
||||
HR.value.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
|
||||
LC.value.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
|
||||
LB.value.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
|
||||
ML.value.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
|
||||
MR.value.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
|
||||
RC.value.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export const ContextKey = Symbol('ContextKey') as InjectionKey<ReturnType<typeof usePluginContext>>;
|
||||
|
||||
function corePlugin(): Plugin {
|
||||
return {
|
||||
name: 'core',
|
||||
rtRender: {
|
||||
name: 'ss',
|
||||
render() {
|
||||
return () => 123;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { Ref } from "vue";
|
||||
import { getClosestValInSortedArr } from "../utils/closest";
|
||||
import { Block } from "./block";
|
||||
import { Container } from "./container";
|
||||
|
||||
/**
|
||||
* 组件参考线
|
||||
|
|
@ -8,7 +9,7 @@ import { Block } from "./block";
|
|||
* @param current 当前组件
|
||||
* @returns
|
||||
*/
|
||||
export const useReferenceLine = (blocks: Ref<Block[]>, current: Ref<Block | null>) => {
|
||||
export const useReferenceLine = (container: Ref<Container>) => {
|
||||
let xYsMap = new Map<number, number[]>();
|
||||
let yXsMap = new Map<number, number[]>();
|
||||
let sortedXs: number[] = [];
|
||||
|
|
@ -22,8 +23,8 @@ export const useReferenceLine = (blocks: Ref<Block[]>, current: Ref<Block | null
|
|||
*/
|
||||
const recordBlocksXY = () => {
|
||||
clear();
|
||||
for (const block of blocks.value) {
|
||||
if (block === current.value) {
|
||||
for (const block of container.value.children) {
|
||||
if (block === container.value.current) {
|
||||
continue;
|
||||
}
|
||||
const { minX, minY, midX, midY, maxX, maxY } = getBlockBox(block);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
export function arraify<T>(data: T | T[]): T[] {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
return Array.isArray(data) ? data : [data];
|
||||
}
|
||||
|
|
@ -1,12 +1,4 @@
|
|||
import {
|
||||
AnForm,
|
||||
AnFormInstance,
|
||||
AnFormModal,
|
||||
AnFormModalInstance,
|
||||
AnFormModalProps,
|
||||
AnFormProps,
|
||||
getModel,
|
||||
} from '@/components/AnForm';
|
||||
import { AnForm, AnFormInstance, AnFormModal, AnFormModalInstance, AnFormModalProps, AnFormProps, getModel } from '@/components/AnForm';
|
||||
import AnEmpty from '@/components/AnEmpty/AnEmpty.vue';
|
||||
import { Button, PaginationProps, Table, TableColumnData, TableData, TableInstance } from '@arco-design/web-vue';
|
||||
import { isArray, isFunction, merge } from 'lodash-es';
|
||||
|
|
@ -14,14 +6,8 @@ import { InjectionKey, PropType, Ref, VNodeChild, defineComponent, ref } from 'v
|
|||
import { PluginContainer } from '../hooks/useTablePlugin';
|
||||
|
||||
type DataFn = (filter: { page: number; size: number; [key: string]: any }) => any | Promise<any>;
|
||||
|
||||
export type ArcoTableProps = Omit<
|
||||
TableInstance['$props'],
|
||||
'ref' | 'pagination' | 'loading' | 'data' | 'onPageChange' | 'onPageSizeChange'
|
||||
>;
|
||||
|
||||
export type ArcoTableProps = Omit<TableInstance['$props'], 'ref' | 'pagination' | 'loading' | 'data' | 'onPageChange' | 'onPageSizeChange'>;
|
||||
export const AnTableContextKey = Symbol('AnTableContextKey') as InjectionKey<AnTableContext>;
|
||||
|
||||
export type TableColumnRender = (data: { record: TableData; column: TableColumnData; rowIndex: number }) => VNodeChild;
|
||||
|
||||
/**
|
||||
|
|
@ -207,9 +193,7 @@ export const AnTable = defineComponent({
|
|||
<div class="an-table table w-full">
|
||||
<div class={`mb-3 flex gap-2 toolbar justify-between`}>
|
||||
{this.create && <AnFormModal {...this.create} ref="createRef" onSubmited={this.reload}></AnFormModal>}
|
||||
{this.modify && (
|
||||
<AnFormModal {...this.modify} trigger={false} ref="modifyRef" onSubmited={this.refresh}></AnFormModal>
|
||||
)}
|
||||
{this.modify && <AnFormModal {...this.modify} trigger={false} ref="modifyRef" onSubmited={this.refresh}></AnFormModal>}
|
||||
{this.$slots.action?.(this.renderData)}
|
||||
{this.pluginer?.actions && (
|
||||
<div class={`flex-1 flex gap-2 items-center`}>
|
||||
|
|
@ -220,12 +204,7 @@ export const AnTable = defineComponent({
|
|||
)}
|
||||
{this.search && (
|
||||
<div>
|
||||
<AnForm
|
||||
ref="searchRef"
|
||||
v-model:model={this.search.model}
|
||||
items={this.search.items}
|
||||
formProps={this.search.formProps}
|
||||
>
|
||||
<AnForm ref="searchRef" v-model:model={this.search.model} items={this.search.items} formProps={this.search.formProps}>
|
||||
{{
|
||||
submit: () => (
|
||||
<Button type="primary" loading={this.loading} onClick={this.reload}>
|
||||
|
|
@ -279,10 +258,7 @@ export type AnTableInstance = InstanceType<typeof AnTable>;
|
|||
/**
|
||||
* 表格组件参数
|
||||
*/
|
||||
export type AnTableProps = Pick<
|
||||
AnTableInstance['$props'],
|
||||
'source' | 'columns' | 'search' | 'paging' | 'create' | 'modify' | 'tableProps' | 'pluginer'
|
||||
>;
|
||||
export type AnTableProps = Pick<AnTableInstance['$props'], 'source' | 'columns' | 'search' | 'paging' | 'create' | 'modify' | 'tableProps' | 'pluginer'>;
|
||||
|
||||
export interface AnTableContext {
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
import { dayjs } from '@/libs/dayjs';
|
||||
import { Avatar } from '@arco-design/web-vue';
|
||||
import { TableColumn } from '../hooks/useTableColumn';
|
||||
|
||||
export function useUpdateColumn(extra: TableColumn = {}): TableColumn {
|
||||
return {
|
||||
title: '更新',
|
||||
title: '最近修改',
|
||||
dataIndex: 'createdAt',
|
||||
width: 180,
|
||||
render: ({ record }) => (
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<span>{record.updatedBy ?? '无'}</span>
|
||||
<span class="text-gray-400 text-xs truncate" title={record.updatedAt}>
|
||||
更新于 {dayjs(record.updatedAt).fromNow()}
|
||||
<div class="flex items-center gap-2 overflow-hidden">
|
||||
<span>
|
||||
<Avatar size={22}>{record.updatedBy?.substr(0,1) ?? '无'}</Avatar>
|
||||
</span>
|
||||
<span class="truncate" title={record.updatedAt}>
|
||||
{dayjs(record.updatedAt).fromNow()}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
|
|
@ -24,10 +27,12 @@ export function useCreateColumn(extra: TableColumn = {}): TableColumn {
|
|||
dataIndex: 'createdAt',
|
||||
width: 180,
|
||||
render: ({ record }) => (
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<span>{record.createdBy ?? '无'}</span>
|
||||
<div class="flex direction-col items-center gap-2 overflow-hidden">
|
||||
<span>
|
||||
{record.createdBy ?? '无'}
|
||||
</span>
|
||||
<span class="text-gray-400 text-xs truncate" title={record.createdAt}>
|
||||
创建于 {dayjs(record.createdAt).fromNow()}
|
||||
{dayjs(record.createdAt).fromNow()}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import { ButtonProps, TableData } from '@arco-design/web-vue';
|
||||
|
||||
export interface AnTableActionBase {
|
||||
text: string;
|
||||
icon: string | Component;
|
||||
visible: () => boolean;
|
||||
disable: () => boolean;
|
||||
buttonProps: ButtonProps;
|
||||
}
|
||||
|
||||
interface AnTableActionBatch {
|
||||
type: 'batch';
|
||||
onClick: (rows: TableData) => void;
|
||||
}
|
||||
|
||||
export type AnTableAction = AnTableActionBase & AnTableActionBatch;
|
||||
|
|
@ -35,6 +35,17 @@ export interface TableUseOptions extends Pick<AnTableProps, 'source' | 'tablePro
|
|||
* ```
|
||||
*/
|
||||
columns?: TableColumn[];
|
||||
/**
|
||||
* 操作栏
|
||||
* @example
|
||||
* ```ts
|
||||
* [{
|
||||
* text: '按钮',
|
||||
* onClick: () => null,
|
||||
* }]
|
||||
* ```
|
||||
*/
|
||||
actions?: any[];
|
||||
/**
|
||||
* 搜索表单
|
||||
* @example
|
||||
|
|
|
|||
|
|
@ -2,22 +2,17 @@ import dayjs from 'dayjs';
|
|||
import 'dayjs/locale/zh-cn';
|
||||
import localData from 'dayjs/plugin/localeData';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { App } from 'vue';
|
||||
|
||||
/**
|
||||
*
|
||||
* 默认日期时间格式
|
||||
*/
|
||||
const DATETIME = 'YYYY-MM-DD HH:mm';
|
||||
|
||||
/**
|
||||
* 默认日期格式
|
||||
*/
|
||||
const DATE = 'YYYY-MM-DD';
|
||||
|
||||
/**
|
||||
* 默认时间格式
|
||||
*/
|
||||
const TIME = 'HH:mm:ss';
|
||||
declare module 'dayjs' {
|
||||
export var DATETIME: 'YYYY-MM-DD HH:mm';
|
||||
export var DATE: 'YYYY-MM-DD';
|
||||
export var TIME: 'HH:mm:ss';
|
||||
export var install: (app: App) => void;
|
||||
interface Dayjs {
|
||||
_format: Dayjs['format'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 中文语言包
|
||||
|
|
@ -39,17 +34,17 @@ dayjs.extend(localData);
|
|||
/**
|
||||
* 默认时间格式
|
||||
*/
|
||||
dayjs.DATETIME = DATETIME;
|
||||
dayjs.DATETIME = 'YYYY-MM-DD HH:mm';
|
||||
|
||||
/**
|
||||
* 默认日期格式
|
||||
*/
|
||||
dayjs.DATE = DATE;
|
||||
dayjs.DATE = 'YYYY-MM-DD';
|
||||
|
||||
/**
|
||||
* 默认时间格式
|
||||
*/
|
||||
dayjs.TIME = TIME;
|
||||
dayjs.TIME = 'HH:mm:ss';
|
||||
|
||||
/**
|
||||
* 保留原方法
|
||||
|
|
@ -59,11 +54,15 @@ dayjs.prototype._format = dayjs.prototype.format;
|
|||
/**
|
||||
* 重写,设置默认时间格式
|
||||
*/
|
||||
dayjs.prototype.format = function (format?: string) {
|
||||
if (format) {
|
||||
return this._format(format);
|
||||
}
|
||||
return this._format(dayjs.DATETIME);
|
||||
dayjs.prototype.format = function (format: string = dayjs.DATETIME) {
|
||||
return this._format(format);
|
||||
};
|
||||
|
||||
export { DATE, DATETIME, TIME, dayjs };
|
||||
/**
|
||||
* 作为VUE插件进行初始化
|
||||
*/
|
||||
dayjs.install = function dayjsPlugin(app: App) {
|
||||
app.config.globalProperties.$dayjs = dayjs;
|
||||
};
|
||||
|
||||
export { dayjs };
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
import 'dayjs';
|
||||
|
||||
declare module 'dayjs' {
|
||||
/**
|
||||
* 默认日期时间格式
|
||||
*/
|
||||
export var DATETIME: 'YYYY-MM-DD HH:mm';
|
||||
|
||||
export var DATE: 'YYYY-MM-DD';
|
||||
|
||||
export var TIME: 'HH:mm:ss';
|
||||
|
||||
interface Dayjs {
|
||||
_format: Dayjs['format'];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import NProgress from 'nprogress';
|
||||
import 'nprogress/nprogress.css';
|
||||
import './nprogress.css';
|
||||
import { App } from 'vue';
|
||||
|
||||
NProgress.configure({
|
||||
showSpinner: false,
|
||||
|
|
@ -8,5 +9,9 @@ NProgress.configure({
|
|||
minimum: 0.3,
|
||||
});
|
||||
|
||||
export { NProgress };
|
||||
/**
|
||||
* 作为VUE插件进行初始化
|
||||
*/
|
||||
NProgress.install = function (app: App) {};
|
||||
|
||||
export { NProgress };
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
import 'nprogress';
|
||||
import { App } from 'vue';
|
||||
|
||||
declare module 'nprogress' {
|
||||
interface NProgress {
|
||||
install: (app: App) => void;
|
||||
}
|
||||
}
|
||||
14
src/main.ts
14
src/main.ts
|
|
@ -1,12 +1,18 @@
|
|||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import { router } from './router';
|
||||
import { store } from './store';
|
||||
import { style } from './styles';
|
||||
import App from '@/App.vue';
|
||||
import { router } from '@/router';
|
||||
import { store } from '@/store';
|
||||
import { style } from '@/styles';
|
||||
import { dayjs } from '@/libs/dayjs';
|
||||
import { NProgress } from '@/libs/nprogress';
|
||||
import { api } from '@/api';
|
||||
|
||||
const run = async () => {
|
||||
const app = createApp(App);
|
||||
app.use(dayjs);
|
||||
app.use(NProgress);
|
||||
app.use(store);
|
||||
app.use(api);
|
||||
app.use(style);
|
||||
app.use(router);
|
||||
await router.isReady();
|
||||
|
|
|
|||
|
|
@ -13,9 +13,11 @@ export default defineComponent({
|
|||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
selectedKeys.value = route.matched.map(i => i.path);
|
||||
selectedKeys.value = route.matched.map(i => i.aliasOf?.path ?? i.path);
|
||||
},
|
||||
{ immediate: true }
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function goto(route: MenuItem) {
|
||||
|
|
@ -40,14 +42,26 @@ export default defineComponent({
|
|||
}
|
||||
return (
|
||||
<>
|
||||
<a-menu-item key={route.path} v-slots={{ icon }} onClick={() => goto(route)}>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div>{route.title}</div>
|
||||
<div class="text-xs text-gray-400">
|
||||
{/* <a-badge count={8}>8</a-badge> */}
|
||||
{route.hide === 'prod' ? <a-tag color="red">{'开发'}</a-tag> : null}
|
||||
<a-menu-item key={route.path} v-slots={{ icon }}>
|
||||
{route.link ? (
|
||||
<div class="flex items-center justify-between gap-2" onClick={() => goto(route)}>
|
||||
<div>{route.title}</div>
|
||||
<div class="text-xs text-gray-400">
|
||||
{/* <a-badge count={8}>8</a-badge> */}
|
||||
{route.hide === 'prod' ? <a-tag color="red">{'开发'}</a-tag> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<router-link to={route.path}>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div>{route.title}</div>
|
||||
<div class="text-xs text-gray-400">
|
||||
{/* <a-badge count={8}>8</a-badge> */}
|
||||
{route.hide === 'prod' ? <a-tag color="red">{'开发'}</a-tag> : null}
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
)}
|
||||
</a-menu-item>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,14 +16,16 @@
|
|||
<a-avatar :size="32">
|
||||
<img :src="userStore.avatar || 'https://github.com/juetan.png'" :alt="userStore.nickname" />
|
||||
</a-avatar>
|
||||
<div class="leading-4 my-2">
|
||||
<div class="leading-4 text-base my-2">
|
||||
{{ userStore.nickname }}
|
||||
<span class="text-xs text-gray-400">({{ userStore.username }})</span>
|
||||
<div class="text-xs text-gray-400">管理员</div>
|
||||
<a-tag color="red" size="small" >管理员</a-tag>
|
||||
<div class="text-xs text-gray-400">
|
||||
<span class="text-gray-400">@{{ userStore.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-doption>
|
||||
<a-divider :margin="4"></a-divider>
|
||||
<a-divider :margin="4" class="border-gray-100!"></a-divider>
|
||||
<a-doption @click="open()">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-lock"></i>
|
||||
|
|
@ -36,7 +38,7 @@
|
|||
</template>
|
||||
账号信息
|
||||
</a-doption>
|
||||
<a-divider :margin="4"></a-divider>
|
||||
<!-- <a-divider :margin="4" class="border-gray-100!"></a-divider> -->
|
||||
<a-doption @click="router.push('/user')">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-config"></i>
|
||||
|
|
@ -49,7 +51,7 @@
|
|||
</template>
|
||||
关于
|
||||
</a-doption>
|
||||
<a-divider :margin="4"></a-divider>
|
||||
<a-divider :margin="4" class="border-gray-100!"></a-divider>
|
||||
<a-doption @click="logout">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-power"></i>
|
||||
|
|
@ -62,8 +64,8 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { useFormModal } from '@/components/AnForm';
|
||||
import { useUserStore } from '@/store';
|
||||
import { delConfirm } from '@/utils';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { delConfirm, sleep } from '@/utils';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
|
@ -74,10 +76,13 @@ const logout = async () => {
|
|||
await delConfirm({
|
||||
content: '退出后将跳转到登录页面,确定退出吗?',
|
||||
okText: '确定退出',
|
||||
async onBeforeOk() {
|
||||
await sleep(2000);
|
||||
userStore.clearUser();
|
||||
Message.success('提示:已退出登陆!');
|
||||
router.push({ path: '/login', query: { redirect: route.path } });
|
||||
},
|
||||
});
|
||||
userStore.clearUser();
|
||||
Message.success('提示:已退出登陆!');
|
||||
router.push({ path: '/login', query: { redirect: route.path } });
|
||||
};
|
||||
|
||||
const { component: PasswordModal, open } = useFormModal({
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
<template>
|
||||
<a-layout class="layout">
|
||||
<a-layout-header
|
||||
class="h-13 overflow-hidden flex justify-between items-center gap-4 px-2 pr-4 border-b border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700"
|
||||
>
|
||||
<a-layout-header class="h-13 overflow-hidden flex justify-between items-center gap-4 px-2 pr-4 border-b border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700">
|
||||
<div class="h-13 flex items-center">
|
||||
<!-- <a-button size="small" @click="isCollapsed = !isCollapsed">
|
||||
<template #icon>
|
||||
|
|
@ -11,7 +9,7 @@
|
|||
</a-button> -->
|
||||
<router-link to="/" class="px-2 flex items-center gap-2 text-slate-700">
|
||||
<img src="/favicon.ico" alt="" width="24" height="24" class="" />
|
||||
<h1 class="relative text-[18px] leading-[22px] dark:text-white m-0 p-0 font-semibold">
|
||||
<h1 class="relative text-[18px] leading-[22px] dark:text-white m-0 p-0 font-normal">
|
||||
{{ appStore.title }}
|
||||
<span class="absolute -right-10 -top-1 font-normal text-xs text-gray-400"> v0.0.1 </span>
|
||||
</h1>
|
||||
|
|
@ -19,18 +17,6 @@
|
|||
</router-link>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div>
|
||||
<a-input-search placeholder="搜索菜单/页面" :allow-clear="true"></a-input-search>
|
||||
</div>
|
||||
<a-tooltip content="上传文件">
|
||||
<a-button @click="() => null" class="!bg-transparent !hover:bg-gray-100">
|
||||
<template #icon>
|
||||
<a-badge :count="1" :dot="true">
|
||||
<i class="text-base icon-park-outline-upload-one"></i>
|
||||
</a-badge>
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip v-for="btn in buttons" :key="btn.icon" :content="btn.tooltip">
|
||||
<a-button @click="btn.onClick" class="!bg-transparent !hover:bg-gray-100">
|
||||
<template #icon>
|
||||
|
|
@ -57,21 +43,26 @@
|
|||
<Menu />
|
||||
</a-scrollbar>
|
||||
<template #trigger="{ collapsed }">
|
||||
<i
|
||||
:class="collapsed ? `icon-park-outline-expand-left` : 'icon-park-outline-expand-right'"
|
||||
class="text-gray-400 text-base hover:text-gray-700"
|
||||
></i>
|
||||
<div class="w-full h-full py-1 px-1 flex justify-between items-center gap-2" @click.stop>
|
||||
<div
|
||||
class="inline-block w-10 h-10 h-full rounded flex items-center justify-center hover:bg-zinc-100 text-base text-gray-400"
|
||||
@click="() => (isCollapsed = !isCollapsed)"
|
||||
>
|
||||
<i :class="collapsed ? `icon-park-outline-expand-left` : 'icon-park-outline-expand-right'"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-layout-sider>
|
||||
<a-layout class="layout-content flex-1">
|
||||
<a-layout-content class="overflow-x-auto">
|
||||
<a-spin :loading="appStore.pageLoding" tip="页面加载中,请稍等..." class="block h-full w-full">
|
||||
<a-spin :loading="appStore.pageLoding" class="block h-full w-full">
|
||||
<template #icon>
|
||||
<IconSync></IconSync>
|
||||
<div class="loader"></div>
|
||||
</template>
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :include="menuStore.caches">
|
||||
<component :is="Component"></component>
|
||||
<component v-if="hasAuth" :is="Component"></component>
|
||||
<AnForbidden v-else></AnForbidden>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</a-spin>
|
||||
|
|
@ -82,32 +73,46 @@
|
|||
</template>
|
||||
|
||||
<script lang="tsx" setup>
|
||||
import { useAppStore } from '@/store';
|
||||
import { useAppStore } from '@/store/app';
|
||||
import { useMenuStore } from '@/store/menu';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { IconSync } from '@arco-design/web-vue/es/icon';
|
||||
import { useFullscreen } from '@vueuse/core';
|
||||
import Menu from './Menu.vue';
|
||||
import userDropdown from './UserDropdown.vue';
|
||||
import { useUserStore } from '@/store/user';
|
||||
|
||||
defineOptions({ name: 'LayoutPage' });
|
||||
|
||||
const route = useRoute();
|
||||
const appStore = useAppStore();
|
||||
const menuStore = useMenuStore();
|
||||
const userStore = useUserStore();
|
||||
const isCollapsed = ref(false);
|
||||
const themeConfig = ref({ visible: false });
|
||||
const { toggle, isSupported } = useFullscreen();
|
||||
|
||||
const ButtonWithTooltip = (props: { tooltip: string; icon: string; onClick: any }) => {
|
||||
return (
|
||||
<a-tooltip content={props.tooltip}>
|
||||
<a-button onClick={props.onClick} class="!bg-transparent !hover:bg-gray-100">
|
||||
{{
|
||||
icon: () => <i class={`${props.icon} text-base`}></i>,
|
||||
}}
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
);
|
||||
};
|
||||
const hasAuth = computed(() => {
|
||||
return route.matched.every(item => {
|
||||
const needAuth = item.meta.auth;
|
||||
const userAuth = userStore.auth;
|
||||
if (needAuth?.includes('*')) {
|
||||
return true;
|
||||
}
|
||||
if (!userStore.accessToken && needAuth?.includes('unlogin')) {
|
||||
return true;
|
||||
}
|
||||
if (!userStore.accessToken) {
|
||||
return false;
|
||||
}
|
||||
if (!needAuth) {
|
||||
return true;
|
||||
}
|
||||
if (userAuth.some(i => needAuth.some(j => j === i))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
|
|
@ -118,10 +123,14 @@ const buttons = [
|
|||
},
|
||||
},
|
||||
{
|
||||
icon: 'icon-park-outline-config',
|
||||
tooltip: '设置',
|
||||
icon: 'icon-park-outline-full-screen',
|
||||
tooltip: '全屏',
|
||||
onClick: () => {
|
||||
themeConfig.value.visible = true;
|
||||
if (!isSupported) {
|
||||
Message.info('您的浏览器不支持全屏');
|
||||
return;
|
||||
}
|
||||
toggle();
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -131,6 +140,13 @@ const buttons = [
|
|||
window.open('https://github.com/appnify/starter-vue', '_blank');
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'icon-park-outline-config',
|
||||
tooltip: '设置',
|
||||
onClick: () => {
|
||||
themeConfig.value.visible = true;
|
||||
},
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
|
|
@ -196,16 +212,40 @@ const buttons = [
|
|||
background-color: #e4ebf1;
|
||||
transition: padding 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
|
||||
}
|
||||
|
||||
/* HTML: <div class="loader"></div> */
|
||||
.loader {
|
||||
width: 120px;
|
||||
height: 16px;
|
||||
border-radius: 20px;
|
||||
color: #222;
|
||||
border: 2px solid;
|
||||
position: relative;
|
||||
}
|
||||
.loader::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
margin: 2px;
|
||||
inset: 0 100% 0 0;
|
||||
border-radius: inherit;
|
||||
background: currentColor;
|
||||
animation: l6 2s infinite;
|
||||
}
|
||||
@keyframes l6 {
|
||||
100% {
|
||||
inset: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"redirect": "/",
|
||||
"meta": {
|
||||
"name": "LayoutPage",
|
||||
"sort": 101,
|
||||
"title": "首页",
|
||||
"icon": "icon-park-outline-home",
|
||||
"keepAlive": true
|
||||
"icon": "icon-park-outline-home"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
</div>
|
||||
<div class="flex items-center justify-center w-full overflow-hidden">
|
||||
<div
|
||||
class="login-box w-[960px] h-[560px] relative mx-4 grid md:grid-cols-2 rounded overflow-hidden border border-blue-100"
|
||||
class="login-box w-[960px] h-[560px] relative mx-4 grid md:grid-cols-2 rounded-lg overflow-hidden border border-blue-100"
|
||||
>
|
||||
<div
|
||||
class="login-left relative hidden md:block w-full h-full overflow-hidden bg-[rgb(var(--primary-6))] px-4"
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
<a-link @click="onForgetPassword">忘记密码?</a-link>
|
||||
</div>
|
||||
<a-button type="primary" html-type="submit" long class="mt-2" :loading="loading" @click="onSubmitForm">
|
||||
{{ loading ? "登陆中" : "立即登录" }}
|
||||
{{ loading ? '登陆中' : '立即登录' }}
|
||||
</a-button>
|
||||
<p type="text" long class="text-gray-400 text-center m-0">暂不支持其他方式登录</p>
|
||||
</a-space>
|
||||
|
|
@ -55,18 +55,19 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { api } from "@/api";
|
||||
import { dayjs } from "@/libs/dayjs";
|
||||
import { useAppStore, useUserStore } from "@/store";
|
||||
import { FieldRule, Form, Message, Modal, Notification } from "@arco-design/web-vue";
|
||||
import { reactive } from "vue";
|
||||
import { api } from '@/api';
|
||||
import { dayjs } from '@/libs/dayjs';
|
||||
import { useAppStore } from '@/store/app';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { FieldRule, Form, Message, Modal, Notification } from '@arco-design/web-vue';
|
||||
import { reactive } from 'vue';
|
||||
|
||||
defineOptions({ name: "LoginPage" });
|
||||
defineOptions({ name: 'LoginPage' });
|
||||
|
||||
const meridiem = dayjs.localeData().meridiem(dayjs().hour(), dayjs().minute());
|
||||
const appStore = useAppStore();
|
||||
const userStore = useUserStore();
|
||||
const model = reactive({ username: "", password: "" });
|
||||
const model = reactive({ username: '', password: '' });
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
|
|
@ -76,22 +77,22 @@ const formRules: Record<string, FieldRule[]> = {
|
|||
username: [
|
||||
{
|
||||
required: true,
|
||||
message: "请输入账号/手机号/邮箱",
|
||||
message: '请输入账号/手机号/邮箱',
|
||||
},
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: "请输入密码",
|
||||
message: '请输入密码',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const onForgetPassword = () => {
|
||||
Modal.info({
|
||||
title: "忘记密码?",
|
||||
content: "如已忘记密码,请联系管理员进行密码重置!",
|
||||
modalClass: "text-center",
|
||||
title: '忘记密码?',
|
||||
content: '如已忘记密码,请联系管理员进行密码重置!',
|
||||
modalClass: 'text-center',
|
||||
maskClosable: false,
|
||||
});
|
||||
};
|
||||
|
|
@ -105,10 +106,10 @@ const onSubmitForm = async () => {
|
|||
const res = await api.auth.login(model);
|
||||
userStore.setAccessToken(res.data.data);
|
||||
Notification.success({
|
||||
title: "提示",
|
||||
title: '提示',
|
||||
content: `${meridiem}好,您已成功登陆本系统!`,
|
||||
});
|
||||
router.push({ path: (route.query.redirect as string) || "/" });
|
||||
router.push({ path: (route.query.redirect as string) || '/' });
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data?.message;
|
||||
message && Message.warning(`提示:${message}`);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const { component: CategoryTable } = useTable({
|
|||
<div class="flex flex-col overflow-hidden">
|
||||
<span>
|
||||
{record.name}
|
||||
<span class="text-gray-400 text-xs truncate ml-2">@{record.code}</span>
|
||||
<span class="text-orange-500 truncate ml-2">@{record.code}</span>
|
||||
</span>
|
||||
<div class="text-gray-400 text-xs truncate mt-0.5">{record.description}</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,16 +5,8 @@
|
|||
</template>
|
||||
上传
|
||||
</a-button>
|
||||
<a-modal
|
||||
title="上传文件"
|
||||
title-align="start"
|
||||
v-model:visible="visible"
|
||||
:width="960"
|
||||
:mask-closable="false"
|
||||
:on-before-cancel="onBeforeCancel"
|
||||
@close="onClose"
|
||||
>
|
||||
<div class="flex items-center gap-4 py-0">
|
||||
<a-modal class="an-upload" title="上传文件" title-align="start" v-model:visible="visible" :width="960" :mask-closable="false" :on-before-cancel="onBeforeCancel" @close="onClose">
|
||||
<div class="flex items-center justify-between gap-4 py-0">
|
||||
<a-upload
|
||||
ref="uploadRef"
|
||||
class="upload"
|
||||
|
|
@ -27,7 +19,7 @@
|
|||
@error="onUploadError"
|
||||
>
|
||||
<template #upload-button>
|
||||
<a-button type="primary">
|
||||
<a-button type="outline">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-upload-one"></i>
|
||||
</template>
|
||||
|
|
@ -35,8 +27,8 @@
|
|||
</a-button>
|
||||
</template>
|
||||
</a-upload>
|
||||
<div class="flex-1 flex items-center text-gray-400">
|
||||
归类为:
|
||||
<div class="flex items-center text-gray-400">
|
||||
已选择 {{ fileList.length }} 项,归类为:
|
||||
<span>
|
||||
<a-select v-model="group" :bordered="false" :options="groupOptions"></a-select>
|
||||
</span>
|
||||
|
|
@ -59,14 +51,12 @@
|
|||
<div v-show="item.status !== 'done'">
|
||||
<a-link v-show="item.status === 'uploading'" @click="pauseItem(item)"> 停止 </a-link>
|
||||
<a-link v-show="item.status === 'error'" @click="retryItem(item)"> 重试 </a-link>
|
||||
<a-link v-show="item.status === 'init' || item.status === 'error'" @click="removeItem(item)">
|
||||
移除
|
||||
</a-link>
|
||||
<a-link v-show="item.status === 'init' || item.status === 'error'" @click="removeItem(item)"> 移除 </a-link>
|
||||
</div>
|
||||
</div>
|
||||
<a-progress :percent="formatProgress(item, true)" :show-text="false" class="block!"></a-progress>
|
||||
<a-progress :percent="formatProgress(item, true)" :show-text="false" class="block! mt-0.5"></a-progress>
|
||||
<div class="flex items-center justify-between gap-2 text-gray-400 mt-1.5 text-xs">
|
||||
<span class="text-xs">
|
||||
<!-- <span class="text-xs">
|
||||
<span v-if="item.status === 'init'">
|
||||
<i class="icon-park-outline-lightning"></i>
|
||||
等待上传
|
||||
|
|
@ -86,14 +76,10 @@
|
|||
</span>
|
||||
<span>
|
||||
<span v-if="item.status === 'init'"> </span>
|
||||
<span v-else-if="item.status === 'uploading'">
|
||||
速度:{{ formatSpeed(item.uid) }}/s, 进度:{{ formatProgress(item) }} %
|
||||
</span>
|
||||
<span v-else-if="item.status === 'done'">
|
||||
耗时:{{ fileMap.get(item.uid)?.cost || 0 }} 秒, 平均:{{ formatAspeed(item.uid) }}/s
|
||||
</span>
|
||||
<span v-else-if="item.status === 'uploading'"> 速度:{{ formatSpeed(item.uid) }}/s, 进度:{{ formatProgress(item) }} % </span>
|
||||
<span v-else-if="item.status === 'done'"> 耗时:{{ fileMap.get(item.uid)?.cost || 0 }} 秒, 平均:{{ formatAspeed(item.uid) }}/s </span>
|
||||
<span v-else="item.status === 'error'"> 原因:{{ fileMap.get(item.uid)?.error }} </span>
|
||||
</span>
|
||||
</span> -->
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
|
@ -106,14 +92,10 @@
|
|||
|
||||
<template #footer>
|
||||
<div class="flex justify-between gap-2 items-center">
|
||||
<div class="text-gray-400">已上传 {{ stat.doneCount }}/{{ fileList.length }} 项</div>
|
||||
<div class="text-gray-400"></div>
|
||||
<div class="space-x-2">
|
||||
<a-button type="text" :disabled="!fileList.length || Boolean(stat.uploadingCount)" @click="clearUploaded">
|
||||
清空
|
||||
</a-button>
|
||||
<a-button type="primary" :disabled="!fileList.length || !stat.initCount" @click="startUpload">
|
||||
开始上传
|
||||
</a-button>
|
||||
<a-button type="text" :disabled="!fileList.length || Boolean(stat.uploadingCount)" @click="clearUploaded"> 清空 </a-button>
|
||||
<a-button type="primary" :disabled="!fileList.length || !stat.initCount" @click="startUpload"> 开始上传 </a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -306,4 +288,10 @@ const groupOptions = [
|
|||
];
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
<style lang="less">
|
||||
.an-upload {
|
||||
.arco-modal-body {
|
||||
padding-top: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -25,14 +25,14 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { FileCategory, api } from '@/api';
|
||||
import { useTable, useTableDelete, useUpdateColumn } from '@/components/AnTable';
|
||||
import { FileTypes } from '@/constants/file';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import numeral from 'numeral';
|
||||
import AnCategory from './AnCategory.vue';
|
||||
import AnPreview from './AnPreview.vue';
|
||||
import AnUpload from './AnUpload.vue';
|
||||
import { FileCategory, api } from '@/api';
|
||||
import { useCreateColumn, useTable, useTableDelete, useUpdateColumn } from '@/components/AnTable';
|
||||
import { FileTypes } from '@/constants/file';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { getIcon } from './util';
|
||||
|
||||
const current = ref<FileCategory>();
|
||||
|
|
@ -116,7 +116,7 @@ const {
|
|||
width: 150,
|
||||
render: ({ record }) => numeral(record.size).format('0 b'),
|
||||
},
|
||||
useCreateColumn(),
|
||||
// useCreateColumn(),
|
||||
useUpdateColumn(),
|
||||
{
|
||||
type: 'button',
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,59 +0,0 @@
|
|||
<template>
|
||||
<div ref="editorRef" class="w-full h-full border"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as monaco from "monaco-editor";
|
||||
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
|
||||
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
|
||||
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
|
||||
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
|
||||
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
|
||||
|
||||
self.MonacoEnvironment = {
|
||||
getWorker(_, label) {
|
||||
if (label === "json") {
|
||||
return new jsonWorker();
|
||||
}
|
||||
if (label === "css" || label === "scss" || label === "less") {
|
||||
return new cssWorker();
|
||||
}
|
||||
if (label === "html" || label === "handlebars" || label === "razor") {
|
||||
return new htmlWorker();
|
||||
}
|
||||
if (label === "typescript" || label === "javascript") {
|
||||
return new tsWorker();
|
||||
}
|
||||
return new editorWorker();
|
||||
},
|
||||
};
|
||||
|
||||
let editor: monaco.editor.IStandaloneCodeEditor | undefined;
|
||||
const editorRef = ref<HTMLElement | null>(null);
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
if (editorRef.value) {
|
||||
editor = monaco.editor.create(editorRef.value, {
|
||||
value: "",
|
||||
language: "html",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.content,
|
||||
(value) => {
|
||||
editor?.setValue(value);
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
<template>
|
||||
<bread-page>
|
||||
<div class="h-full grid grid-cols-[1fr_auto_1fr]">
|
||||
<div>
|
||||
<a-tabs @change="onChange">
|
||||
<a-tab-pane v-for="tag in tags" :key="tag.name" :title="tag.description">
|
||||
<a-form :model="{}" layout="vertical">
|
||||
<a-form-item label="新增接口">
|
||||
<a-radio-group type="button" v-model="type.create">
|
||||
<a-radio
|
||||
v-for="route in routes.filter(i => i.tag === tag.name)"
|
||||
:value="route.operationId"
|
||||
:key="route.path"
|
||||
>
|
||||
{{ route.description }}
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="修改接口">
|
||||
<a-radio-group type="button" v-model="type.modify">
|
||||
<a-radio
|
||||
v-for="route in routes.filter(i => i.tag === tag.name)"
|
||||
:value="route.operationId"
|
||||
:key="route.path"
|
||||
>
|
||||
{{ route.description }}
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="查询接口">
|
||||
<a-radio-group type="button" v-model="type.select">
|
||||
<a-radio
|
||||
v-for="route in routes.filter(i => i.tag === tag.name)"
|
||||
:value="route.operationId"
|
||||
:key="route.path"
|
||||
>
|
||||
{{ route.description }}
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="删除接口">
|
||||
<a-radio-group type="button" v-model="type.delete">
|
||||
<a-radio
|
||||
v-for="route in routes.filter(i => i.tag === tag.name)"
|
||||
:value="route.operationId"
|
||||
:key="route.path"
|
||||
>
|
||||
{{ route.description }}
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
<a-divider direction="vertical"></a-divider>
|
||||
<div class="h-full grid grid-rows-[auto_1fr] gap-2">
|
||||
<div>
|
||||
<a-button type="primary" @click="onOpen">确定</a-button>
|
||||
</div>
|
||||
<editor-modal class="bg-gray-100" :content="content"></editor-modal>
|
||||
</div>
|
||||
</div>
|
||||
</bread-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ejs from 'ejs';
|
||||
import doc from './data.json';
|
||||
import editorModal from './editor.vue';
|
||||
import template from './page.ejs?raw';
|
||||
|
||||
const content = ref('');
|
||||
const { tags, routes } = doc;
|
||||
const type = ref({
|
||||
create: undefined,
|
||||
select: undefined,
|
||||
modify: undefined,
|
||||
delete: undefined,
|
||||
});
|
||||
|
||||
const onChange = (value: string | number) => {
|
||||
console.log(value);
|
||||
};
|
||||
|
||||
const onOpen = () => {
|
||||
const data = {
|
||||
tag: '',
|
||||
operationId: '',
|
||||
create: {},
|
||||
select: {},
|
||||
modify: {},
|
||||
delete: {},
|
||||
};
|
||||
for (const route of doc.routes) {
|
||||
if (route.operationId === type.value.create) {
|
||||
data.create = route;
|
||||
}
|
||||
if (route.operationId === type.value.select) {
|
||||
data.select = route;
|
||||
}
|
||||
if (route.operationId === type.value.modify) {
|
||||
data.modify = route;
|
||||
}
|
||||
if (route.operationId === type.value.delete) {
|
||||
data.delete = route;
|
||||
}
|
||||
}
|
||||
content.value = ejs.render(template, data);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"meta": {
|
||||
"sort": 20010,
|
||||
"hide": "prod",
|
||||
"title": "接口生成",
|
||||
"icon": "icon-park-outline-code"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
<template>
|
||||
<BreadPage>
|
||||
<ani-table> </ani-table>
|
||||
</BreadPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { api } from "@/api";
|
||||
import { createColumn, updateColumn, useAniTable } from "@/components";
|
||||
|
||||
const [aniTable, aniCtx] = useAniTable({
|
||||
data: async (model, paging) => {
|
||||
return api.<%= select.tag %>.<%= operationId %>({ ...model, ...paging });
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
title: "用户描述",
|
||||
dataIndex: "description",
|
||||
},
|
||||
createColumn,
|
||||
updateColumn,
|
||||
{
|
||||
title: "操作",
|
||||
type: "button",
|
||||
width: 180,
|
||||
buttons: [
|
||||
{
|
||||
type: "modify",
|
||||
text: "修改",
|
||||
},
|
||||
{
|
||||
type: "delete",
|
||||
text: "删除",
|
||||
onClick: async ({ record }) => {
|
||||
return api.<%= tag %>.<%= operationId %>(record.id);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
search: {
|
||||
items: [
|
||||
{
|
||||
extend: "nickname",
|
||||
required: false,
|
||||
type: 'search',
|
||||
enableLoad: true,
|
||||
itemProps: {
|
||||
hideLabel: true,
|
||||
},
|
||||
nodeProps: {
|
||||
placeholder: "用户昵称",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
create: {
|
||||
title: "新建用户",
|
||||
modalProps: {
|
||||
width: 732,
|
||||
maskClosable: false,
|
||||
},
|
||||
formProps: {
|
||||
layout: "vertical",
|
||||
class: "!grid grid-cols-2 gap-x-6",
|
||||
},
|
||||
items: [
|
||||
<%_ for(const item of create.bodyParams) { _%>
|
||||
{
|
||||
field: "<%= item.name %>",
|
||||
label: "<%= item.description %>",
|
||||
type: "<%= item.type %>",
|
||||
required: <%= item.required %>,
|
||||
},
|
||||
<%_ } _%>
|
||||
],
|
||||
submit: ({ model }) => {
|
||||
return api.<%= create.tag %>.<%= create.operationId %>(model);
|
||||
},
|
||||
},
|
||||
modify: {
|
||||
extend: true,
|
||||
title: "修改用户",
|
||||
submit: ({ model }) => {
|
||||
return api.<%= modify.tag %>.<%= modify.operationId %>(model.id, model);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
<%_ if(false) { _%>
|
||||
<route lang="json">
|
||||
{
|
||||
"meta": {
|
||||
"sort": 10301,
|
||||
"title": "用户管理",
|
||||
"icon": "icon-park-outline-user"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
<%_ } _%>
|
||||
|
|
@ -1,15 +1,13 @@
|
|||
<template>
|
||||
<div></div>
|
||||
<iframe src="https://nav.juetan.cn" frameborder="0" class="w-full h-full overflow-hidden"></iframe>
|
||||
</template>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"component": null,
|
||||
"meta": {
|
||||
"sort": 120012,
|
||||
"hide": "prod",
|
||||
"title": "前端导航",
|
||||
"link": "https://nav.juetan.cn",
|
||||
"icon": "icon-park-outline-mail"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white px-5 py-4 rounded-sm mt-4">
|
||||
<div>常用服务</div>
|
||||
<div class="grid grid-cols-5 justify-between gap-4 mt-4">
|
||||
|
|
@ -58,7 +59,7 @@
|
|||
<ul class="list-none w-full m-0 p-0">
|
||||
<li v-for="i in 8" class="w-full h-6 items-center overflow-hidden justify-between flex gap-2 mb-2">
|
||||
<a-tag>{{ i }}</a-tag>
|
||||
<span class="flex-1 truncate hover:underline underline-offset-2 cursor-pointer">
|
||||
<span class="flex-1 truncate hover:underline underline-offset-2 hover:text-brand-500 cursor-pointer">
|
||||
但是预测已加载的数据不足以
|
||||
</span>
|
||||
<span class="text-gray-400">3天前</span>
|
||||
|
|
@ -71,7 +72,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from '@/store';
|
||||
import { useUserStore } from '@/store/user';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
|
|
@ -108,9 +109,10 @@ const stat = {
|
|||
|
||||
<route lang="json">
|
||||
{
|
||||
"alias": "/",
|
||||
"meta": {
|
||||
"sort": 1000,
|
||||
"title": "首页",
|
||||
"title": "概览",
|
||||
"icon": "icon-park-outline-home"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
<template>
|
||||
<bread-page>
|
||||
<a-form :model="{}" :label-col-props="{ span: 3 }" label-align="left" layout="vertical" class="space-y-6">
|
||||
<div>
|
||||
<h2 class="m-0 text-base">常规设置</h2>
|
||||
<p class="text-gray-500 mt-1">首次为你的帐户添加密码时,你需要前往密码重置页面,以便我们验证你的身份。</p>
|
||||
</div>
|
||||
<a-form :model="{}" label-align="left" class="space-y-6 mt-6 col-form divide-y">
|
||||
<a-form-item label="站点LOGO">
|
||||
<a-avatar :size="64">
|
||||
<img :src="appStore.logo" alt="" />
|
||||
|
|
@ -10,7 +14,7 @@
|
|||
</a-avatar>
|
||||
<template #help>提示:仅支持 5MB 以内大小, png 或 jpg 格式的图片 </template>
|
||||
</a-form-item>
|
||||
<a-form-item label="站点名称">
|
||||
<a-form-item label="站点名称" class="pt-6">
|
||||
<a-input
|
||||
v-model="appStore.title"
|
||||
placeholder="请输入"
|
||||
|
|
@ -21,7 +25,7 @@
|
|||
></a-input>
|
||||
<template #help> 用作系统内显示的名称,可在后台修改 </template>
|
||||
</a-form-item>
|
||||
<a-form-item label="站点描述">
|
||||
<a-form-item label="站点描述" class="pt-6">
|
||||
<a-textarea
|
||||
v-model="appStore.subtitle"
|
||||
placeholder="请输入"
|
||||
|
|
@ -31,11 +35,11 @@
|
|||
></a-textarea>
|
||||
<template #help> 启用后,消息通知将在左上角进行提示. </template>
|
||||
</a-form-item>
|
||||
<a-form-item label="站点URL">
|
||||
<a-form-item label="站点URL" class="pt-6">
|
||||
<a-input v-model="appStore.title" placeholder="请输入" class="!w-[432px]" allow-clear></a-input>
|
||||
<template #help> 示例:https://www.juetan.cn。用于静态资源前缀、应用接口前缀等用途。 </template>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-form-item class="pt-6">
|
||||
<a-button type="primary">保存修改</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
|
|
|||
|
|
@ -2,10 +2,6 @@
|
|||
<AnPage></AnPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx"></script>
|
||||
|
||||
<style lang="less"></style>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"redirect": "/setting/common",
|
||||
|
|
|
|||
|
|
@ -1,67 +1,68 @@
|
|||
<template>
|
||||
<bread-page>
|
||||
<div class="flex">
|
||||
<a-form
|
||||
:model="{}"
|
||||
:label-col-props="{ span: 3 }"
|
||||
:disabled="!mail.enable"
|
||||
layout="vertical"
|
||||
label-align="left"
|
||||
class="w-[580px]! space-y-6"
|
||||
>
|
||||
<a-form-item label="是否启用" :disabled="false">
|
||||
<a-radio-group v-model="mail.enable">
|
||||
<a-radio :value="true">启用</a-radio>
|
||||
<a-radio :value="false">禁用</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="服务器和端口">
|
||||
<a-input v-model="mail.smtpHost" allow-clear placeholder="请输入" class="!w-[314px]"></a-input>
|
||||
<span class="inline-block px-2">:</span>
|
||||
<a-input-number v-model="mail.smtpPort" :min="0" :max="65535" class="w-24!"></a-input-number>
|
||||
<template #help>
|
||||
示例: smtp.163.com:25。国内常见有
|
||||
<a target="_blank" class="mr-2" href="https://mail.163.com">网易邮箱</a>
|
||||
<a target="_blank" class="mr-2" href="http://mail.aliyun.com/">阿里邮箱</a>
|
||||
<a target="_blank" class="mr-2" href="https://mail.qq.com">QQ邮箱</a>等。
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item label="发信人地址">
|
||||
<a-input v-model="mail.smtpFrom" placeholder="请输入" class="!w-[432px]"></a-input>
|
||||
<template #help> 示例: example@mail.com。仅作为发送邮件时的发送人标识,与登陆无关。</template>
|
||||
</a-form-item>
|
||||
<a-form-item label="是否需要验证">
|
||||
<a-radio-group v-model="mail.smtpAuth">
|
||||
<a-radio :value="true">是</a-radio>
|
||||
<a-radio :value="false">否</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="验证账号">
|
||||
<a-input
|
||||
:disabled="!mail.enable || !mail.smtpAuth"
|
||||
v-model="mail.smtpUser"
|
||||
placeholder="请输入"
|
||||
class="!w-[432px]"
|
||||
></a-input>
|
||||
<template #help> 示例: example@mail.com。企业邮箱请使用企业域名后缀。</template>
|
||||
</a-form-item>
|
||||
<a-form-item label="验证密码">
|
||||
<a-input
|
||||
:disabled="!mail.enable || !mail.smtpAuth"
|
||||
v-model="mail.smtpPass"
|
||||
placeholder="请输入"
|
||||
class="!w-[432px]"
|
||||
></a-input>
|
||||
<template #help> 示例:AATOLARFABJKYWUY。具体请在对应邮箱设置面板进行生成。 </template>
|
||||
</a-form-item>
|
||||
<a-form-item :disabled="false">
|
||||
<a-button type="primary"> 保存修改 </a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider direction="vertical" :margin="32"></a-divider>
|
||||
<div class="w-full">
|
||||
<div class="flex item-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-normal flex items-center gap-2">
|
||||
邮件设置
|
||||
<a-tag :color="mail.enable ? 'green' : 'red'">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-check-one"></i>
|
||||
{{ mail.enable ? '已启用' : '已停用' }}
|
||||
</template>
|
||||
</a-tag>
|
||||
</h2>
|
||||
<p class="text-gray-500 mt-1.5 p-0 m0 m-0">首次为你的帐户添加密码时,你需要前往密码重置页面,以便我们验证你的身份。</p>
|
||||
</div>
|
||||
<div class="flex items-center pr-6">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<a-form :model="{}" :disabled="!mail.enable" label-align="left" class="col-form divide-y divide-gray-100 mt-8 space-y-6">
|
||||
<a-form-item label="是否启用" :disabled="false">
|
||||
<a-switch v-model="mail.enable"> </a-switch>
|
||||
<template #help> 启用后其他服务可发送邮件通知。 </template>
|
||||
</a-form-item>
|
||||
<a-form-item label="服务器地址和端口" class="pt-6">
|
||||
<a-input v-model="mail.smtpHost" allow-clear placeholder="请输入" class="!w-[314px]"></a-input>
|
||||
<span class="inline-block px-2">:</span>
|
||||
<a-input-number v-model="mail.smtpPort" :min="0" :max="65535" class="w-24!"></a-input-number>
|
||||
<template #help>
|
||||
示例: smtp.163.com:25。国内常见有
|
||||
<a target="_blank" class="mr-2" href="https://mail.163.com">网易邮箱</a>
|
||||
<a target="_blank" class="mr-2" href="http://mail.aliyun.com/">阿里邮箱</a>
|
||||
<a target="_blank" class="mr-2" href="https://mail.qq.com">QQ邮箱</a>等。
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item label="发信人地址" class="pt-6">
|
||||
<a-input v-model="mail.smtpFrom" placeholder="请输入" class="!w-[432px]"></a-input>
|
||||
<template #help> 示例: example@mail.com。仅作为发送邮件时的发送人标识,与登陆无关。</template>
|
||||
</a-form-item>
|
||||
<a-form-item label="是否需要验证" class="pt-6">
|
||||
<a-switch v-model="mail.smtpAuth"> </a-switch>
|
||||
<template #help> 可选 </template>
|
||||
</a-form-item>
|
||||
<a-form-item label="验证账号" class="pt-6">
|
||||
<a-input :disabled="!mail.enable || !mail.smtpAuth" v-model="mail.smtpUser" placeholder="请输入" class="!w-[432px]"></a-input>
|
||||
<template #help> 示例: example@mail.com。企业邮箱请使用企业域名后缀。</template>
|
||||
</a-form-item>
|
||||
<a-form-item label="验证密码" class="pt-6">
|
||||
<a-input :disabled="!mail.enable || !mail.smtpAuth" v-model="mail.smtpPass" placeholder="请输入" class="!w-[432px]"></a-input>
|
||||
<template #help> 示例:AATOLARFABJKYWUY。具体请在对应邮箱设置面板进行生成。 </template>
|
||||
</a-form-item>
|
||||
<a-form-item :disabled="false" class="pt-6">
|
||||
<a-button type="primary"> 保存修改 </a-button>
|
||||
<a-button class="ml-4">
|
||||
测试
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
<!-- <a-divider direction="vertical" :margin="32"></a-divider>
|
||||
<div class="flex-1">
|
||||
<div>
|
||||
<div class="text-base font-semibold">配置测试</div>
|
||||
<div class="text-lg font-normal">邮件测试</div>
|
||||
<div class="text-gray-400 mt-1">发送一封测试邮件,检测邮件设置是否能正常工作。</div>
|
||||
<div class="mt-6">
|
||||
<a-input placeholder="接收人邮箱" class="w-[432px]!"></a-input>
|
||||
|
|
@ -71,7 +72,7 @@
|
|||
<a-button type="primary" :disabled="!mail.enable">发送邮件</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</bread-page>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<script setup lang="tsx">
|
||||
import { api } from '@/api';
|
||||
import { useFormModal } from '@/components/AnForm';
|
||||
import { TableColumnRender, useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||
import { TableColumnRender, useCreateColumn, useTable } from '@/components/AnTable';
|
||||
|
||||
defineOptions({ name: 'SystemDepartmentPage' });
|
||||
|
||||
|
|
@ -64,9 +64,6 @@ const { component: UserTable } = useTable({
|
|||
{
|
||||
...useCreateColumn(),
|
||||
},
|
||||
{
|
||||
...useUpdateColumn(),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
type: 'button',
|
||||
|
|
@ -173,7 +170,6 @@ const { component: UserTable } = useTable({
|
|||
{
|
||||
"meta": {
|
||||
"name": "SystemDepartmentPage",
|
||||
"keepAlive": true,
|
||||
"sort": 10301,
|
||||
"title": "部门管理",
|
||||
"icon": "icon-park-outline-group"
|
||||
|
|
|
|||
|
|
@ -8,12 +8,16 @@
|
|||
<an-group :current="current" @change="onTypeChange"></an-group>
|
||||
</div>
|
||||
<div class="bg-white p-4">
|
||||
<div :show-icon="false" class="rounded mb-3 bg-gray-200 px-4 py-3">
|
||||
<div :show-icon="false" class="rounded mb-3 bg-gray-100 px-4 py-3">
|
||||
<span class="text-base">
|
||||
<i class="icon-park-outline-folder-close"></i>
|
||||
{{ current?.name }}
|
||||
</span>
|
||||
<div class="mt-1.5 text-gray-500">描述:{{ current?.description }}</div>
|
||||
<div class="mt-2 flex gap-1">
|
||||
<a-link>修改</a-link>
|
||||
<a-link status="danger">删除</a-link>
|
||||
</div>
|
||||
</div>
|
||||
<dict-table></dict-table>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<BreadPage>
|
||||
<role-table></role-table>
|
||||
<RoleTable></RoleTable>
|
||||
</BreadPage>
|
||||
</template>
|
||||
|
||||
|
|
@ -106,6 +106,7 @@ const { component: RoleTable } = useTable({
|
|||
"name": "SystemRolePage",
|
||||
"sort": 10302,
|
||||
"title": "角色管理",
|
||||
"auth": ["role"],
|
||||
"icon": "icon-park-outline-shield"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<script setup lang="tsx">
|
||||
import { api } from '@/api';
|
||||
import { useFormModal } from '@/components/AnForm';
|
||||
import { TableColumnRender, useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||
import { TableColumnRender, useTable } from '@/components/AnTable';
|
||||
|
||||
defineOptions({ name: 'SystemUserPage' });
|
||||
|
||||
|
|
@ -32,24 +32,14 @@ const { component: PasswordModal, open } = useFormModal({
|
|||
|
||||
const usernameRender: TableColumnRender = ({ record }) => (
|
||||
<div class="flex items-center gap-4 w-full overflow-hidden">
|
||||
<a-avatar size={32}>
|
||||
<a-avatar size={32} class="bg-brand-500!">
|
||||
{record.avatar?.startsWith('/') ? <img src={record.avatar} alt="" /> : record.nickname?.[0]}
|
||||
</a-avatar>
|
||||
<div class="w-full flex-1 overflow-hidden">
|
||||
<div>
|
||||
<span class="cursor-pointer hover:text-brand-500">{record.nickname}</span>
|
||||
<span class="text-gray-400 text-xs truncate ml-2">@{record.username}</span>
|
||||
</div>
|
||||
<div class="w-full text-gray-400 space-x-4 text-xs">
|
||||
<span>
|
||||
<i class="icon-park-outline-mail mr-1 align-[-4px]"></i>
|
||||
contact@juetan.cn
|
||||
</span>
|
||||
<span>
|
||||
<i class="icon-park-outline-phone-telephone mr-1"></i>
|
||||
1591234568
|
||||
</span>
|
||||
<span class="cursor-pointer ">{record.nickname}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -62,16 +52,19 @@ const { component: UserTable } = useTable({
|
|||
render: usernameRender,
|
||||
},
|
||||
{
|
||||
...useCreateColumn(),
|
||||
},
|
||||
{
|
||||
...useUpdateColumn(),
|
||||
title: '创建',
|
||||
render: () => '3 天前'
|
||||
},
|
||||
// {
|
||||
// ...useCreateColumn(),
|
||||
// },
|
||||
// {
|
||||
// ...useUpdateColumn(),
|
||||
// },
|
||||
{
|
||||
title: '操作',
|
||||
type: 'button',
|
||||
width: 200,
|
||||
align: 'right',
|
||||
buttons: [
|
||||
{
|
||||
text: '重置密码',
|
||||
|
|
@ -176,7 +169,6 @@ const { component: UserTable } = useTable({
|
|||
"cache": true,
|
||||
"sort": 10301,
|
||||
"title": "用户管理",
|
||||
"auth": ["*"],
|
||||
"icon": "icon-park-outline-user"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
import { api } from '@/api';
|
||||
import { env } from '@/config/env';
|
||||
import { store, useUserStore } from '@/store';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useMenuStore } from '@/store/menu';
|
||||
import { treeEach, treeFilter, treeFind } from '@/utils/listToTree';
|
||||
import { store } from '@/store';
|
||||
import { treeEach } from '@/utils/listToTree';
|
||||
import { Notification } from '@arco-design/web-vue';
|
||||
import { Router } from 'vue-router';
|
||||
import { menus } from '../menus';
|
||||
import { APP_HOME_NAME } from '../routes/base';
|
||||
import { APP_ROUTE_NAME, routes } from '../routes/page';
|
||||
import { appRoutes } from '../routes/page';
|
||||
|
||||
/**
|
||||
* 权限守卫
|
||||
* @param to 路由
|
||||
* @description store不能放在外面,否则 pinia-plugin-peristedstate 插件会失效
|
||||
* @returns
|
||||
*/
|
||||
export function useAuthGuard(router: Router) {
|
||||
api.expireHandler = () => {
|
||||
|
|
@ -39,17 +38,17 @@ export function useAuthGuard(router: Router) {
|
|||
return true;
|
||||
}
|
||||
|
||||
// 直接访问跳转回首页(非路由跳转)
|
||||
if (!from.matched.length) {
|
||||
return '/';
|
||||
}
|
||||
|
||||
// 提示已登陆
|
||||
Notification.warning({
|
||||
title: '跳转提示',
|
||||
content: `您已登陆,如需重新登陆请退出后再操作!`,
|
||||
});
|
||||
|
||||
// 直接访问跳转回首页(不是从路由跳转)
|
||||
if (!from.matched.length) {
|
||||
return '/';
|
||||
}
|
||||
|
||||
// 已登陆不允许
|
||||
return false;
|
||||
}
|
||||
|
|
@ -64,37 +63,24 @@ export function useAuthGuard(router: Router) {
|
|||
|
||||
// 未获取权限进行获取
|
||||
if (!menuStore.menus.length) {
|
||||
// 菜单处理
|
||||
const authMenus = treeFilter(menus, item => {
|
||||
if (item.path === env.homePath) {
|
||||
item.path = '/';
|
||||
}
|
||||
return true;
|
||||
});
|
||||
menuStore.setMenus(authMenus);
|
||||
menuStore.setMenus(menus);
|
||||
menuStore.setHome(env.homePath);
|
||||
|
||||
// 路由处理
|
||||
for (const route of routes) {
|
||||
router.addRoute(route);
|
||||
}
|
||||
|
||||
// 缓存处理
|
||||
treeEach(routes, (item, level) => {
|
||||
treeEach(appRoutes, item => {
|
||||
const { cache, name } = item.meta ?? {};
|
||||
if (cache && name) {
|
||||
menuStore.caches.push(name);
|
||||
}
|
||||
// if (item.path === menuStore.home) {
|
||||
// item.alias = '/';
|
||||
// }
|
||||
// if (!router.hasRoute(item.name!)) {
|
||||
// const route = { ...item, children: undefined } as any;
|
||||
// router.addRoute(route.parentName!, route);
|
||||
// }
|
||||
});
|
||||
|
||||
// 首页处理
|
||||
const home = treeFind(routes, i => i.path === menuStore.home);
|
||||
if (home) {
|
||||
const route = { ...home, name: APP_HOME_NAME, alias: '/' };
|
||||
router.removeRoute(home.name!);
|
||||
router.addRoute(APP_ROUTE_NAME, route);
|
||||
return router.replace(to.path);
|
||||
}
|
||||
return to.fullPath;
|
||||
}
|
||||
|
||||
// 兜底处理
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { NProgress } from '@/libs/nprogress';
|
||||
import { useAppStore } from '@/store';
|
||||
import NProgress from 'nprogress';
|
||||
import { useAppStore } from '@/store/app';
|
||||
import { Router } from 'vue-router';
|
||||
|
||||
const routeMap = new Map<string, boolean>();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { store, useAppStore } from '@/store';
|
||||
import { store } from '@/store';
|
||||
import { useAppStore } from '@/store/app';
|
||||
import { Router } from 'vue-router';
|
||||
|
||||
export function useTitleGuard(router: Router) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { createRouter } from 'vue-router';
|
|||
import { useAuthGuard } from '../guards/auth';
|
||||
import { useProgressGard } from '../guards/progress';
|
||||
import { useTitleGuard } from '../guards/title';
|
||||
import { baseRoutes } from '../routes/base';
|
||||
import { historyMode } from './util';
|
||||
import { routes } from '../routes/page';
|
||||
|
||||
|
|
@ -11,7 +10,7 @@ import { routes } from '../routes/page';
|
|||
*/
|
||||
export const router = createRouter({
|
||||
history: historyMode(),
|
||||
routes: [...baseRoutes, ...routes],
|
||||
routes: routes,
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
import { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
export const APP_HOME_NAME = '__APP_HOME__';
|
||||
|
||||
/**
|
||||
* 基本路由
|
||||
*/
|
||||
export const baseRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
name: APP_HOME_NAME,
|
||||
component: () => 'Home Page',
|
||||
},
|
||||
];
|
||||
|
|
@ -15,8 +15,8 @@ function treeRoutes(list: RouteRecordRaw[]) {
|
|||
for (const item of list) {
|
||||
const parentPath = item.path.split('/').slice(0, -1).join('/');
|
||||
const parent = map[parentPath];
|
||||
item.parentName = (parent?.name as string) || APP_ROUTE_NAME;
|
||||
if (parent) {
|
||||
(item as any).parentPath = parentPath;
|
||||
(parent.children || (parent.children = [])).push(item);
|
||||
} else {
|
||||
tree.push(item);
|
||||
|
|
@ -47,12 +47,14 @@ function sortRoutes(routes: RouteRecordRaw[]) {
|
|||
const transformRoutes = (routes: RouteRecordRaw[]) => {
|
||||
const topRoutes: RouteRecordRaw[] = [];
|
||||
const appRoutes: RouteRecordRaw[] = [];
|
||||
let app: RouteRecordRaw;
|
||||
|
||||
for (const route of routes) {
|
||||
if (route.name === APP_ROUTE_NAME) {
|
||||
app = route;
|
||||
route.children = appRoutes;
|
||||
}
|
||||
if ((route.name as string)?.startsWith(TOP_ROUTE_PREF)) {
|
||||
if (route.name === APP_ROUTE_NAME) {
|
||||
route.children = appRoutes;
|
||||
}
|
||||
route.path = route.path.replace(TOP_ROUTE_PREF, '');
|
||||
topRoutes.push(route);
|
||||
continue;
|
||||
|
|
@ -60,7 +62,8 @@ const transformRoutes = (routes: RouteRecordRaw[]) => {
|
|||
appRoutes.push(route);
|
||||
}
|
||||
|
||||
return [topRoutes, sortRoutes(treeRoutes(appRoutes))];
|
||||
app!.children = sortRoutes(treeRoutes(appRoutes));
|
||||
return [topRoutes, app!.children];
|
||||
};
|
||||
|
||||
export const [routes, appRoutes] = transformRoutes(generatedRoutes);
|
||||
|
|
|
|||
|
|
@ -1,3 +1 @@
|
|||
export * from './app';
|
||||
export * from './store';
|
||||
export * from './user';
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export const useUserStore = defineStore({
|
||||
id: "user",
|
||||
id: 'user',
|
||||
state: (): UserStore => {
|
||||
return {
|
||||
id: 0,
|
||||
username: "juetan",
|
||||
nickname: "绝弹",
|
||||
avatar: "https://github.com/juetan.png",
|
||||
accessToken: "",
|
||||
username: 'juetan',
|
||||
nickname: '绝弹',
|
||||
avatar: 'https://github.com/juetan.png',
|
||||
accessToken: '',
|
||||
refreshToken: undefined,
|
||||
auth: []
|
||||
auth: [],
|
||||
};
|
||||
},
|
||||
actions: {
|
||||
|
|
@ -48,7 +48,10 @@ export const useUserStore = defineStore({
|
|||
accessToken && (this.accessToken = accessToken);
|
||||
},
|
||||
},
|
||||
persist: true,
|
||||
persist: {
|
||||
key: '__APP_USER__',
|
||||
paths: ['accessToken'],
|
||||
},
|
||||
});
|
||||
|
||||
export interface UserStore {
|
||||
|
|
@ -65,11 +68,11 @@ export interface UserStore {
|
|||
*/
|
||||
nickname: string;
|
||||
/**
|
||||
* 用户头像地址
|
||||
* 头像地址
|
||||
*/
|
||||
avatar?: string;
|
||||
/**
|
||||
* JWT令牌
|
||||
* 访问令牌
|
||||
*/
|
||||
accessToken?: string;
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
@arcoblue-6: #08f;
|
||||
|
||||
body {
|
||||
// --border-radius-small: 4px;
|
||||
--border-radius-small: 2px;
|
||||
|
||||
.arco-table .arco-table-element {
|
||||
table-layout: fixed;
|
||||
|
|
@ -62,20 +62,20 @@ body {
|
|||
.arco-menu {
|
||||
&.arco-menu-vertical .arco-menu-item {
|
||||
line-height: 36px;
|
||||
margin-top: 2px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
&.arco-menu-vertical .arco-menu-group-title {
|
||||
line-height: 28px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
[class^="icon-"] {
|
||||
font-size: 14px;
|
||||
font-size: 18px;
|
||||
vertical-align: -2px;
|
||||
}
|
||||
.arco-menu-item {
|
||||
margin: 0 4px;
|
||||
&:hover {
|
||||
background-color: var(--color-neutral-1);
|
||||
background-color: var(--color-neutral-2);
|
||||
}
|
||||
&.arco-menu-selected {
|
||||
// color: @arcoblue-6;
|
||||
|
|
@ -135,6 +135,10 @@ body {
|
|||
.an-form-modal .arco-modal-body {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.arco-form-item-message {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
.dark {
|
||||
.arco-menu-item.arco-menu-selected {
|
||||
|
|
@ -155,3 +159,16 @@ body {
|
|||
border-color: var(--color-neutral-2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.col-form {
|
||||
.arco-form-item-wrapper-col {
|
||||
// flex-direction: row;
|
||||
}
|
||||
.arco-form-item-content-wrapper {
|
||||
width: 450px;
|
||||
}
|
||||
.arco-form-item-message {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ declare module 'vue' {
|
|||
export interface GlobalComponents {
|
||||
AAutoComplete: typeof import('@arco-design/web-vue')['AutoComplete']
|
||||
AAvatar: typeof import('@arco-design/web-vue')['Avatar']
|
||||
ABadge: typeof import('@arco-design/web-vue')['Badge']
|
||||
ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb']
|
||||
ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem']
|
||||
AButton: typeof import('@arco-design/web-vue')['Button']
|
||||
|
|
|
|||
|
|
@ -50,12 +50,11 @@ declare module 'vue-router/auto/routes' {
|
|||
'/content/material-category/': RouteRecordInfo<'/content/material-category/', '/content/material-category', Record<never, never>, Record<never, never>>,
|
||||
'/content/post/': RouteRecordInfo<'/content/post/', '/content/post', Record<never, never>, Record<never, never>>,
|
||||
'/dev/': RouteRecordInfo<'/dev/', '/dev', Record<never, never>, Record<never, never>>,
|
||||
'/dev/editor/': RouteRecordInfo<'/dev/editor/', '/dev/editor', Record<never, never>, Record<never, never>>,
|
||||
'/dev/nav/': RouteRecordInfo<'/dev/nav/', '/dev/nav', Record<never, never>, Record<never, never>>,
|
||||
'/dev/openapi/': RouteRecordInfo<'/dev/openapi/', '/dev/openapi', Record<never, never>, Record<never, never>>,
|
||||
'/home/': RouteRecordInfo<'/home/', '/home', Record<never, never>, Record<never, never>>,
|
||||
'/log/': RouteRecordInfo<'/log/', '/log', Record<never, never>, Record<never, never>>,
|
||||
'/log/login/': RouteRecordInfo<'/log/login/', '/log/login', Record<never, never>, Record<never, never>>,
|
||||
'/log/auth/': RouteRecordInfo<'/log/auth/', '/log/auth', Record<never, never>, Record<never, never>>,
|
||||
'/log/operation/': RouteRecordInfo<'/log/operation/', '/log/operation', Record<never, never>, Record<never, never>>,
|
||||
'/setting/': RouteRecordInfo<'/setting/', '/setting', Record<never, never>, Record<never, never>>,
|
||||
'/setting/common/': RouteRecordInfo<'/setting/common/', '/setting/common', Record<never, never>, Record<never, never>>,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,30 @@
|
|||
import "vue-router";
|
||||
import 'vue-router';
|
||||
|
||||
declare module 'vue-router' {
|
||||
interface RouteRecordRaw {
|
||||
parentName?: string;
|
||||
}
|
||||
|
||||
interface RouteRecordSingleViewWithChildren {
|
||||
parentName?: string;
|
||||
}
|
||||
|
||||
interface RouteRecordSingleView {
|
||||
parentName?: string;
|
||||
}
|
||||
|
||||
interface RouteRecordMultipleViews {
|
||||
parentName?: string;
|
||||
}
|
||||
|
||||
interface RouteRecordMultipleViewsWithChildren {
|
||||
parentName?: string;
|
||||
}
|
||||
|
||||
interface RouteRecordRedirect {
|
||||
parentName?: string;
|
||||
}
|
||||
|
||||
declare module "vue-router" {
|
||||
interface RouteMeta {
|
||||
/**
|
||||
* 页面标题
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { Router, RouteLocationNormalizedLoaded } from "vue-router";
|
||||
import { Router, RouteLocationNormalizedLoaded } from 'vue-router';
|
||||
|
||||
declare module "vue" {
|
||||
declare module 'vue' {
|
||||
interface ComponentCustomProperties {
|
||||
$router: Router;
|
||||
$route: RouteLocationNormalizedLoaded;
|
||||
$dayjs: Dayjs;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,7 +111,6 @@ export default defineConfig(({ mode }) => {
|
|||
brand: arcoToUnoColor('primary'),
|
||||
},
|
||||
},
|
||||
include: ['src/**/*.{vue,ts,tsx,css,scss,sass,less,styl}'],
|
||||
presets: [
|
||||
presetUno(),
|
||||
presetIcons({
|
||||
|
|
@ -122,6 +121,11 @@ export default defineConfig(({ mode }) => {
|
|||
},
|
||||
}),
|
||||
],
|
||||
content: {
|
||||
pipeline: {
|
||||
include: ['src/**/*.{vue,ts,tsx,css,scss,sass,less,styl}'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue