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 init
|
||||||
git add -A
|
git add -A
|
||||||
git commit -m "Build through github action"
|
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,
|
"tabWidth": 2,
|
||||||
"printWidth": 120,
|
"printWidth": 180,
|
||||||
"bracketSpacing": true,
|
"bracketSpacing": true,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"arrowParens": "avoid"
|
"arrowParens": "avoid"
|
||||||
|
|
|
||||||
|
|
@ -18,5 +18,7 @@ COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
# 复制nginx配置
|
# 复制nginx配置
|
||||||
COPY --from=builder /app/.github/nginx.conf /etc/nginx/conf.d/default.conf
|
COPY --from=builder /app/.github/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# 显式暴露端口
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
# 启动,关闭后台运行启动前台运行,不然 docker 会结束运行
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
144
README.md
144
README.md
|
|
@ -15,6 +15,7 @@
|
||||||
- 遵循 Conventional Changelog 规范, 自动生成版本记录文档
|
- 遵循 Conventional Changelog 规范, 自动生成版本记录文档
|
||||||
- 内置常用 VsCode 代码片段和推荐扩展,提升开发效率
|
- 内置常用 VsCode 代码片段和推荐扩展,提升开发效率
|
||||||
- 支持路由动态打包、路由权限、路由缓存和动态首页
|
- 支持路由动态打包、路由权限、路由缓存和动态首页
|
||||||
|
- 支持 Docker 部署,包含优化过的 Dockerfile 配置
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
|
|
@ -53,20 +54,153 @@ pnpm dev
|
||||||
|
|
||||||
根据 src/pages 目录生成路由数组,包含以下以下规则:
|
根据 src/pages 目录生成路由数组,包含以下以下规则:
|
||||||
|
|
||||||
- 以文件夹为路由,读取该文件夹下 index.vue 的信息作为路由信息,其他文件会跳过,可以包含子文件夹
|
- 以文件夹为路由,读取该文件夹下 index.vue 的信息作为路由信息,其他文件会跳过,可以包含子文件夹作为嵌套路由
|
||||||
- 在 src/pages 的文件夹层级,作为菜单层级,路由层级最终只有 2 层(配合 keep-alive 缓存)
|
- 在 src/pages 的文件夹层级,作为菜单层级,路由层级最终只有 2 层(配合 keep-alive 缓存)
|
||||||
- 在 src/pages 目录下,以 _ 开头的文件夹作为 1 级路由,其他作为 2 级路由,也就是应用路由
|
- 在 src/pages 目录下,以 _ 开头的文件夹作为 1 级路由,其他作为 2 级路由,也就是应用路由
|
||||||
- 子文件夹下,只有 index.vue 文件有效,其他文件会忽略,这些文件可以作为子组件使用
|
- 子文件夹下,只有 index.vue 文件有效,其他文件会忽略,这些文件可以作为子组件使用
|
||||||
- components 目录会被忽视
|
- components 目录会被忽视。
|
||||||
- xxx.xx.xx 文件会被忽视,例如 index.my.vue
|
- xxx.xx.xx 文件会被忽视,例如 index.my.vue 文件。
|
||||||
|
|
||||||
对应目录下的 index.vue 文件中定义如下路由配置:
|
对应目录下的 index.vue 文件中定义如下路由配置:
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
<route lang="json">
|
<route lang="json">
|
||||||
{
|
{
|
||||||
"parentMeta": {
|
// 其他 Route 参数
|
||||||
// 具体属性查阅 src/types/vue-router.d.ts
|
"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>
|
</route>
|
||||||
|
|
|
||||||
81
index.html
81
index.html
|
|
@ -9,16 +9,13 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app" class="dark:bg-slate-900 dark:text-slate-200">
|
<div id="app" class="dark:bg-slate-900 dark:text-slate-200">
|
||||||
<div class="loading">
|
<div class="cube">
|
||||||
<img
|
<div></div>
|
||||||
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHN0eWxlPSJtYXJnaW46IGF1dG87IGJhY2tncm91bmQ6IHJnYigyNTUsIDI1NSwgMjU1KTsgZGlzcGxheTogYmxvY2s7IHNoYXBlLXJlbmRlcmluZzogYXV0bzsiIHdpZHRoPSIyMDBweCIgaGVpZ2h0PSIyMDBweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIj48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1MCA1MCkiPjxnPjxhbmltYXRlVHJhbnNmb3JtIGF0dHJpYnV0ZU5hbWU9InRyYW5zZm9ybSIgdHlwZT0icm90YXRlIiB2YWx1ZXM9IjA7NDUiIGtleVRpbWVzPSIwOzEiIGR1cj0iMC4ycyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiPjwvYW5pbWF0ZVRyYW5zZm9ybT48cGF0aCBkPSJNMjkuNDkxNTI0MjA2MTE3MjU1IC01LjUgTDM3LjQ5MTUyNDIwNjExNzI1NSAtNS41IEwzNy40OTE1MjQyMDYxMTcyNTUgNS41IEwyOS40OTE1MjQyMDYxMTcyNTUgNS41IEEzMCAzMCAwIDAgMSAyNC43NDI3NDQwNTAxOTg3MzggMTYuOTY0NTY5NDU3MTQ2NzEyIEwyNC43NDI3NDQwNTAxOTg3MzggMTYuOTY0NTY5NDU3MTQ2NzEyIEwzMC4zOTk1OTgyOTk2OTExMTcgMjIuNjIxNDIzNzA2NjM5MDkyIEwyMi42MjE0MjM3MDY2MzkwOTYgMzAuMzk5NTk4Mjk5NjkxMTE0IEwxNi45NjQ1Njk0NTcxNDY3MTYgMjQuNzQyNzQ0MDUwMTk4NzM0IEEzMCAzMCAwIDAgMSA1LjUgMjkuNDkxNTI0MjA2MTE3MjU1IEw1LjUgMjkuNDkxNTI0MjA2MTE3MjU1IEw1LjUgMzcuNDkxNTI0MjA2MTE3MjU1IEwtNS40OTk5OTk5OTk5OTk5OTcgMzcuNDkxNTI0MjA2MTE3MjU1IEwtNS40OTk5OTk5OTk5OTk5OTcgMjkuNDkxNTI0MjA2MTE3MjU1IEEzMCAzMCAwIDAgMSAtMTYuOTY0NTY5NDU3MTQ2NzA1IDI0Ljc0Mjc0NDA1MDE5ODczOCBMLTE2Ljk2NDU2OTQ1NzE0NjcwNSAyNC43NDI3NDQwNTAxOTg3MzggTC0yMi42MjE0MjM3MDY2MzkwODUgMzAuMzk5NTk4Mjk5NjkxMTE3IEwtMzAuMzk5NTk4Mjk5NjkxMTE3IDIyLjYyMTQyMzcwNjYzOTA5MiBMLTI0Ljc0Mjc0NDA1MDE5ODczOCAxNi45NjQ1Njk0NTcxNDY3MTIgQTMwIDMwIDAgMCAxIC0yOS40OTE1MjQyMDYxMTcyNTUgNS41MDAwMDAwMDAwMDAwMDkgTC0yOS40OTE1MjQyMDYxMTcyNTUgNS41MDAwMDAwMDAwMDAwMDkgTC0zNy40OTE1MjQyMDYxMTcyNTUgNS41MDAwMDAwMDAwMDAwMSBMLTM3LjQ5MTUyNDIwNjExNzI1NSAtNS41MDAwMDAwMDAwMDAwMDEgTC0yOS40OTE1MjQyMDYxMTcyNTUgLTUuNTAwMDAwMDAwMDAwMDAyIEEzMCAzMCAwIDAgMSAtMjQuNzQyNzQ0MDUwMTk4NzM4IC0xNi45NjQ1Njk0NTcxNDY3MDUgTC0yNC43NDI3NDQwNTAxOTg3MzggLTE2Ljk2NDU2OTQ1NzE0NjcwNSBMLTMwLjM5OTU5ODI5OTY5MTExNyAtMjIuNjIxNDIzNzA2NjM5MDg1IEwtMjIuNjIxNDIzNzA2NjM5MDkyIC0zMC4zOTk1OTgyOTk2OTExMTcgTC0xNi45NjQ1Njk0NTcxNDY3MTIgLTI0Ljc0Mjc0NDA1MDE5ODczOCBBMzAgMzAgMCAwIDEgLTUuNTAwMDAwMDAwMDAwMDExIC0yOS40OTE1MjQyMDYxMTcyNTUgTC01LjUwMDAwMDAwMDAwMDAxMSAtMjkuNDkxNTI0MjA2MTE3MjU1IEwtNS41MDAwMDAwMDAwMDAwMTIgLTM3LjQ5MTUyNDIwNjExNzI1NSBMNS40OTk5OTk5OTk5OTk5OTggLTM3LjQ5MTUyNDIwNjExNzI1NSBMNS41IC0yOS40OTE1MjQyMDYxMTcyNTUgQTMwIDMwIDAgMCAxIDE2Ljk2NDU2OTQ1NzE0NjcwMiAtMjQuNzQyNzQ0MDUwMTk4NzQgTDE2Ljk2NDU2OTQ1NzE0NjcwMiAtMjQuNzQyNzQ0MDUwMTk4NzQgTDIyLjYyMTQyMzcwNjYzOTA4IC0zMC4zOTk1OTgyOTk2OTExMiBMMzAuMzk5NTk4Mjk5NjkxMTE3IC0yMi42MjE0MjM3MDY2MzkxIEwyNC43NDI3NDQwNTAxOTg3MzggLTE2Ljk2NDU2OTQ1NzE0NjcxNiBBMzAgMzAgMCAwIDEgMjkuNDkxNTI0MjA2MTE3MjU1IC01LjUwMDAwMDAwMDAwMDAxMyBNMCAtMjBBMjAgMjAgMCAxIDAgMCAyMCBBMjAgMjAgMCAxIDAgMCAtMjAiIGZpbGw9IiMwOWYiPjwvcGF0aD48L2c+PC9nPjwvc3ZnPgo="
|
<div></div>
|
||||||
alt="loading"
|
<div></div>
|
||||||
class="loading-image"
|
<div></div>
|
||||||
width="64"
|
<div></div>
|
||||||
height="64"
|
<div></div>
|
||||||
/>
|
|
||||||
<h1 class="loading-title">欢迎访问%VITE_TITLE%</h1>
|
|
||||||
<div class="loading-tip">资源加载中, 请稍等...</div>
|
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style>
|
||||||
html,
|
html,
|
||||||
|
|
@ -29,8 +26,7 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-family: Inter, '-apple-system', BlinkMacSystemFont, 'PingFang SC', 'Hiragino Sans GB', 'noto sans',
|
font-family: Inter, '-apple-system', BlinkMacSystemFont, 'PingFang SC', 'Hiragino Sans GB', 'noto sans', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
||||||
}
|
}
|
||||||
#app {
|
#app {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -38,28 +34,53 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
.loading {
|
@keyframes cube {
|
||||||
display: flex;
|
0% {
|
||||||
flex-direction: column;
|
transform: rotate(45deg) rotateX(-25deg) rotateY(25deg);
|
||||||
align-items: center;
|
}
|
||||||
justify-content: center;
|
50% {
|
||||||
|
transform: rotate(45deg) rotateX(-385deg) rotateY(25deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(45deg) rotateX(-385deg) rotateY(385deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.loading-image {
|
.cube {
|
||||||
width: 64px;
|
animation: cube 2s infinite ease;
|
||||||
height: 64px;
|
height: 40px;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
width: 40px;
|
||||||
}
|
}
|
||||||
.loading-title {
|
.cube div {
|
||||||
margin: 0;
|
background-color: rgba(255, 255, 255, 0.25);
|
||||||
margin-top: 20px;
|
height: 100%;
|
||||||
font-size: 22px;
|
position: absolute;
|
||||||
font-weight: 400;
|
width: 100%;
|
||||||
line-height: 1;
|
border: 2px solid #000;
|
||||||
}
|
}
|
||||||
.loading-tip {
|
.cube div:nth-of-type(1) {
|
||||||
margin-top: 12px;
|
transform: translateZ(-20px) rotateY(180deg);
|
||||||
line-height: 1;
|
}
|
||||||
color: #889;
|
.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>
|
</style>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "starter-vue",
|
"name": "starter-vue",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"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 } => {
|
export const arcoToUnoColor = (arcoColorName: string): { [id: string]: string } => {
|
||||||
const colors = {};
|
const colors = {};
|
||||||
for (let i = 0; i < 10; i++) {
|
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;
|
return colors;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { Plugin, ResolvedConfig } from 'vite';
|
import { Plugin } from 'vite';
|
||||||
import pkg from '../../package.json';
|
import pkg from '../../package.json';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -46,8 +46,11 @@ const getBuildInfo = async () => {
|
||||||
const version = commits ? `${latestTag}.${commits}` : `v${pkg.version}`;
|
const version = commits ? `${latestTag}.${commits}` : `v${pkg.version}`;
|
||||||
const content = `欢迎访问!版本: ${version} 标识: ${hash} 构建: ${time}`;
|
const content = `欢迎访问!版本: ${version} 标识: ${hash} 构建: ${time}`;
|
||||||
const style = `"color: #09f; font-weight: 900;", "font-size: 12px; color: #09f; font-family: ''"`;
|
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`;
|
const vString = `var __APP_VERSION__ = '${version}';\n`;
|
||||||
return script;
|
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
|
* @returns Plugin
|
||||||
*/
|
*/
|
||||||
export default function plugin(): Plugin {
|
export default function plugin(): Plugin {
|
||||||
let config: ResolvedConfig;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'vite:info',
|
name: 'vite:info',
|
||||||
enforce: 'pre',
|
enforce: 'pre',
|
||||||
|
|
||||||
configResolved(resolvedConfig) {
|
|
||||||
config = resolvedConfig;
|
|
||||||
},
|
|
||||||
|
|
||||||
async transformIndexHtml() {
|
async transformIndexHtml() {
|
||||||
const script = await getBuildInfo();
|
const script = await getBuildInfo();
|
||||||
return [
|
return [
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,10 @@ const userStore = useUserStore();
|
||||||
const menuStore = useMenuStore();
|
const menuStore = useMenuStore();
|
||||||
|
|
||||||
const hasAuth = computed(() => {
|
const hasAuth = computed(() => {
|
||||||
|
if (!route.name.startsWith('_')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return route.matched.every(item => {
|
return route.matched.every(item => {
|
||||||
console.log('i', item);
|
|
||||||
const needAuth = item.meta.auth;
|
const needAuth = item.meta.auth;
|
||||||
const userAuth = userStore.auth;
|
const userAuth = userStore.auth;
|
||||||
if (needAuth?.includes('*')) {
|
if (needAuth?.includes('*')) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,28 @@
|
||||||
import { Service } from './service';
|
|
||||||
import { addToastInterceptor } from '../interceptors/toast';
|
import { addToastInterceptor } from '../interceptors/toast';
|
||||||
import { addAuthInterceptor } from '../interceptors/auth';
|
import { addAuthInterceptor } from '../interceptors/auth';
|
||||||
import { addExceptionInterceptor } from '../interceptors/exception';
|
import { addExceptionInterceptor } from '../interceptors/exception';
|
||||||
import { env } from '@/config/env';
|
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 接口实例
|
* API 接口实例
|
||||||
|
|
@ -22,8 +42,8 @@ addToastInterceptor(api.instance);
|
||||||
* 添加异常处理拦截器
|
* 添加异常处理拦截器
|
||||||
*/
|
*/
|
||||||
addExceptionInterceptor(api.instance, () => api.expireHandler?.());
|
addExceptionInterceptor(api.instance, () => api.expireHandler?.());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加登陆令牌拦截器
|
* 添加登陆令牌拦截器
|
||||||
*/
|
*/
|
||||||
addAuthInterceptor(api.instance);
|
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 { store } from '@/store';
|
||||||
import { AxiosInstance } from "axios";
|
import { useUserStore } from '@/store/user';
|
||||||
|
import { AxiosInstance } from 'axios';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登陆令牌拦截器
|
* 登陆令牌拦截器
|
||||||
* @param axios Axios实例
|
* @param axios Axios实例
|
||||||
*/
|
*/
|
||||||
export function addAuthInterceptor(axios: AxiosInstance) {
|
export function addAuthInterceptor(axios: AxiosInstance) {
|
||||||
axios.interceptors.request.use((config) => {
|
axios.interceptors.request.use(config => {
|
||||||
const userStore = useUserStore(store);
|
const userStore = useUserStore(store);
|
||||||
if (userStore.accessToken) {
|
if (userStore.accessToken) {
|
||||||
config.headers.Authorization = `Bearer ${userStore.accessToken}`;
|
config.headers.Authorization = `Bearer ${userStore.accessToken}`;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full overflow-hidden grid grid-rows-[auto_1fr]">
|
<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">
|
<div class="flex justify-between gap-4">
|
||||||
<BreadCrumb></BreadCrumb>
|
<BreadCrumb></BreadCrumb>
|
||||||
<div>
|
<div>
|
||||||
<a-link>需要帮助?</a-link>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { InjectionKey } from 'vue';
|
import { InjectionKey } from 'vue';
|
||||||
import { Block, Blocker, Container } from '../core';
|
import { Block, Blocker, Container } from '../core';
|
||||||
import { useTextBlock } from './text';
|
|
||||||
|
|
||||||
const blockers: Record<string, Blocker> = import.meta.glob(['./*/index.ts', '!./font/*'], {
|
const blockers: Record<string, Blocker> = import.meta.glob(['./*/index.ts', '!./font/*'], {
|
||||||
eager: true,
|
eager: true,
|
||||||
|
|
@ -25,47 +24,3 @@ const getIcon = (type: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export { BlockerMap, getBlockerRender, getIcon, getTypeName };
|
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">
|
<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="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">
|
<div class="h-13 bg-white border-b border-slate-200 z-10">
|
||||||
<EditorHeader
|
<EditorHeader v-model:container="container" :saving="saving" @preview="showPreview = true" @config="showConfig = true" @exit="onExit()" @save="saveData()"></EditorHeader>
|
||||||
v-model:container="container"
|
|
||||||
:saving="saving"
|
|
||||||
@preview="showPreview = true"
|
|
||||||
@config="showConfig = true"
|
|
||||||
@exit="onExit()"
|
|
||||||
@save="saveData()"
|
|
||||||
></EditorHeader>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-[auto_1fr_auto] overflow-hidden">
|
<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">
|
<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>
|
<EditorLeft @rm-block="rmBlock" @current-block="setCurrentBlock"></EditorLeft>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full h-full">
|
<div class="w-full h-full">
|
||||||
<EditorMain
|
<EditorMain v-model:rightPanelCollapsed="rightPanelCollapsed" @add-block="addBlock" @current-block="setCurrentBlock" @block-menu="onBlockContextMenu"></EditorMain>
|
||||||
v-model:rightPanelCollapsed="rightPanelCollapsed"
|
|
||||||
@add-block="addBlock"
|
|
||||||
@current-block="setCurrentBlock"
|
|
||||||
@block-menu="onBlockContextMenu"
|
|
||||||
></EditorMain>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="h-full overflow-hidden bg-white shadow-[-2px_0_6px_rgba(0,0,0,.05)]">
|
<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>
|
</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>
|
<EditorSetting v-model:visible="showConfig" v-model="container"></EditorSetting>
|
||||||
<ContextMenu
|
<ContextMenu v-model:visible="blockMenu.show" :x="blockMenu.x" :y="blockMenu.y" :items="blockMenuItems" @done="blockMenu.show = false"></ContextMenu>
|
||||||
v-model:visible="blockMenu.show"
|
|
||||||
:x="blockMenu.x"
|
|
||||||
:y="blockMenu.y"
|
|
||||||
:items="blockMenuItems"
|
|
||||||
@done="blockMenu.show = false"
|
|
||||||
></ContextMenu>
|
|
||||||
</a-modal>
|
</a-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -44,7 +26,7 @@
|
||||||
import { delConfirm, sleep } from '@/utils';
|
import { delConfirm, sleep } from '@/utils';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
import { useVModel } from '@vueuse/core';
|
import { useVModel } from '@vueuse/core';
|
||||||
import { Block, ContextMenuItem, EditorKey, useEditor } from '../core';
|
import { Block, EditorKey, useEditor } from '../core';
|
||||||
import ContextMenu from './ContextMenu.vue';
|
import ContextMenu from './ContextMenu.vue';
|
||||||
import EditorSetting from './EditorConfig.vue';
|
import EditorSetting from './EditorConfig.vue';
|
||||||
import EditorHeader from './EditorHeader.vue';
|
import EditorHeader from './EditorHeader.vue';
|
||||||
|
|
@ -52,6 +34,8 @@ import EditorLeft from './EditorLeft.vue';
|
||||||
import EditorMain from './EditorMain.vue';
|
import EditorMain from './EditorMain.vue';
|
||||||
import EditorPreview from './EditorPreview.vue';
|
import EditorPreview from './EditorPreview.vue';
|
||||||
import EditorRight from './EditorRight.vue';
|
import EditorRight from './EditorRight.vue';
|
||||||
|
import { ContextKey, usePluginContext } from '../core/plugin';
|
||||||
|
import { TextBlock } from '../blocks/text';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: {
|
visible: {
|
||||||
|
|
@ -67,7 +51,10 @@ const showPreview = ref(false);
|
||||||
const showConfig = ref(false);
|
const showConfig = ref(false);
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
const editor = useEditor();
|
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 }>({
|
const blockMenu = reactive<{ show: boolean; x: number; y: number; block: Block | null }>({
|
||||||
show: false,
|
show: false,
|
||||||
|
|
@ -76,7 +63,7 @@ const blockMenu = reactive<{ show: boolean; x: number; y: number; block: Block |
|
||||||
block: null,
|
block: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const blockMenuItems: ContextMenuItem[] = [
|
const blockMenuItems: any[] = [
|
||||||
{
|
{
|
||||||
name: '删除',
|
name: '删除',
|
||||||
icon: 'icon-park-outline-delete',
|
icon: 'icon-park-outline-delete',
|
||||||
|
|
@ -103,7 +90,7 @@ const onBlockContextMenu = (block: Block, e: MouseEvent) => {
|
||||||
const saveData = async () => {
|
const saveData = async () => {
|
||||||
const data = {
|
const data = {
|
||||||
container: container.value,
|
container: container.value,
|
||||||
children: blocks.value,
|
children: container.value.children,
|
||||||
};
|
};
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
await sleep(3000);
|
await sleep(3000);
|
||||||
|
|
@ -120,7 +107,7 @@ const loadData = async () => {
|
||||||
}
|
}
|
||||||
const data = JSON.parse(str);
|
const data = JSON.parse(str);
|
||||||
container.value = data.container;
|
container.value = data.container;
|
||||||
blocks.value = data.children;
|
container.value.children = data.children;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onExit = async () => {
|
const onExit = async () => {
|
||||||
|
|
@ -133,6 +120,7 @@ const onExit = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
provide(EditorKey, editor);
|
provide(EditorKey, editor);
|
||||||
|
provide(ContextKey, context);
|
||||||
onMounted(loadData);
|
onMounted(loadData);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,7 @@ const props = defineProps({
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:visible', 'update:modelValue']);
|
const emit = defineEmits(['update:visible', 'update:modelValue']);
|
||||||
|
|
||||||
const show = useVModel(props, 'visible', emit);
|
const show = useVModel(props, 'visible', emit);
|
||||||
const model = useVModel(props, 'modelValue', emit);
|
const model = useVModel(props, 'modelValue', emit);
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,13 @@
|
||||||
</a-link>
|
</a-link>
|
||||||
<a-divider :direction="'vertical'" :margin="8"></a-divider>
|
<a-divider :direction="'vertical'" :margin="8"></a-divider>
|
||||||
<ani-texter v-model="container.title"></ani-texter>
|
<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">
|
<!-- <a-tag :color="container.id ? 'blue' : 'green'" class="mr-2 ml-1">
|
||||||
{{ container.id ? '修改' : '新增' }}
|
{{ container.id ? '修改' : '新增' }}
|
||||||
</a-tag> -->
|
</a-tag> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
|
<component v-for="item in HR" :key="item.name" :is="item" />
|
||||||
<a-button @click="emit('preview')">
|
<a-button @click="emit('preview')">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="icon-park-outline-play"></i>
|
<i class="icon-park-outline-play"></i>
|
||||||
|
|
@ -38,6 +40,7 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Container } from '../core';
|
import { Container } from '../core';
|
||||||
|
import { ContextKey } from '../core/plugin';
|
||||||
import AniTexter from './InputTexter.vue';
|
import AniTexter from './InputTexter.vue';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
|
|
@ -48,8 +51,9 @@ defineProps({
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['preview', 'config', 'exit', 'save']);
|
const emit = defineEmits(['preview', 'config', 'exit', 'save']);
|
||||||
|
|
||||||
const container = defineModel<Container>('container', { required: true });
|
const container = defineModel<Container>('container', { required: true });
|
||||||
|
|
||||||
|
const { HR, HL } = inject(ContextKey)!
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full grid grid-cols-[auto_1fr]" :style="{ width: !collapsed ? '248px' : undefined }">
|
<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">
|
<div class="h-full grid grid-rows-[1fr_auto] border-r border-slate-200">
|
||||||
<a-menu
|
<a-menu :collapsed="true" :default-selected-keys="['0_0']" :selected-keys="[key]" @menu-item-click="k => (key = k)">
|
||||||
:collapsed="true"
|
|
||||||
:default-selected-keys="['0_0']"
|
|
||||||
:selected-keys="[key]"
|
|
||||||
@menu-item-click="(k) => (key = k)"
|
|
||||||
>
|
|
||||||
<a-menu-item key="list">
|
<a-menu-item key="list">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="icon-park-outline-all-application"></i>
|
<i class="icon-park-outline-all-application"></i>
|
||||||
|
|
@ -31,10 +26,7 @@
|
||||||
<a-tooltip :content="collapsed ? '展开' : '折叠'" position="right">
|
<a-tooltip :content="collapsed ? '展开' : '折叠'" position="right">
|
||||||
<a-button type="text" @click="collapsed = !collapsed">
|
<a-button type="text" @click="collapsed = !collapsed">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<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>
|
||||||
class="text-lg text-gray-400 hover:text-gray-700"
|
|
||||||
:class="collapsed ? 'icon-park-outline-expand-left' : 'icon-park-outline-expand-right'"
|
|
||||||
></i>
|
|
||||||
</template>
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
|
|
@ -61,13 +53,13 @@
|
||||||
</ul>
|
</ul>
|
||||||
<ul v-show="key === 'data'" class="list-none px-2 grid gap-2">
|
<ul v-show="key === 'data'" class="list-none px-2 grid gap-2">
|
||||||
<li
|
<li
|
||||||
v-for="item in blocks"
|
v-for="item in container.children"
|
||||||
:key="item.id"
|
: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="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="{
|
:class="{
|
||||||
'!bg-brand-50': currentBlock === item,
|
'!bg-brand-50': container.current === item,
|
||||||
'!text-brand-500': currentBlock === item,
|
'!text-brand-500': container.current === item,
|
||||||
'!border-brand-300': currentBlock === item,
|
'!border-brand-300': container.current === item,
|
||||||
}"
|
}"
|
||||||
@click="emit('current-block', item)"
|
@click="emit('current-block', item)"
|
||||||
>
|
>
|
||||||
|
|
@ -78,10 +70,7 @@
|
||||||
{{ item.title }}
|
{{ item.title }}
|
||||||
</div>
|
</div>
|
||||||
<div class="w-4">
|
<div class="w-4">
|
||||||
<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>
|
||||||
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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -90,24 +79,24 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getIcon } from "../blocks";
|
import { getIcon } from '../blocks';
|
||||||
import { Block, EditorKey } from "../core";
|
import { Block, EditorKey } from '../core';
|
||||||
|
|
||||||
const { blocks, currentBlock, BlockerMap } = inject(EditorKey)!;
|
const { container, BlockerMap } = inject(EditorKey)!;
|
||||||
const blockList = Object.values(BlockerMap);
|
const blockList = Object.values(BlockerMap);
|
||||||
const collapsed = ref(false);
|
const collapsed = ref(false);
|
||||||
const key = ref<"list" | "data">("list");
|
const key = ref<'list' | 'data'>('list');
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: "rm-block", block: Block): void;
|
(event: 'rm-block', block: Block): void;
|
||||||
(event: "current-block", block: Block | null): void;
|
(event: 'current-block', block: Block | null): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 拖拽开始时设置数据
|
* 拖拽开始时设置数据
|
||||||
*/
|
*/
|
||||||
const onDragStart = (e: DragEvent) => {
|
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>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full grid grid-rows-[auto_1fr]">
|
<div class="h-full grid grid-rows-[auto_1fr]">
|
||||||
<div class="h-10">
|
<div class="h-10">
|
||||||
<EditorMainHeader
|
<EditorMainHeader :container="container" v-model:rightPanelCollapsed="rightPanelCollapsed" @preview="emit('preview')"></EditorMainHeader>
|
||||||
:container="container"
|
|
||||||
v-model:rightPanelCollapsed="rightPanelCollapsed"
|
|
||||||
@preview="emit('preview')"
|
|
||||||
></EditorMainHeader>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="h-full w-full overflow-hidden p-4">
|
<div class="h-full w-full overflow-hidden p-4">
|
||||||
<div
|
<div class="juetan-editor-container w-full h-full flex items-center justify-center overflow-hidden relative bg-slate-50">
|
||||||
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="relative"
|
|
||||||
:style="containerStyle"
|
|
||||||
@dragover.prevent
|
|
||||||
@click="onClick"
|
|
||||||
@drop="onDragDrop"
|
|
||||||
@wheel="onMouseWheel"
|
|
||||||
@mousedown="onMouseDown"
|
|
||||||
>
|
|
||||||
<EditorMainBlock
|
<EditorMainBlock
|
||||||
v-for="block in blocks"
|
v-for="block in container.children"
|
||||||
:key="block.id"
|
:key="block.id"
|
||||||
:data="block"
|
:data="block"
|
||||||
:container="container"
|
:container="container"
|
||||||
|
|
@ -60,12 +46,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 EditorMainBlock from './EditorMainBlock.vue';
|
||||||
import EditorMainHeader from './EditorMainHeader.vue';
|
import EditorMainHeader from './EditorMainHeader.vue';
|
||||||
|
|
||||||
const rightPanelCollapsed = defineModel<boolean>('rightPanelCollapsed');
|
const rightPanelCollapsed = defineModel<boolean>('rightPanelCollapsed');
|
||||||
const { blocks, container, refLine, formatContainerStyle, scene } = inject(EditorKey)!;
|
const { container, refLine, scene } = inject(ContextKey)!;
|
||||||
const { onMouseDown, onMouseWheel } = scene;
|
const { onMouseDown, onMouseWheel } = scene;
|
||||||
const { active, xLines, yLines } = refLine;
|
const { active, xLines, yLines } = refLine;
|
||||||
|
|
||||||
|
|
@ -107,14 +94,7 @@ const onDragDrop = (e: DragEvent) => {
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.juetan-editor-container {
|
.juetan-editor-container {
|
||||||
--color: rgba(0, 0, 0, 0.2);
|
--color: rgba(0, 0, 0, 0.2);
|
||||||
background: linear-gradient(
|
background: linear-gradient(45deg, var(--color) 25%, transparent 25%, transparent 75%, var(--color) 75%, var(--color) 100%),
|
||||||
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%);
|
linear-gradient(45deg, var(--color) 25%, transparent 25%, transparent 75%, var(--color) 75%, var(--color) 100%);
|
||||||
background-size: 20px 20px;
|
background-size: 20px 20px;
|
||||||
background-position: 0 0, 10px 10px;
|
background-position: 0 0, 10px 10px;
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,8 @@
|
||||||
import { PropType } from "vue";
|
import { PropType } from "vue";
|
||||||
import { BlockerMap } from "../blocks";
|
import { BlockerMap } from "../blocks";
|
||||||
import DragResizer from "./DragResizer.vue";
|
import DragResizer from "./DragResizer.vue";
|
||||||
import { Block, Container, EditorKey } from "../core";
|
import { Block, Container } from "../core";
|
||||||
|
import { ContextKey } from "../core/plugin";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -41,7 +42,7 @@ const props = defineProps({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { setCurrentBlock, refLine } = inject(EditorKey)!;
|
const { setCurrentBlock, refLine } = inject(ContextKey)!;
|
||||||
const { active, recordBlocksXY, updateRefLine } = refLine;
|
const { active, recordBlocksXY, updateRefLine } = refLine;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
</span>
|
</span>
|
||||||
<span class="text-gray-400 text-xs mr-2">
|
<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>
|
</span>
|
||||||
<a-tooltip content="自适应比例" position="bottom">
|
<a-tooltip content="自适应比例" position="bottom">
|
||||||
<a-button type="text" @click="setContainerOrigin">
|
<a-button type="text" @click="setContainerOrigin">
|
||||||
|
|
@ -62,8 +62,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import InputTexter from './InputTexter.vue';
|
import InputTexter from './InputTexter.vue';
|
||||||
// import EditorMainConfig from './EditorMainConfig.vue';
|
// import EditorMainConfig from './EditorMainConfig.vue';
|
||||||
import { EditorKey } from '../core';
|
|
||||||
import { useVModel } from '@vueuse/core';
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { ContextKey } from '../core/plugin';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
rightPanelCollapsed: {
|
rightPanelCollapsed: {
|
||||||
|
|
@ -74,7 +74,7 @@ const props = defineProps({
|
||||||
|
|
||||||
const emit = defineEmits(['preview', 'update:rightPanelCollapsed']);
|
const emit = defineEmits(['preview', 'update:rightPanelCollapsed']);
|
||||||
const collapsed = useVModel(props, 'rightPanelCollapsed', emit);
|
const collapsed = useVModel(props, 'rightPanelCollapsed', emit);
|
||||||
const { container, blocks, setContainerOrigin } = inject(EditorKey)!;
|
const { container, setContainerOrigin } = inject(ContextKey)!;
|
||||||
|
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i :class="BlockerMap[model.type].icon"></i>
|
<i :class="BlockerMap[model.type].icon"></i>
|
||||||
</template>
|
</template>
|
||||||
{{ BlockerMap[model.type].title }}属性
|
{{ BlockerMap[model.type].title }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto">
|
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto">
|
||||||
<a-form :model="{}" layout="vertical" class="pr-3">
|
<a-form :model="{}" layout="vertical" class="pr-3">
|
||||||
|
|
@ -23,12 +23,13 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { BlockerMap } from '../blocks';
|
import { BlockerMap } from '../blocks';
|
||||||
import { Block, EditorKey } from '../core';
|
import { Block } from '../core';
|
||||||
|
import { ContextKey } from '../core/plugin';
|
||||||
import EditorSetting from './EditorSetting.vue';
|
import EditorSetting from './EditorSetting.vue';
|
||||||
|
|
||||||
const collapsed = defineModel<boolean>('collapsed');
|
const collapsed = defineModel<boolean>('collapsed');
|
||||||
const model = defineModel<Block | null>('block');
|
const model = defineModel<Block | null>('block');
|
||||||
const { container } = inject(EditorKey)!;
|
const { container } = inject(ContextKey)!;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="p-3">
|
<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>
|
<template #icon>
|
||||||
<i class="icon-park-outline-config" ></i>
|
<i class="icon-park-outline-config" ></i>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component } from "vue";
|
import { Component } from 'vue';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组件参数
|
* 组件参数
|
||||||
|
|
@ -70,25 +70,11 @@ export interface Block<T = any> {
|
||||||
params: T;
|
params: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContextMenuItem {
|
export function formatBlockStyle(block: Block) {
|
||||||
type?: 'divider' | 'menu'
|
const { bgColor, bgImage } = block;
|
||||||
showChildren?: boolean
|
return {
|
||||||
onClick?: (item: ContextMenuItem) => void;
|
backgroundColor: bgColor,
|
||||||
icon?: Component | string
|
backgroundImage: bgImage ? `url(${bgImage})` : null,
|
||||||
name: string
|
backgroundSize: '100% 100%',
|
||||||
tip?: string
|
};
|
||||||
class?: string;
|
|
||||||
children?: ContextMenuItem[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
bgColor: string;
|
||||||
|
/**
|
||||||
|
* 使用的语言列表
|
||||||
|
*/
|
||||||
langList: string[];
|
langList: string[];
|
||||||
|
/**
|
||||||
|
* 语言的切换间隔
|
||||||
|
*/
|
||||||
langSwitch: number;
|
langSwitch: number;
|
||||||
|
/**
|
||||||
|
* 组件列表
|
||||||
|
*/
|
||||||
|
children: Block[];
|
||||||
|
/**
|
||||||
|
* 当前选中的组件
|
||||||
|
*/
|
||||||
|
current: Block | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -51,15 +68,30 @@ export interface Container {
|
||||||
*/
|
*/
|
||||||
export const defaultContainer: Container = {
|
export const defaultContainer: Container = {
|
||||||
id: 11,
|
id: 11,
|
||||||
title: "国庆节喜庆版式设计",
|
title: '国庆节喜庆版式设计',
|
||||||
description: "适用于国庆节1日-7日间上午9:00-10:00播出的版式设计",
|
description: '适用于国庆节1日-7日间上午9:00-10:00播出的版式设计',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
zoom: 0.7,
|
zoom: 0.7,
|
||||||
width: 1920,
|
width: 1920,
|
||||||
height: 1080,
|
height: 1080,
|
||||||
bgImage: "",
|
bgImage: '',
|
||||||
bgColor: "#ffffff",
|
bgColor: '#ffffff',
|
||||||
langList: ['ch', 'en'],
|
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 { Container, defaultContainer } from './container';
|
||||||
import { Block } from "./block";
|
import { Block } from './block';
|
||||||
import { useReferenceLine } from "./ref-line";
|
import { useReferenceLine } from './ref-line';
|
||||||
import { BlockerMap } from "../blocks";
|
import { BlockerMap } from '../blocks';
|
||||||
import { cloneDeep } from "lodash-es";
|
import { cloneDeep } from 'lodash-es';
|
||||||
import { CSSProperties, InjectionKey } from "vue";
|
import { CSSProperties, InjectionKey } from 'vue';
|
||||||
import { useScene } from "./scene";
|
import { useScene } from './scene';
|
||||||
|
|
||||||
export const useEditor = () => {
|
export const useEditor = () => {
|
||||||
/**
|
/**
|
||||||
* 画布设置
|
* 画布设置
|
||||||
*/
|
*/
|
||||||
const container = ref<Container>({ ...defaultContainer });
|
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) {
|
if (!blocker) {
|
||||||
return;
|
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 maxId = ids.length ? Math.max.apply(null, ids) : 0;
|
||||||
const id = (maxId + 1).toString();
|
const id = (maxId + 1).toString();
|
||||||
const title = `${blocker.title}${id}`;
|
const title = `${blocker.title}${id}`;
|
||||||
blocks.value.push({
|
container.value.children.push({ ...cloneDeep(blocker.initial), id, x, y, title });
|
||||||
...cloneDeep(blocker.initial),
|
|
||||||
id,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
title,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -61,9 +51,9 @@ export const useEditor = () => {
|
||||||
* @param block 组件
|
* @param block 组件
|
||||||
*/
|
*/
|
||||||
const rmBlock = (block: Block) => {
|
const rmBlock = (block: Block) => {
|
||||||
const index = blocks.value.indexOf(block);
|
const index = container.value.children.indexOf(block);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
blocks.value.splice(index, 1);
|
container.value.children.splice(index, 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -77,7 +67,7 @@ export const useEditor = () => {
|
||||||
return {
|
return {
|
||||||
backgroundColor: bgColor,
|
backgroundColor: bgColor,
|
||||||
backgroundImage: bgImage ? `url(${bgImage})` : undefined,
|
backgroundImage: bgImage ? `url(${bgImage})` : undefined,
|
||||||
backgroundSize: "100% 100%",
|
backgroundSize: '100% 100%',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -89,12 +79,12 @@ export const useEditor = () => {
|
||||||
const formatContainerStyle = (container: Container) => {
|
const formatContainerStyle = (container: Container) => {
|
||||||
const { width, height, bgColor, bgImage, zoom, x, y } = container;
|
const { width, height, bgColor, bgImage, zoom, x, y } = container;
|
||||||
return {
|
return {
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
width: `${width}px`,
|
width: `${width}px`,
|
||||||
height: `${height}px`,
|
height: `${height}px`,
|
||||||
backgroundColor: bgColor,
|
backgroundColor: bgColor,
|
||||||
backgroundImage: bgImage ? `url(${bgImage})` : undefined,
|
backgroundImage: bgImage ? `url(${bgImage})` : undefined,
|
||||||
backgroundSize: "100% 100%",
|
backgroundSize: '100% 100%',
|
||||||
transform: `translate3d(${x}px, ${y}px, 0) scale(${zoom})`,
|
transform: `translate3d(${x}px, ${y}px, 0) scale(${zoom})`,
|
||||||
} as CSSProperties;
|
} as CSSProperties;
|
||||||
};
|
};
|
||||||
|
|
@ -104,7 +94,7 @@ export const useEditor = () => {
|
||||||
* @param block 组件
|
* @param block 组件
|
||||||
*/
|
*/
|
||||||
const setCurrentBlock = (block: Block | null) => {
|
const setCurrentBlock = (block: Block | null) => {
|
||||||
for (const item of blocks.value) {
|
for (const item of container.value.children) {
|
||||||
item.actived = false;
|
item.actived = false;
|
||||||
}
|
}
|
||||||
if (!block) {
|
if (!block) {
|
||||||
|
|
@ -121,7 +111,7 @@ export const useEditor = () => {
|
||||||
const setContainerOrigin = () => {
|
const setContainerOrigin = () => {
|
||||||
container.value.x = 0;
|
container.value.x = 0;
|
||||||
container.value.y = 0;
|
container.value.y = 0;
|
||||||
const el = document.querySelector(".juetan-editor-container");
|
const el = document.querySelector('.juetan-editor-container');
|
||||||
if (el) {
|
if (el) {
|
||||||
const { width, height } = el.getBoundingClientRect();
|
const { width, height } = el.getBoundingClientRect();
|
||||||
const wZoom = width / container.value.width;
|
const wZoom = width / container.value.width;
|
||||||
|
|
@ -133,7 +123,6 @@ export const useEditor = () => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
container,
|
container,
|
||||||
blocks,
|
|
||||||
currentBlock,
|
currentBlock,
|
||||||
refLine,
|
refLine,
|
||||||
scene,
|
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 { Ref } from "vue";
|
||||||
import { getClosestValInSortedArr } from "../utils/closest";
|
import { getClosestValInSortedArr } from "../utils/closest";
|
||||||
import { Block } from "./block";
|
import { Block } from "./block";
|
||||||
|
import { Container } from "./container";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组件参考线
|
* 组件参考线
|
||||||
|
|
@ -8,7 +9,7 @@ import { Block } from "./block";
|
||||||
* @param current 当前组件
|
* @param current 当前组件
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const useReferenceLine = (blocks: Ref<Block[]>, current: Ref<Block | null>) => {
|
export const useReferenceLine = (container: Ref<Container>) => {
|
||||||
let xYsMap = new Map<number, number[]>();
|
let xYsMap = new Map<number, number[]>();
|
||||||
let yXsMap = new Map<number, number[]>();
|
let yXsMap = new Map<number, number[]>();
|
||||||
let sortedXs: number[] = [];
|
let sortedXs: number[] = [];
|
||||||
|
|
@ -22,8 +23,8 @@ export const useReferenceLine = (blocks: Ref<Block[]>, current: Ref<Block | null
|
||||||
*/
|
*/
|
||||||
const recordBlocksXY = () => {
|
const recordBlocksXY = () => {
|
||||||
clear();
|
clear();
|
||||||
for (const block of blocks.value) {
|
for (const block of container.value.children) {
|
||||||
if (block === current.value) {
|
if (block === container.value.current) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const { minX, minY, midX, midY, maxX, maxY } = getBlockBox(block);
|
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 {
|
import { AnForm, AnFormInstance, AnFormModal, AnFormModalInstance, AnFormModalProps, AnFormProps, getModel } from '@/components/AnForm';
|
||||||
AnForm,
|
|
||||||
AnFormInstance,
|
|
||||||
AnFormModal,
|
|
||||||
AnFormModalInstance,
|
|
||||||
AnFormModalProps,
|
|
||||||
AnFormProps,
|
|
||||||
getModel,
|
|
||||||
} from '@/components/AnForm';
|
|
||||||
import AnEmpty from '@/components/AnEmpty/AnEmpty.vue';
|
import AnEmpty from '@/components/AnEmpty/AnEmpty.vue';
|
||||||
import { Button, PaginationProps, Table, TableColumnData, TableData, TableInstance } from '@arco-design/web-vue';
|
import { Button, PaginationProps, Table, TableColumnData, TableData, TableInstance } from '@arco-design/web-vue';
|
||||||
import { isArray, isFunction, merge } from 'lodash-es';
|
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';
|
import { PluginContainer } from '../hooks/useTablePlugin';
|
||||||
|
|
||||||
type DataFn = (filter: { page: number; size: number; [key: string]: any }) => any | Promise<any>;
|
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 const AnTableContextKey = Symbol('AnTableContextKey') as InjectionKey<AnTableContext>;
|
||||||
|
|
||||||
export type TableColumnRender = (data: { record: TableData; column: TableColumnData; rowIndex: number }) => VNodeChild;
|
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="an-table table w-full">
|
||||||
<div class={`mb-3 flex gap-2 toolbar justify-between`}>
|
<div class={`mb-3 flex gap-2 toolbar justify-between`}>
|
||||||
{this.create && <AnFormModal {...this.create} ref="createRef" onSubmited={this.reload}></AnFormModal>}
|
{this.create && <AnFormModal {...this.create} ref="createRef" onSubmited={this.reload}></AnFormModal>}
|
||||||
{this.modify && (
|
{this.modify && <AnFormModal {...this.modify} trigger={false} ref="modifyRef" onSubmited={this.refresh}></AnFormModal>}
|
||||||
<AnFormModal {...this.modify} trigger={false} ref="modifyRef" onSubmited={this.refresh}></AnFormModal>
|
|
||||||
)}
|
|
||||||
{this.$slots.action?.(this.renderData)}
|
{this.$slots.action?.(this.renderData)}
|
||||||
{this.pluginer?.actions && (
|
{this.pluginer?.actions && (
|
||||||
<div class={`flex-1 flex gap-2 items-center`}>
|
<div class={`flex-1 flex gap-2 items-center`}>
|
||||||
|
|
@ -220,12 +204,7 @@ export const AnTable = defineComponent({
|
||||||
)}
|
)}
|
||||||
{this.search && (
|
{this.search && (
|
||||||
<div>
|
<div>
|
||||||
<AnForm
|
<AnForm ref="searchRef" v-model:model={this.search.model} items={this.search.items} formProps={this.search.formProps}>
|
||||||
ref="searchRef"
|
|
||||||
v-model:model={this.search.model}
|
|
||||||
items={this.search.items}
|
|
||||||
formProps={this.search.formProps}
|
|
||||||
>
|
|
||||||
{{
|
{{
|
||||||
submit: () => (
|
submit: () => (
|
||||||
<Button type="primary" loading={this.loading} onClick={this.reload}>
|
<Button type="primary" loading={this.loading} onClick={this.reload}>
|
||||||
|
|
@ -279,10 +258,7 @@ export type AnTableInstance = InstanceType<typeof AnTable>;
|
||||||
/**
|
/**
|
||||||
* 表格组件参数
|
* 表格组件参数
|
||||||
*/
|
*/
|
||||||
export type AnTableProps = Pick<
|
export type AnTableProps = Pick<AnTableInstance['$props'], 'source' | 'columns' | 'search' | 'paging' | 'create' | 'modify' | 'tableProps' | 'pluginer'>;
|
||||||
AnTableInstance['$props'],
|
|
||||||
'source' | 'columns' | 'search' | 'paging' | 'create' | 'modify' | 'tableProps' | 'pluginer'
|
|
||||||
>;
|
|
||||||
|
|
||||||
export interface AnTableContext {
|
export interface AnTableContext {
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
import { dayjs } from '@/libs/dayjs';
|
import { dayjs } from '@/libs/dayjs';
|
||||||
|
import { Avatar } from '@arco-design/web-vue';
|
||||||
import { TableColumn } from '../hooks/useTableColumn';
|
import { TableColumn } from '../hooks/useTableColumn';
|
||||||
|
|
||||||
export function useUpdateColumn(extra: TableColumn = {}): TableColumn {
|
export function useUpdateColumn(extra: TableColumn = {}): TableColumn {
|
||||||
return {
|
return {
|
||||||
title: '更新',
|
title: '最近修改',
|
||||||
dataIndex: 'createdAt',
|
dataIndex: 'createdAt',
|
||||||
width: 180,
|
width: 180,
|
||||||
render: ({ record }) => (
|
render: ({ record }) => (
|
||||||
<div class="flex flex-col overflow-hidden">
|
<div class="flex items-center gap-2 overflow-hidden">
|
||||||
<span>{record.updatedBy ?? '无'}</span>
|
<span>
|
||||||
<span class="text-gray-400 text-xs truncate" title={record.updatedAt}>
|
<Avatar size={22}>{record.updatedBy?.substr(0,1) ?? '无'}</Avatar>
|
||||||
更新于 {dayjs(record.updatedAt).fromNow()}
|
</span>
|
||||||
|
<span class="truncate" title={record.updatedAt}>
|
||||||
|
{dayjs(record.updatedAt).fromNow()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|
@ -24,10 +27,12 @@ export function useCreateColumn(extra: TableColumn = {}): TableColumn {
|
||||||
dataIndex: 'createdAt',
|
dataIndex: 'createdAt',
|
||||||
width: 180,
|
width: 180,
|
||||||
render: ({ record }) => (
|
render: ({ record }) => (
|
||||||
<div class="flex flex-col overflow-hidden">
|
<div class="flex direction-col items-center gap-2 overflow-hidden">
|
||||||
<span>{record.createdBy ?? '无'}</span>
|
<span>
|
||||||
|
{record.createdBy ?? '无'}
|
||||||
|
</span>
|
||||||
<span class="text-gray-400 text-xs truncate" title={record.createdAt}>
|
<span class="text-gray-400 text-xs truncate" title={record.createdAt}>
|
||||||
创建于 {dayjs(record.createdAt).fromNow()}
|
{dayjs(record.createdAt).fromNow()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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[];
|
columns?: TableColumn[];
|
||||||
|
/**
|
||||||
|
* 操作栏
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* [{
|
||||||
|
* text: '按钮',
|
||||||
|
* onClick: () => null,
|
||||||
|
* }]
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
actions?: any[];
|
||||||
/**
|
/**
|
||||||
* 搜索表单
|
* 搜索表单
|
||||||
* @example
|
* @example
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,17 @@ import dayjs from 'dayjs';
|
||||||
import 'dayjs/locale/zh-cn';
|
import 'dayjs/locale/zh-cn';
|
||||||
import localData from 'dayjs/plugin/localeData';
|
import localData from 'dayjs/plugin/localeData';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
import { App } from 'vue';
|
||||||
|
|
||||||
/**
|
declare module 'dayjs' {
|
||||||
*
|
export var DATETIME: 'YYYY-MM-DD HH:mm';
|
||||||
* 默认日期时间格式
|
export var DATE: 'YYYY-MM-DD';
|
||||||
*/
|
export var TIME: 'HH:mm:ss';
|
||||||
const DATETIME = 'YYYY-MM-DD HH:mm';
|
export var install: (app: App) => void;
|
||||||
|
interface Dayjs {
|
||||||
/**
|
_format: Dayjs['format'];
|
||||||
* 默认日期格式
|
}
|
||||||
*/
|
}
|
||||||
const DATE = 'YYYY-MM-DD';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 默认时间格式
|
|
||||||
*/
|
|
||||||
const TIME = 'HH:mm:ss';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 中文语言包
|
* 中文语言包
|
||||||
|
|
@ -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) {
|
dayjs.prototype.format = function (format: string = dayjs.DATETIME) {
|
||||||
if (format) {
|
return this._format(format);
|
||||||
return this._format(format);
|
|
||||||
}
|
|
||||||
return this._format(dayjs.DATETIME);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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 from 'nprogress';
|
||||||
import 'nprogress/nprogress.css';
|
import 'nprogress/nprogress.css';
|
||||||
import './nprogress.css';
|
import './nprogress.css';
|
||||||
|
import { App } from 'vue';
|
||||||
|
|
||||||
NProgress.configure({
|
NProgress.configure({
|
||||||
showSpinner: false,
|
showSpinner: false,
|
||||||
|
|
@ -8,5 +9,9 @@ NProgress.configure({
|
||||||
minimum: 0.3,
|
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 { createApp } from 'vue';
|
||||||
import App from './App.vue';
|
import App from '@/App.vue';
|
||||||
import { router } from './router';
|
import { router } from '@/router';
|
||||||
import { store } from './store';
|
import { store } from '@/store';
|
||||||
import { style } from './styles';
|
import { style } from '@/styles';
|
||||||
|
import { dayjs } from '@/libs/dayjs';
|
||||||
|
import { NProgress } from '@/libs/nprogress';
|
||||||
|
import { api } from '@/api';
|
||||||
|
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
app.use(dayjs);
|
||||||
|
app.use(NProgress);
|
||||||
app.use(store);
|
app.use(store);
|
||||||
|
app.use(api);
|
||||||
app.use(style);
|
app.use(style);
|
||||||
app.use(router);
|
app.use(router);
|
||||||
await router.isReady();
|
await router.isReady();
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,11 @@ export default defineComponent({
|
||||||
watch(
|
watch(
|
||||||
() => route.path,
|
() => 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) {
|
function goto(route: MenuItem) {
|
||||||
|
|
@ -40,14 +42,26 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<a-menu-item key={route.path} v-slots={{ icon }} onClick={() => goto(route)}>
|
<a-menu-item key={route.path} v-slots={{ icon }}>
|
||||||
<div class="flex items-center justify-between gap-2">
|
{route.link ? (
|
||||||
<div>{route.title}</div>
|
<div class="flex items-center justify-between gap-2" onClick={() => goto(route)}>
|
||||||
<div class="text-xs text-gray-400">
|
<div>{route.title}</div>
|
||||||
{/* <a-badge count={8}>8</a-badge> */}
|
<div class="text-xs text-gray-400">
|
||||||
{route.hide === 'prod' ? <a-tag color="red">{'开发'}</a-tag> : null}
|
{/* <a-badge count={8}>8</a-badge> */}
|
||||||
|
{route.hide === 'prod' ? <a-tag color="red">{'开发'}</a-tag> : null}
|
||||||
|
</div>
|
||||||
</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>
|
</a-menu-item>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,16 @@
|
||||||
<a-avatar :size="32">
|
<a-avatar :size="32">
|
||||||
<img :src="userStore.avatar || 'https://github.com/juetan.png'" :alt="userStore.nickname" />
|
<img :src="userStore.avatar || 'https://github.com/juetan.png'" :alt="userStore.nickname" />
|
||||||
</a-avatar>
|
</a-avatar>
|
||||||
<div class="leading-4 my-2">
|
<div class="leading-4 text-base my-2">
|
||||||
{{ userStore.nickname }}
|
{{ userStore.nickname }}
|
||||||
<span class="text-xs text-gray-400">({{ userStore.username }})</span>
|
<a-tag color="red" size="small" >管理员</a-tag>
|
||||||
<div class="text-xs text-gray-400">管理员</div>
|
<div class="text-xs text-gray-400">
|
||||||
|
<span class="text-gray-400">@{{ userStore.username }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-doption>
|
</a-doption>
|
||||||
<a-divider :margin="4"></a-divider>
|
<a-divider :margin="4" class="border-gray-100!"></a-divider>
|
||||||
<a-doption @click="open()">
|
<a-doption @click="open()">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="icon-park-outline-lock"></i>
|
<i class="icon-park-outline-lock"></i>
|
||||||
|
|
@ -36,7 +38,7 @@
|
||||||
</template>
|
</template>
|
||||||
账号信息
|
账号信息
|
||||||
</a-doption>
|
</a-doption>
|
||||||
<a-divider :margin="4"></a-divider>
|
<!-- <a-divider :margin="4" class="border-gray-100!"></a-divider> -->
|
||||||
<a-doption @click="router.push('/user')">
|
<a-doption @click="router.push('/user')">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="icon-park-outline-config"></i>
|
<i class="icon-park-outline-config"></i>
|
||||||
|
|
@ -49,7 +51,7 @@
|
||||||
</template>
|
</template>
|
||||||
关于
|
关于
|
||||||
</a-doption>
|
</a-doption>
|
||||||
<a-divider :margin="4"></a-divider>
|
<a-divider :margin="4" class="border-gray-100!"></a-divider>
|
||||||
<a-doption @click="logout">
|
<a-doption @click="logout">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="icon-park-outline-power"></i>
|
<i class="icon-park-outline-power"></i>
|
||||||
|
|
@ -62,8 +64,8 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useFormModal } from '@/components/AnForm';
|
import { useFormModal } from '@/components/AnForm';
|
||||||
import { useUserStore } from '@/store';
|
import { useUserStore } from '@/store/user';
|
||||||
import { delConfirm } from '@/utils';
|
import { delConfirm, sleep } from '@/utils';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
@ -74,10 +76,13 @@ const logout = async () => {
|
||||||
await delConfirm({
|
await delConfirm({
|
||||||
content: '退出后将跳转到登录页面,确定退出吗?',
|
content: '退出后将跳转到登录页面,确定退出吗?',
|
||||||
okText: '确定退出',
|
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({
|
const { component: PasswordModal, open } = useFormModal({
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<a-layout class="layout">
|
<a-layout class="layout">
|
||||||
<a-layout-header
|
<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">
|
||||||
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">
|
<div class="h-13 flex items-center">
|
||||||
<!-- <a-button size="small" @click="isCollapsed = !isCollapsed">
|
<!-- <a-button size="small" @click="isCollapsed = !isCollapsed">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
|
|
@ -11,7 +9,7 @@
|
||||||
</a-button> -->
|
</a-button> -->
|
||||||
<router-link to="/" class="px-2 flex items-center gap-2 text-slate-700">
|
<router-link to="/" class="px-2 flex items-center gap-2 text-slate-700">
|
||||||
<img src="/favicon.ico" alt="" width="24" height="24" class="" />
|
<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 }}
|
{{ appStore.title }}
|
||||||
<span class="absolute -right-10 -top-1 font-normal text-xs text-gray-400"> v0.0.1 </span>
|
<span class="absolute -right-10 -top-1 font-normal text-xs text-gray-400"> v0.0.1 </span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
@ -19,18 +17,6 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<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-tooltip v-for="btn in buttons" :key="btn.icon" :content="btn.tooltip">
|
||||||
<a-button @click="btn.onClick" class="!bg-transparent !hover:bg-gray-100">
|
<a-button @click="btn.onClick" class="!bg-transparent !hover:bg-gray-100">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
|
|
@ -57,21 +43,26 @@
|
||||||
<Menu />
|
<Menu />
|
||||||
</a-scrollbar>
|
</a-scrollbar>
|
||||||
<template #trigger="{ collapsed }">
|
<template #trigger="{ collapsed }">
|
||||||
<i
|
<div class="w-full h-full py-1 px-1 flex justify-between items-center gap-2" @click.stop>
|
||||||
:class="collapsed ? `icon-park-outline-expand-left` : 'icon-park-outline-expand-right'"
|
<div
|
||||||
class="text-gray-400 text-base hover:text-gray-700"
|
class="inline-block w-10 h-10 h-full rounded flex items-center justify-center hover:bg-zinc-100 text-base text-gray-400"
|
||||||
></i>
|
@click="() => (isCollapsed = !isCollapsed)"
|
||||||
|
>
|
||||||
|
<i :class="collapsed ? `icon-park-outline-expand-left` : 'icon-park-outline-expand-right'"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</a-layout-sider>
|
</a-layout-sider>
|
||||||
<a-layout class="layout-content flex-1">
|
<a-layout class="layout-content flex-1">
|
||||||
<a-layout-content class="overflow-x-auto">
|
<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>
|
<template #icon>
|
||||||
<IconSync></IconSync>
|
<div class="loader"></div>
|
||||||
</template>
|
</template>
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<keep-alive :include="menuStore.caches">
|
<keep-alive :include="menuStore.caches">
|
||||||
<component :is="Component"></component>
|
<component v-if="hasAuth" :is="Component"></component>
|
||||||
|
<AnForbidden v-else></AnForbidden>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</router-view>
|
</router-view>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
|
|
@ -82,32 +73,46 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="tsx" setup>
|
<script lang="tsx" setup>
|
||||||
import { useAppStore } from '@/store';
|
import { useAppStore } from '@/store/app';
|
||||||
import { useMenuStore } from '@/store/menu';
|
import { useMenuStore } from '@/store/menu';
|
||||||
import { Message } from '@arco-design/web-vue';
|
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 Menu from './Menu.vue';
|
||||||
import userDropdown from './UserDropdown.vue';
|
import userDropdown from './UserDropdown.vue';
|
||||||
|
import { useUserStore } from '@/store/user';
|
||||||
|
|
||||||
defineOptions({ name: 'LayoutPage' });
|
defineOptions({ name: 'LayoutPage' });
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const menuStore = useMenuStore();
|
const menuStore = useMenuStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
const isCollapsed = ref(false);
|
const isCollapsed = ref(false);
|
||||||
const themeConfig = ref({ visible: false });
|
const themeConfig = ref({ visible: false });
|
||||||
|
const { toggle, isSupported } = useFullscreen();
|
||||||
|
|
||||||
const ButtonWithTooltip = (props: { tooltip: string; icon: string; onClick: any }) => {
|
const hasAuth = computed(() => {
|
||||||
return (
|
return route.matched.every(item => {
|
||||||
<a-tooltip content={props.tooltip}>
|
const needAuth = item.meta.auth;
|
||||||
<a-button onClick={props.onClick} class="!bg-transparent !hover:bg-gray-100">
|
const userAuth = userStore.auth;
|
||||||
{{
|
if (needAuth?.includes('*')) {
|
||||||
icon: () => <i class={`${props.icon} text-base`}></i>,
|
return true;
|
||||||
}}
|
}
|
||||||
</a-button>
|
if (!userStore.accessToken && needAuth?.includes('unlogin')) {
|
||||||
</a-tooltip>
|
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 = [
|
const buttons = [
|
||||||
{
|
{
|
||||||
|
|
@ -118,10 +123,14 @@ const buttons = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'icon-park-outline-config',
|
icon: 'icon-park-outline-full-screen',
|
||||||
tooltip: '设置',
|
tooltip: '全屏',
|
||||||
onClick: () => {
|
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');
|
window.open('https://github.com/appnify/starter-vue', '_blank');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: 'icon-park-outline-config',
|
||||||
|
tooltip: '设置',
|
||||||
|
onClick: () => {
|
||||||
|
themeConfig.value.visible = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -196,16 +212,40 @@ const buttons = [
|
||||||
background-color: #e4ebf1;
|
background-color: #e4ebf1;
|
||||||
transition: padding 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
|
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>
|
</style>
|
||||||
|
|
||||||
<route lang="json">
|
<route lang="json">
|
||||||
{
|
{
|
||||||
|
"redirect": "/",
|
||||||
"meta": {
|
"meta": {
|
||||||
"name": "LayoutPage",
|
"name": "LayoutPage",
|
||||||
"sort": 101,
|
"sort": 101,
|
||||||
"title": "首页",
|
"title": "首页",
|
||||||
"icon": "icon-park-outline-home",
|
"icon": "icon-park-outline-home"
|
||||||
"keepAlive": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</route>
|
</route>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-center w-full overflow-hidden">
|
<div class="flex items-center justify-center w-full overflow-hidden">
|
||||||
<div
|
<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
|
<div
|
||||||
class="login-left relative hidden md:block w-full h-full overflow-hidden bg-[rgb(var(--primary-6))] px-4"
|
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>
|
<a-link @click="onForgetPassword">忘记密码?</a-link>
|
||||||
</div>
|
</div>
|
||||||
<a-button type="primary" html-type="submit" long class="mt-2" :loading="loading" @click="onSubmitForm">
|
<a-button type="primary" html-type="submit" long class="mt-2" :loading="loading" @click="onSubmitForm">
|
||||||
{{ loading ? "登陆中" : "立即登录" }}
|
{{ loading ? '登陆中' : '立即登录' }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<p type="text" long class="text-gray-400 text-center m-0">暂不支持其他方式登录</p>
|
<p type="text" long class="text-gray-400 text-center m-0">暂不支持其他方式登录</p>
|
||||||
</a-space>
|
</a-space>
|
||||||
|
|
@ -55,18 +55,19 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { api } from "@/api";
|
import { api } from '@/api';
|
||||||
import { dayjs } from "@/libs/dayjs";
|
import { dayjs } from '@/libs/dayjs';
|
||||||
import { useAppStore, useUserStore } from "@/store";
|
import { useAppStore } from '@/store/app';
|
||||||
import { FieldRule, Form, Message, Modal, Notification } from "@arco-design/web-vue";
|
import { useUserStore } from '@/store/user';
|
||||||
import { reactive } from "vue";
|
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 meridiem = dayjs.localeData().meridiem(dayjs().hour(), dayjs().minute());
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const model = reactive({ username: "", password: "" });
|
const model = reactive({ username: '', password: '' });
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
@ -76,22 +77,22 @@ const formRules: Record<string, FieldRule[]> = {
|
||||||
username: [
|
username: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: "请输入账号/手机号/邮箱",
|
message: '请输入账号/手机号/邮箱',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
password: [
|
password: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: "请输入密码",
|
message: '请输入密码',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const onForgetPassword = () => {
|
const onForgetPassword = () => {
|
||||||
Modal.info({
|
Modal.info({
|
||||||
title: "忘记密码?",
|
title: '忘记密码?',
|
||||||
content: "如已忘记密码,请联系管理员进行密码重置!",
|
content: '如已忘记密码,请联系管理员进行密码重置!',
|
||||||
modalClass: "text-center",
|
modalClass: 'text-center',
|
||||||
maskClosable: false,
|
maskClosable: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -105,10 +106,10 @@ const onSubmitForm = async () => {
|
||||||
const res = await api.auth.login(model);
|
const res = await api.auth.login(model);
|
||||||
userStore.setAccessToken(res.data.data);
|
userStore.setAccessToken(res.data.data);
|
||||||
Notification.success({
|
Notification.success({
|
||||||
title: "提示",
|
title: '提示',
|
||||||
content: `${meridiem}好,您已成功登陆本系统!`,
|
content: `${meridiem}好,您已成功登陆本系统!`,
|
||||||
});
|
});
|
||||||
router.push({ path: (route.query.redirect as string) || "/" });
|
router.push({ path: (route.query.redirect as string) || '/' });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const message = error?.response?.data?.message;
|
const message = error?.response?.data?.message;
|
||||||
message && Message.warning(`提示:${message}`);
|
message && Message.warning(`提示:${message}`);
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ const { component: CategoryTable } = useTable({
|
||||||
<div class="flex flex-col overflow-hidden">
|
<div class="flex flex-col overflow-hidden">
|
||||||
<span>
|
<span>
|
||||||
{record.name}
|
{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>
|
</span>
|
||||||
<div class="text-gray-400 text-xs truncate mt-0.5">{record.description}</div>
|
<div class="text-gray-400 text-xs truncate mt-0.5">{record.description}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,8 @@
|
||||||
</template>
|
</template>
|
||||||
上传
|
上传
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-modal
|
<a-modal class="an-upload" title="上传文件" title-align="start" v-model:visible="visible" :width="960" :mask-closable="false" :on-before-cancel="onBeforeCancel" @close="onClose">
|
||||||
title="上传文件"
|
<div class="flex items-center justify-between gap-4 py-0">
|
||||||
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-upload
|
<a-upload
|
||||||
ref="uploadRef"
|
ref="uploadRef"
|
||||||
class="upload"
|
class="upload"
|
||||||
|
|
@ -27,7 +19,7 @@
|
||||||
@error="onUploadError"
|
@error="onUploadError"
|
||||||
>
|
>
|
||||||
<template #upload-button>
|
<template #upload-button>
|
||||||
<a-button type="primary">
|
<a-button type="outline">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="icon-park-outline-upload-one"></i>
|
<i class="icon-park-outline-upload-one"></i>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -35,8 +27,8 @@
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
</a-upload>
|
</a-upload>
|
||||||
<div class="flex-1 flex items-center text-gray-400">
|
<div class="flex items-center text-gray-400">
|
||||||
归类为:
|
已选择 {{ fileList.length }} 项,归类为:
|
||||||
<span>
|
<span>
|
||||||
<a-select v-model="group" :bordered="false" :options="groupOptions"></a-select>
|
<a-select v-model="group" :bordered="false" :options="groupOptions"></a-select>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -59,14 +51,12 @@
|
||||||
<div v-show="item.status !== 'done'">
|
<div v-show="item.status !== 'done'">
|
||||||
<a-link v-show="item.status === 'uploading'" @click="pauseItem(item)"> 停止 </a-link>
|
<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 === 'error'" @click="retryItem(item)"> 重试 </a-link>
|
||||||
<a-link v-show="item.status === 'init' || item.status === 'error'" @click="removeItem(item)">
|
<a-link v-show="item.status === 'init' || item.status === 'error'" @click="removeItem(item)"> 移除 </a-link>
|
||||||
移除
|
|
||||||
</a-link>
|
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<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'">
|
<span v-if="item.status === 'init'">
|
||||||
<i class="icon-park-outline-lightning"></i>
|
<i class="icon-park-outline-lightning"></i>
|
||||||
等待上传
|
等待上传
|
||||||
|
|
@ -86,14 +76,10 @@
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<span v-if="item.status === 'init'"> </span>
|
<span v-if="item.status === 'init'"> </span>
|
||||||
<span v-else-if="item.status === 'uploading'">
|
<span v-else-if="item.status === 'uploading'"> 速度:{{ formatSpeed(item.uid) }}/s, 进度:{{ formatProgress(item) }} % </span>
|
||||||
速度:{{ formatSpeed(item.uid) }}/s, 进度:{{ formatProgress(item) }} %
|
<span v-else-if="item.status === 'done'"> 耗时:{{ fileMap.get(item.uid)?.cost || 0 }} 秒, 平均:{{ formatAspeed(item.uid) }}/s </span>
|
||||||
</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 v-else="item.status === 'error'"> 原因:{{ fileMap.get(item.uid)?.error }} </span>
|
||||||
</span>
|
</span> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -106,14 +92,10 @@
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex justify-between gap-2 items-center">
|
<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">
|
<div class="space-x-2">
|
||||||
<a-button type="text" :disabled="!fileList.length || Boolean(stat.uploadingCount)" @click="clearUploaded">
|
<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>
|
|
||||||
<a-button type="primary" :disabled="!fileList.length || !stat.initCount" @click="startUpload">
|
|
||||||
开始上传
|
|
||||||
</a-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -306,4 +288,10 @@ const groupOptions = [
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
<style lang="less">
|
||||||
|
.an-upload {
|
||||||
|
.arco-modal-body {
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<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 numeral from 'numeral';
|
||||||
import AnCategory from './AnCategory.vue';
|
import AnCategory from './AnCategory.vue';
|
||||||
import AnPreview from './AnPreview.vue';
|
import AnPreview from './AnPreview.vue';
|
||||||
import AnUpload from './AnUpload.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';
|
import { getIcon } from './util';
|
||||||
|
|
||||||
const current = ref<FileCategory>();
|
const current = ref<FileCategory>();
|
||||||
|
|
@ -116,7 +116,7 @@ const {
|
||||||
width: 150,
|
width: 150,
|
||||||
render: ({ record }) => numeral(record.size).format('0 b'),
|
render: ({ record }) => numeral(record.size).format('0 b'),
|
||||||
},
|
},
|
||||||
useCreateColumn(),
|
// useCreateColumn(),
|
||||||
useUpdateColumn(),
|
useUpdateColumn(),
|
||||||
{
|
{
|
||||||
type: 'button',
|
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>
|
<template>
|
||||||
<div></div>
|
<iframe src="https://nav.juetan.cn" frameborder="0" class="w-full h-full overflow-hidden"></iframe>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<route lang="json">
|
<route lang="json">
|
||||||
{
|
{
|
||||||
"component": null,
|
|
||||||
"meta": {
|
"meta": {
|
||||||
"sort": 120012,
|
"sort": 120012,
|
||||||
"hide": "prod",
|
"hide": "prod",
|
||||||
"title": "前端导航",
|
"title": "前端导航",
|
||||||
"link": "https://nav.juetan.cn",
|
|
||||||
"icon": "icon-park-outline-mail"
|
"icon": "icon-park-outline-mail"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white px-5 py-4 rounded-sm mt-4">
|
<div class="bg-white px-5 py-4 rounded-sm mt-4">
|
||||||
<div>常用服务</div>
|
<div>常用服务</div>
|
||||||
<div class="grid grid-cols-5 justify-between gap-4 mt-4">
|
<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">
|
<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">
|
<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>
|
<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>
|
||||||
<span class="text-gray-400">3天前</span>
|
<span class="text-gray-400">3天前</span>
|
||||||
|
|
@ -71,7 +72,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useUserStore } from '@/store';
|
import { useUserStore } from '@/store/user';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
|
@ -108,9 +109,10 @@ const stat = {
|
||||||
|
|
||||||
<route lang="json">
|
<route lang="json">
|
||||||
{
|
{
|
||||||
|
"alias": "/",
|
||||||
"meta": {
|
"meta": {
|
||||||
"sort": 1000,
|
"sort": 1000,
|
||||||
"title": "首页",
|
"title": "概览",
|
||||||
"icon": "icon-park-outline-home"
|
"icon": "icon-park-outline-home"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<bread-page>
|
<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-form-item label="站点LOGO">
|
||||||
<a-avatar :size="64">
|
<a-avatar :size="64">
|
||||||
<img :src="appStore.logo" alt="" />
|
<img :src="appStore.logo" alt="" />
|
||||||
|
|
@ -10,7 +14,7 @@
|
||||||
</a-avatar>
|
</a-avatar>
|
||||||
<template #help>提示:仅支持 5MB 以内大小, png 或 jpg 格式的图片 </template>
|
<template #help>提示:仅支持 5MB 以内大小, png 或 jpg 格式的图片 </template>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="站点名称">
|
<a-form-item label="站点名称" class="pt-6">
|
||||||
<a-input
|
<a-input
|
||||||
v-model="appStore.title"
|
v-model="appStore.title"
|
||||||
placeholder="请输入"
|
placeholder="请输入"
|
||||||
|
|
@ -21,7 +25,7 @@
|
||||||
></a-input>
|
></a-input>
|
||||||
<template #help> 用作系统内显示的名称,可在后台修改 </template>
|
<template #help> 用作系统内显示的名称,可在后台修改 </template>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="站点描述">
|
<a-form-item label="站点描述" class="pt-6">
|
||||||
<a-textarea
|
<a-textarea
|
||||||
v-model="appStore.subtitle"
|
v-model="appStore.subtitle"
|
||||||
placeholder="请输入"
|
placeholder="请输入"
|
||||||
|
|
@ -31,11 +35,11 @@
|
||||||
></a-textarea>
|
></a-textarea>
|
||||||
<template #help> 启用后,消息通知将在左上角进行提示. </template>
|
<template #help> 启用后,消息通知将在左上角进行提示. </template>
|
||||||
</a-form-item>
|
</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>
|
<a-input v-model="appStore.title" placeholder="请输入" class="!w-[432px]" allow-clear></a-input>
|
||||||
<template #help> 示例:https://www.juetan.cn。用于静态资源前缀、应用接口前缀等用途。 </template>
|
<template #help> 示例:https://www.juetan.cn。用于静态资源前缀、应用接口前缀等用途。 </template>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item class="pt-6">
|
||||||
<a-button type="primary">保存修改</a-button>
|
<a-button type="primary">保存修改</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,6 @@
|
||||||
<AnPage></AnPage>
|
<AnPage></AnPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx"></script>
|
|
||||||
|
|
||||||
<style lang="less"></style>
|
|
||||||
|
|
||||||
<route lang="json">
|
<route lang="json">
|
||||||
{
|
{
|
||||||
"redirect": "/setting/common",
|
"redirect": "/setting/common",
|
||||||
|
|
|
||||||
|
|
@ -1,67 +1,68 @@
|
||||||
<template>
|
<template>
|
||||||
<bread-page>
|
<bread-page>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<a-form
|
<div class="w-full">
|
||||||
:model="{}"
|
<div class="flex item-center justify-between gap-4">
|
||||||
:label-col-props="{ span: 3 }"
|
<div>
|
||||||
:disabled="!mail.enable"
|
<h2 class="m-0 text-lg font-normal flex items-center gap-2">
|
||||||
layout="vertical"
|
邮件设置
|
||||||
label-align="left"
|
<a-tag :color="mail.enable ? 'green' : 'red'">
|
||||||
class="w-[580px]! space-y-6"
|
<template #icon>
|
||||||
>
|
<i class="icon-park-outline-check-one"></i>
|
||||||
<a-form-item label="是否启用" :disabled="false">
|
{{ mail.enable ? '已启用' : '已停用' }}
|
||||||
<a-radio-group v-model="mail.enable">
|
</template>
|
||||||
<a-radio :value="true">启用</a-radio>
|
</a-tag>
|
||||||
<a-radio :value="false">禁用</a-radio>
|
</h2>
|
||||||
</a-radio-group>
|
<p class="text-gray-500 mt-1.5 p-0 m0 m-0">首次为你的帐户添加密码时,你需要前往密码重置页面,以便我们验证你的身份。</p>
|
||||||
</a-form-item>
|
</div>
|
||||||
<a-form-item label="服务器和端口">
|
<div class="flex items-center pr-6">
|
||||||
<a-input v-model="mail.smtpHost" allow-clear placeholder="请输入" class="!w-[314px]"></a-input>
|
|
||||||
<span class="inline-block px-2">:</span>
|
</div>
|
||||||
<a-input-number v-model="mail.smtpPort" :min="0" :max="65535" class="w-24!"></a-input-number>
|
</div>
|
||||||
<template #help>
|
<a-form :model="{}" :disabled="!mail.enable" label-align="left" class="col-form divide-y divide-gray-100 mt-8 space-y-6">
|
||||||
示例: smtp.163.com:25。国内常见有
|
<a-form-item label="是否启用" :disabled="false">
|
||||||
<a target="_blank" class="mr-2" href="https://mail.163.com">网易邮箱</a>
|
<a-switch v-model="mail.enable"> </a-switch>
|
||||||
<a target="_blank" class="mr-2" href="http://mail.aliyun.com/">阿里邮箱</a>
|
<template #help> 启用后其他服务可发送邮件通知。 </template>
|
||||||
<a target="_blank" class="mr-2" href="https://mail.qq.com">QQ邮箱</a>等。
|
</a-form-item>
|
||||||
</template>
|
<a-form-item label="服务器地址和端口" class="pt-6">
|
||||||
</a-form-item>
|
<a-input v-model="mail.smtpHost" allow-clear placeholder="请输入" class="!w-[314px]"></a-input>
|
||||||
<a-form-item label="发信人地址">
|
<span class="inline-block px-2">:</span>
|
||||||
<a-input v-model="mail.smtpFrom" placeholder="请输入" class="!w-[432px]"></a-input>
|
<a-input-number v-model="mail.smtpPort" :min="0" :max="65535" class="w-24!"></a-input-number>
|
||||||
<template #help> 示例: example@mail.com。仅作为发送邮件时的发送人标识,与登陆无关。</template>
|
<template #help>
|
||||||
</a-form-item>
|
示例: smtp.163.com:25。国内常见有
|
||||||
<a-form-item label="是否需要验证">
|
<a target="_blank" class="mr-2" href="https://mail.163.com">网易邮箱</a>
|
||||||
<a-radio-group v-model="mail.smtpAuth">
|
<a target="_blank" class="mr-2" href="http://mail.aliyun.com/">阿里邮箱</a>
|
||||||
<a-radio :value="true">是</a-radio>
|
<a target="_blank" class="mr-2" href="https://mail.qq.com">QQ邮箱</a>等。
|
||||||
<a-radio :value="false">否</a-radio>
|
</template>
|
||||||
</a-radio-group>
|
</a-form-item>
|
||||||
</a-form-item>
|
<a-form-item label="发信人地址" class="pt-6">
|
||||||
<a-form-item label="验证账号">
|
<a-input v-model="mail.smtpFrom" placeholder="请输入" class="!w-[432px]"></a-input>
|
||||||
<a-input
|
<template #help> 示例: example@mail.com。仅作为发送邮件时的发送人标识,与登陆无关。</template>
|
||||||
:disabled="!mail.enable || !mail.smtpAuth"
|
</a-form-item>
|
||||||
v-model="mail.smtpUser"
|
<a-form-item label="是否需要验证" class="pt-6">
|
||||||
placeholder="请输入"
|
<a-switch v-model="mail.smtpAuth"> </a-switch>
|
||||||
class="!w-[432px]"
|
<template #help> 可选 </template>
|
||||||
></a-input>
|
</a-form-item>
|
||||||
<template #help> 示例: example@mail.com。企业邮箱请使用企业域名后缀。</template>
|
<a-form-item label="验证账号" class="pt-6">
|
||||||
</a-form-item>
|
<a-input :disabled="!mail.enable || !mail.smtpAuth" v-model="mail.smtpUser" placeholder="请输入" class="!w-[432px]"></a-input>
|
||||||
<a-form-item label="验证密码">
|
<template #help> 示例: example@mail.com。企业邮箱请使用企业域名后缀。</template>
|
||||||
<a-input
|
</a-form-item>
|
||||||
:disabled="!mail.enable || !mail.smtpAuth"
|
<a-form-item label="验证密码" class="pt-6">
|
||||||
v-model="mail.smtpPass"
|
<a-input :disabled="!mail.enable || !mail.smtpAuth" v-model="mail.smtpPass" placeholder="请输入" class="!w-[432px]"></a-input>
|
||||||
placeholder="请输入"
|
<template #help> 示例:AATOLARFABJKYWUY。具体请在对应邮箱设置面板进行生成。 </template>
|
||||||
class="!w-[432px]"
|
</a-form-item>
|
||||||
></a-input>
|
<a-form-item :disabled="false" class="pt-6">
|
||||||
<template #help> 示例:AATOLARFABJKYWUY。具体请在对应邮箱设置面板进行生成。 </template>
|
<a-button type="primary"> 保存修改 </a-button>
|
||||||
</a-form-item>
|
<a-button class="ml-4">
|
||||||
<a-form-item :disabled="false">
|
测试
|
||||||
<a-button type="primary"> 保存修改 </a-button>
|
</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
<a-divider direction="vertical" :margin="32"></a-divider>
|
</div>
|
||||||
|
<!-- <a-divider direction="vertical" :margin="32"></a-divider>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div>
|
<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="text-gray-400 mt-1">发送一封测试邮件,检测邮件设置是否能正常工作。</div>
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<a-input placeholder="接收人邮箱" class="w-[432px]!"></a-input>
|
<a-input placeholder="接收人邮箱" class="w-[432px]!"></a-input>
|
||||||
|
|
@ -71,7 +72,7 @@
|
||||||
<a-button type="primary" :disabled="!mail.enable">发送邮件</a-button>
|
<a-button type="primary" :disabled="!mail.enable">发送邮件</a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</bread-page>
|
</bread-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from '@/api';
|
import { api } from '@/api';
|
||||||
import { useFormModal } from '@/components/AnForm';
|
import { useFormModal } from '@/components/AnForm';
|
||||||
import { TableColumnRender, useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
import { TableColumnRender, useCreateColumn, useTable } from '@/components/AnTable';
|
||||||
|
|
||||||
defineOptions({ name: 'SystemDepartmentPage' });
|
defineOptions({ name: 'SystemDepartmentPage' });
|
||||||
|
|
||||||
|
|
@ -64,9 +64,6 @@ const { component: UserTable } = useTable({
|
||||||
{
|
{
|
||||||
...useCreateColumn(),
|
...useCreateColumn(),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
...useUpdateColumn(),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
|
|
@ -173,7 +170,6 @@ const { component: UserTable } = useTable({
|
||||||
{
|
{
|
||||||
"meta": {
|
"meta": {
|
||||||
"name": "SystemDepartmentPage",
|
"name": "SystemDepartmentPage",
|
||||||
"keepAlive": true,
|
|
||||||
"sort": 10301,
|
"sort": 10301,
|
||||||
"title": "部门管理",
|
"title": "部门管理",
|
||||||
"icon": "icon-park-outline-group"
|
"icon": "icon-park-outline-group"
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,16 @@
|
||||||
<an-group :current="current" @change="onTypeChange"></an-group>
|
<an-group :current="current" @change="onTypeChange"></an-group>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white p-4">
|
<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">
|
<span class="text-base">
|
||||||
<i class="icon-park-outline-folder-close"></i>
|
<i class="icon-park-outline-folder-close"></i>
|
||||||
{{ current?.name }}
|
{{ current?.name }}
|
||||||
</span>
|
</span>
|
||||||
<div class="mt-1.5 text-gray-500">描述:{{ current?.description }}</div>
|
<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>
|
</div>
|
||||||
<dict-table></dict-table>
|
<dict-table></dict-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<BreadPage>
|
<BreadPage>
|
||||||
<role-table></role-table>
|
<RoleTable></RoleTable>
|
||||||
</BreadPage>
|
</BreadPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -106,6 +106,7 @@ const { component: RoleTable } = useTable({
|
||||||
"name": "SystemRolePage",
|
"name": "SystemRolePage",
|
||||||
"sort": 10302,
|
"sort": 10302,
|
||||||
"title": "角色管理",
|
"title": "角色管理",
|
||||||
|
"auth": ["role"],
|
||||||
"icon": "icon-park-outline-shield"
|
"icon": "icon-park-outline-shield"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from '@/api';
|
import { api } from '@/api';
|
||||||
import { useFormModal } from '@/components/AnForm';
|
import { useFormModal } from '@/components/AnForm';
|
||||||
import { TableColumnRender, useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
import { TableColumnRender, useTable } from '@/components/AnTable';
|
||||||
|
|
||||||
defineOptions({ name: 'SystemUserPage' });
|
defineOptions({ name: 'SystemUserPage' });
|
||||||
|
|
||||||
|
|
@ -32,24 +32,14 @@ const { component: PasswordModal, open } = useFormModal({
|
||||||
|
|
||||||
const usernameRender: TableColumnRender = ({ record }) => (
|
const usernameRender: TableColumnRender = ({ record }) => (
|
||||||
<div class="flex items-center gap-4 w-full overflow-hidden">
|
<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]}
|
{record.avatar?.startsWith('/') ? <img src={record.avatar} alt="" /> : record.nickname?.[0]}
|
||||||
</a-avatar>
|
</a-avatar>
|
||||||
<div class="w-full flex-1 overflow-hidden">
|
<div class="w-full flex-1 overflow-hidden">
|
||||||
<div>
|
<div>
|
||||||
<span class="cursor-pointer hover:text-brand-500">{record.nickname}</span>
|
<span class="cursor-pointer ">{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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -62,16 +52,19 @@ const { component: UserTable } = useTable({
|
||||||
render: usernameRender,
|
render: usernameRender,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...useCreateColumn(),
|
title: '创建',
|
||||||
},
|
render: () => '3 天前'
|
||||||
{
|
|
||||||
...useUpdateColumn(),
|
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// ...useCreateColumn(),
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// ...useUpdateColumn(),
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
width: 200,
|
width: 200,
|
||||||
align: 'right',
|
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: '重置密码',
|
text: '重置密码',
|
||||||
|
|
@ -176,7 +169,6 @@ const { component: UserTable } = useTable({
|
||||||
"cache": true,
|
"cache": true,
|
||||||
"sort": 10301,
|
"sort": 10301,
|
||||||
"title": "用户管理",
|
"title": "用户管理",
|
||||||
"auth": ["*"],
|
|
||||||
"icon": "icon-park-outline-user"
|
"icon": "icon-park-outline-user"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,18 @@
|
||||||
import { api } from '@/api';
|
import { api } from '@/api';
|
||||||
import { env } from '@/config/env';
|
import { env } from '@/config/env';
|
||||||
import { store, useUserStore } from '@/store';
|
import { useUserStore } from '@/store/user';
|
||||||
import { useMenuStore } from '@/store/menu';
|
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 { Notification } from '@arco-design/web-vue';
|
||||||
import { Router } from 'vue-router';
|
import { Router } from 'vue-router';
|
||||||
import { menus } from '../menus';
|
import { menus } from '../menus';
|
||||||
import { APP_HOME_NAME } from '../routes/base';
|
import { appRoutes } from '../routes/page';
|
||||||
import { APP_ROUTE_NAME, routes } from '../routes/page';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 权限守卫
|
* 权限守卫
|
||||||
* @param to 路由
|
* @param to 路由
|
||||||
* @description store不能放在外面,否则 pinia-plugin-peristedstate 插件会失效
|
* @description store不能放在外面,否则 pinia-plugin-peristedstate 插件会失效
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
export function useAuthGuard(router: Router) {
|
export function useAuthGuard(router: Router) {
|
||||||
api.expireHandler = () => {
|
api.expireHandler = () => {
|
||||||
|
|
@ -39,17 +38,17 @@ export function useAuthGuard(router: Router) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 直接访问跳转回首页(非路由跳转)
|
||||||
|
if (!from.matched.length) {
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
|
||||||
// 提示已登陆
|
// 提示已登陆
|
||||||
Notification.warning({
|
Notification.warning({
|
||||||
title: '跳转提示',
|
title: '跳转提示',
|
||||||
content: `您已登陆,如需重新登陆请退出后再操作!`,
|
content: `您已登陆,如需重新登陆请退出后再操作!`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 直接访问跳转回首页(不是从路由跳转)
|
|
||||||
if (!from.matched.length) {
|
|
||||||
return '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 已登陆不允许
|
// 已登陆不允许
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -64,37 +63,24 @@ export function useAuthGuard(router: Router) {
|
||||||
|
|
||||||
// 未获取权限进行获取
|
// 未获取权限进行获取
|
||||||
if (!menuStore.menus.length) {
|
if (!menuStore.menus.length) {
|
||||||
// 菜单处理
|
menuStore.setMenus(menus);
|
||||||
const authMenus = treeFilter(menus, item => {
|
|
||||||
if (item.path === env.homePath) {
|
|
||||||
item.path = '/';
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
menuStore.setMenus(authMenus);
|
|
||||||
menuStore.setHome(env.homePath);
|
menuStore.setHome(env.homePath);
|
||||||
|
|
||||||
// 路由处理
|
treeEach(appRoutes, item => {
|
||||||
for (const route of routes) {
|
|
||||||
router.addRoute(route);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 缓存处理
|
|
||||||
treeEach(routes, (item, level) => {
|
|
||||||
const { cache, name } = item.meta ?? {};
|
const { cache, name } = item.meta ?? {};
|
||||||
if (cache && name) {
|
if (cache && name) {
|
||||||
menuStore.caches.push(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);
|
||||||
|
// }
|
||||||
});
|
});
|
||||||
|
|
||||||
// 首页处理
|
return to.fullPath;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兜底处理
|
// 兜底处理
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { NProgress } from '@/libs/nprogress';
|
import NProgress from 'nprogress';
|
||||||
import { useAppStore } from '@/store';
|
import { useAppStore } from '@/store/app';
|
||||||
import { Router } from 'vue-router';
|
import { Router } from 'vue-router';
|
||||||
|
|
||||||
const routeMap = new Map<string, boolean>();
|
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';
|
import { Router } from 'vue-router';
|
||||||
|
|
||||||
export function useTitleGuard(router: Router) {
|
export function useTitleGuard(router: Router) {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { createRouter } from 'vue-router';
|
||||||
import { useAuthGuard } from '../guards/auth';
|
import { useAuthGuard } from '../guards/auth';
|
||||||
import { useProgressGard } from '../guards/progress';
|
import { useProgressGard } from '../guards/progress';
|
||||||
import { useTitleGuard } from '../guards/title';
|
import { useTitleGuard } from '../guards/title';
|
||||||
import { baseRoutes } from '../routes/base';
|
|
||||||
import { historyMode } from './util';
|
import { historyMode } from './util';
|
||||||
import { routes } from '../routes/page';
|
import { routes } from '../routes/page';
|
||||||
|
|
||||||
|
|
@ -11,7 +10,7 @@ import { routes } from '../routes/page';
|
||||||
*/
|
*/
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
history: historyMode(),
|
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) {
|
for (const item of list) {
|
||||||
const parentPath = item.path.split('/').slice(0, -1).join('/');
|
const parentPath = item.path.split('/').slice(0, -1).join('/');
|
||||||
const parent = map[parentPath];
|
const parent = map[parentPath];
|
||||||
|
item.parentName = (parent?.name as string) || APP_ROUTE_NAME;
|
||||||
if (parent) {
|
if (parent) {
|
||||||
(item as any).parentPath = parentPath;
|
|
||||||
(parent.children || (parent.children = [])).push(item);
|
(parent.children || (parent.children = [])).push(item);
|
||||||
} else {
|
} else {
|
||||||
tree.push(item);
|
tree.push(item);
|
||||||
|
|
@ -47,12 +47,14 @@ function sortRoutes(routes: RouteRecordRaw[]) {
|
||||||
const transformRoutes = (routes: RouteRecordRaw[]) => {
|
const transformRoutes = (routes: RouteRecordRaw[]) => {
|
||||||
const topRoutes: RouteRecordRaw[] = [];
|
const topRoutes: RouteRecordRaw[] = [];
|
||||||
const appRoutes: RouteRecordRaw[] = [];
|
const appRoutes: RouteRecordRaw[] = [];
|
||||||
|
let app: RouteRecordRaw;
|
||||||
|
|
||||||
for (const route of routes) {
|
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 as string)?.startsWith(TOP_ROUTE_PREF)) {
|
||||||
if (route.name === APP_ROUTE_NAME) {
|
|
||||||
route.children = appRoutes;
|
|
||||||
}
|
|
||||||
route.path = route.path.replace(TOP_ROUTE_PREF, '');
|
route.path = route.path.replace(TOP_ROUTE_PREF, '');
|
||||||
topRoutes.push(route);
|
topRoutes.push(route);
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -60,7 +62,8 @@ const transformRoutes = (routes: RouteRecordRaw[]) => {
|
||||||
appRoutes.push(route);
|
appRoutes.push(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [topRoutes, sortRoutes(treeRoutes(appRoutes))];
|
app!.children = sortRoutes(treeRoutes(appRoutes));
|
||||||
|
return [topRoutes, app!.children];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const [routes, appRoutes] = transformRoutes(generatedRoutes);
|
export const [routes, appRoutes] = transformRoutes(generatedRoutes);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1 @@
|
||||||
export * from './app';
|
|
||||||
export * from './store';
|
export * from './store';
|
||||||
export * from './user';
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
export const useUserStore = defineStore({
|
export const useUserStore = defineStore({
|
||||||
id: "user",
|
id: 'user',
|
||||||
state: (): UserStore => {
|
state: (): UserStore => {
|
||||||
return {
|
return {
|
||||||
id: 0,
|
id: 0,
|
||||||
username: "juetan",
|
username: 'juetan',
|
||||||
nickname: "绝弹",
|
nickname: '绝弹',
|
||||||
avatar: "https://github.com/juetan.png",
|
avatar: 'https://github.com/juetan.png',
|
||||||
accessToken: "",
|
accessToken: '',
|
||||||
refreshToken: undefined,
|
refreshToken: undefined,
|
||||||
auth: []
|
auth: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
|
@ -48,7 +48,10 @@ export const useUserStore = defineStore({
|
||||||
accessToken && (this.accessToken = accessToken);
|
accessToken && (this.accessToken = accessToken);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
persist: true,
|
persist: {
|
||||||
|
key: '__APP_USER__',
|
||||||
|
paths: ['accessToken'],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface UserStore {
|
export interface UserStore {
|
||||||
|
|
@ -65,11 +68,11 @@ export interface UserStore {
|
||||||
*/
|
*/
|
||||||
nickname: string;
|
nickname: string;
|
||||||
/**
|
/**
|
||||||
* 用户头像地址
|
* 头像地址
|
||||||
*/
|
*/
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
/**
|
/**
|
||||||
* JWT令牌
|
* 访问令牌
|
||||||
*/
|
*/
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
@arcoblue-6: #08f;
|
@arcoblue-6: #08f;
|
||||||
|
|
||||||
body {
|
body {
|
||||||
// --border-radius-small: 4px;
|
--border-radius-small: 2px;
|
||||||
|
|
||||||
.arco-table .arco-table-element {
|
.arco-table .arco-table-element {
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
|
|
@ -62,20 +62,20 @@ body {
|
||||||
.arco-menu {
|
.arco-menu {
|
||||||
&.arco-menu-vertical .arco-menu-item {
|
&.arco-menu-vertical .arco-menu-item {
|
||||||
line-height: 36px;
|
line-height: 36px;
|
||||||
margin-top: 2px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
&.arco-menu-vertical .arco-menu-group-title {
|
&.arco-menu-vertical .arco-menu-group-title {
|
||||||
line-height: 28px;
|
line-height: 28px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
[class^="icon-"] {
|
[class^="icon-"] {
|
||||||
font-size: 14px;
|
font-size: 18px;
|
||||||
vertical-align: -2px;
|
vertical-align: -2px;
|
||||||
}
|
}
|
||||||
.arco-menu-item {
|
.arco-menu-item {
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--color-neutral-1);
|
background-color: var(--color-neutral-2);
|
||||||
}
|
}
|
||||||
&.arco-menu-selected {
|
&.arco-menu-selected {
|
||||||
// color: @arcoblue-6;
|
// color: @arcoblue-6;
|
||||||
|
|
@ -135,6 +135,10 @@ body {
|
||||||
.an-form-modal .arco-modal-body {
|
.an-form-modal .arco-modal-body {
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.arco-form-item-message {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.dark {
|
.dark {
|
||||||
.arco-menu-item.arco-menu-selected {
|
.arco-menu-item.arco-menu-selected {
|
||||||
|
|
@ -155,3 +159,16 @@ body {
|
||||||
border-color: var(--color-neutral-2);
|
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 {
|
export interface GlobalComponents {
|
||||||
AAutoComplete: typeof import('@arco-design/web-vue')['AutoComplete']
|
AAutoComplete: typeof import('@arco-design/web-vue')['AutoComplete']
|
||||||
AAvatar: typeof import('@arco-design/web-vue')['Avatar']
|
AAvatar: typeof import('@arco-design/web-vue')['Avatar']
|
||||||
ABadge: typeof import('@arco-design/web-vue')['Badge']
|
|
||||||
ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb']
|
ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb']
|
||||||
ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem']
|
ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem']
|
||||||
AButton: typeof import('@arco-design/web-vue')['Button']
|
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/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>>,
|
'/content/post/': RouteRecordInfo<'/content/post/', '/content/post', Record<never, never>, Record<never, never>>,
|
||||||
'/dev/': RouteRecordInfo<'/dev/', '/dev', 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/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>>,
|
'/dev/openapi/': RouteRecordInfo<'/dev/openapi/', '/dev/openapi', Record<never, never>, Record<never, never>>,
|
||||||
'/home/': RouteRecordInfo<'/home/', '/home', 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/': 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>>,
|
'/log/operation/': RouteRecordInfo<'/log/operation/', '/log/operation', Record<never, never>, Record<never, never>>,
|
||||||
'/setting/': RouteRecordInfo<'/setting/', '/setting', 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>>,
|
'/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 {
|
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 {
|
interface ComponentCustomProperties {
|
||||||
$router: Router;
|
$router: Router;
|
||||||
$route: RouteLocationNormalizedLoaded;
|
$route: RouteLocationNormalizedLoaded;
|
||||||
|
$dayjs: Dayjs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,6 @@ export default defineConfig(({ mode }) => {
|
||||||
brand: arcoToUnoColor('primary'),
|
brand: arcoToUnoColor('primary'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: ['src/**/*.{vue,ts,tsx,css,scss,sass,less,styl}'],
|
|
||||||
presets: [
|
presets: [
|
||||||
presetUno(),
|
presetUno(),
|
||||||
presetIcons({
|
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