init
5
.dockerignore
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
11
.editorconfig
Normal file
@ -0,0 +1,11 @@
|
||||
# 🎨 editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
5
.env
Normal file
@ -0,0 +1,5 @@
|
||||
# 应用名称
|
||||
VITE_NAME = "COOL MALL"
|
||||
|
||||
# 网络超时请求时间
|
||||
VITE_TIMEOUT = 30000
|
3
.eslintignore
Normal file
@ -0,0 +1,3 @@
|
||||
packages/
|
||||
dist/
|
||||
node_modules/
|
47
.eslintrc.js
Normal file
@ -0,0 +1,47 @@
|
||||
/* eslint-env node */
|
||||
require("@rushstack/eslint-patch/modern-module-resolution");
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended",
|
||||
"@vue/eslint-config-typescript",
|
||||
"@vue/eslint-config-prettier/skip-formatting"
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest"
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/ban-ts-ignore": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"vue/no-mutating-props": "off",
|
||||
"vue/component-name-in-template-casing": ["error", "kebab-case"],
|
||||
"vue/component-definition-name-casing": ["error", "kebab-case"],
|
||||
"no-use-before-define": "off",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-namespace": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"space-before-function-paren": "off",
|
||||
"vue/attributes-order": "off",
|
||||
"vue/one-component-per-file": "off",
|
||||
"vue/html-closing-bracket-newline": "off",
|
||||
"vue/max-attributes-per-line": "off",
|
||||
"vue/multiline-html-element-content-newline": "off",
|
||||
"vue/multi-word-component-names": "off",
|
||||
"vue/singleline-html-element-content-newline": "off",
|
||||
"vue/attribute-hyphenation": "off",
|
||||
"vue/html-self-closing": "off",
|
||||
"vue/require-default-prop": "off",
|
||||
"vue/v-on-event-hyphenation": "off",
|
||||
"no-self-assign": "off"
|
||||
}
|
||||
};
|
4
.gitattributes
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
*.js text eol=lf
|
||||
*.json text eol=lf
|
||||
*.ts text eol=lf
|
||||
*.vue text eol=lf
|
18
.gitignore
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
.DS_Store
|
||||
node_modules/
|
||||
/dist/
|
||||
dist-ssr/
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.project
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw*
|
16
.hintrc
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": [
|
||||
"development"
|
||||
],
|
||||
"hints": {
|
||||
"meta-viewport": "off",
|
||||
"axe/text-alternatives": [
|
||||
"default",
|
||||
{
|
||||
"document-title": "off"
|
||||
}
|
||||
],
|
||||
"disown-opener": "off",
|
||||
"css-prefix-order": "off"
|
||||
}
|
||||
}
|
8
.prettierrc
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"tabWidth": 4,
|
||||
"useTabs": true,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none"
|
||||
}
|
15
.vscode/config.code-snippets
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"module-config": {
|
||||
"prefix": "module-config",
|
||||
"scope": "typescript",
|
||||
"body": [
|
||||
"import type { ModuleConfig } from \"/@/cool\";",
|
||||
"",
|
||||
"export default (): ModuleConfig => {",
|
||||
" return {};",
|
||||
"};",
|
||||
""
|
||||
],
|
||||
"description": "module config snippets"
|
||||
}
|
||||
}
|
102
.vscode/crud.code-snippets
vendored
Normal file
@ -0,0 +1,102 @@
|
||||
{
|
||||
"cl-crud": {
|
||||
"prefix": "cl-crud",
|
||||
"scope": "vue",
|
||||
"body": [
|
||||
"<template>",
|
||||
" <cl-crud ref=\"Crud\">",
|
||||
" <cl-row>",
|
||||
" <!-- 刷新按钮 -->",
|
||||
" <cl-refresh-btn />",
|
||||
" <!-- 新增按钮 -->",
|
||||
" <cl-add-btn />",
|
||||
" <!-- 删除按钮 -->",
|
||||
" <cl-multi-delete-btn />",
|
||||
" <cl-flex1 />",
|
||||
" <!-- 关键字搜索 -->",
|
||||
" <cl-search-key />",
|
||||
" </cl-row>",
|
||||
"",
|
||||
" <cl-row>",
|
||||
" <!-- 数据表格 -->",
|
||||
" <cl-table ref=\"Table\" />",
|
||||
" </cl-row>",
|
||||
"",
|
||||
" <cl-row>",
|
||||
" <cl-flex1 />",
|
||||
" <!-- 分页控件 -->",
|
||||
" <cl-pagination />",
|
||||
" </cl-row>",
|
||||
"",
|
||||
" <!-- 新增、编辑 -->",
|
||||
" <cl-upsert ref=\"Upsert\" />",
|
||||
" </cl-crud>",
|
||||
"</template>",
|
||||
"",
|
||||
"<script lang=\"ts\" name=\"菜单名称\" setup>",
|
||||
"import { useCrud, useTable, useUpsert } from \"@cool-vue/crud\";",
|
||||
"import { useCool } from \"/@/cool\";",
|
||||
"",
|
||||
"const { service } = useCool();",
|
||||
"",
|
||||
"// cl-upsert",
|
||||
"const Upsert = useUpsert({",
|
||||
" items: []",
|
||||
"});",
|
||||
"",
|
||||
"// cl-table",
|
||||
"const Table = useTable({",
|
||||
" columns: []",
|
||||
"});",
|
||||
"",
|
||||
"// cl-crud",
|
||||
"const Crud = useCrud(",
|
||||
" {",
|
||||
" service: service.demo.goods",
|
||||
" },",
|
||||
" (app) => {",
|
||||
" app.refresh();",
|
||||
" }",
|
||||
");",
|
||||
"",
|
||||
"// 刷新",
|
||||
"function refresh(params?: any) {",
|
||||
" Crud.value?.refresh(params);",
|
||||
"}",
|
||||
"</script>",
|
||||
""
|
||||
],
|
||||
"description": "cl-crud snippets"
|
||||
},
|
||||
"cl-filter": {
|
||||
"prefix": "cl-filter",
|
||||
"scope": "html",
|
||||
"body": [
|
||||
"<cl-filter label=\"\">",
|
||||
" <cl-select :options=\"[$1]\" prop=\"\" />",
|
||||
"</cl-filter>"
|
||||
],
|
||||
"description": "cl-filter snippets"
|
||||
},
|
||||
"item": {
|
||||
"prefix": "item",
|
||||
"scope": "typescript",
|
||||
"body": [
|
||||
"{",
|
||||
" label: \"$1\",",
|
||||
" prop: \"\",",
|
||||
" component: {",
|
||||
" name: \"\"",
|
||||
" }",
|
||||
"},",
|
||||
""
|
||||
],
|
||||
"description": "item snippets"
|
||||
},
|
||||
"column": {
|
||||
"prefix": "column",
|
||||
"scope": "typescript",
|
||||
"body": ["{", " label: \"$1\",", " prop: \"\",", "},", ""],
|
||||
"description": "column snippets"
|
||||
}
|
||||
}
|
4
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"editor.cursorSmoothCaretAnimation": "on",
|
||||
"editor.formatOnSave": true,
|
||||
}
|
16
Dockerfile
Normal file
@ -0,0 +1,16 @@
|
||||
FROM node:lts-alpine
|
||||
WORKDIR /build
|
||||
# 设置Node-Sass的镜像地址
|
||||
RUN npm config set sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
|
||||
# 设置npm镜像
|
||||
RUN npm config set registry https://registry.npm.taobao.org
|
||||
COPY package.json /build/package.json
|
||||
RUN yarn
|
||||
COPY ./ /build
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx
|
||||
RUN mkdir /app
|
||||
COPY --from=0 /build/dist /app
|
||||
COPY --from=0 /build/nginx.conf /etc/nginx/nginx.conf
|
||||
EXPOSE 80
|
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 cool-team-official
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
57
README.md
Normal file
@ -0,0 +1,57 @@
|
||||
# cool-admin [vue3 - ts - vite]
|
||||
|
||||
<p align="center">
|
||||
<a href="https://show.cool-admin.com/" target="blank"><img src="https://admin.cool-js.com/logo.png" width="200" alt="cool-admin Logo" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">cool-admin 一个很酷的后台权限管理系统,开源免费,模块化、插件化、极速开发 CRUD,方便快速构建迭代后台管理系统, 到<a href="https://cool-js.com" target="_blank">文档</a> 进一步了解</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/cool-team-official/cool-admin-vue/blob/master/LICENSE" target="_blank"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="GitHub license" />
|
||||
<a href=""><img src="https://img.shields.io/github/package-json/v/cool-team-official/cool-admin-vue?style=flat-square" alt="GitHub tag"></a>
|
||||
<img src="https://img.shields.io/github/last-commit/cool-team-official/cool-admin-vue?style=flat-square" alt="GitHub tag"></a>
|
||||
</p>
|
||||
|
||||
## 地址
|
||||
|
||||
- [📌 v6 vue3 + element-plus + ts + vite](https://github.com/cool-team-official/cool-admin-vue/tree/6.x)
|
||||
|
||||
- [⚡️ v5 vue3 + element-plus + ts + vite](https://github.com/cool-team-official/cool-admin-vue/tree/5.x)
|
||||
|
||||
- [🌐 码云仓库地址](https://gitee.com/cool-team-official/cool-admin-vue)
|
||||
|
||||
## 演示
|
||||
|
||||
[https://show.cool-admin.com](https://show.cool-admin.com)
|
||||
|
||||
账户:admin,密码:123456
|
||||
|
||||
<img src="https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/home-mini.png" alt="Admin Home" ></a>
|
||||
|
||||
## 项目后端
|
||||
|
||||
[https://github.com/cool-team-official/cool-admin-midway](https://github.com/cool-team-official/cool-admin-midway)
|
||||
|
||||
## 微信群
|
||||
|
||||
<img width="260" src="https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/wechat.jpeg" alt="Admin Wechat"></a>
|
||||
|
||||
## 安装项目依赖
|
||||
|
||||
推荐使用 `yarn`:
|
||||
|
||||
```shell
|
||||
yarn
|
||||
```
|
||||
|
||||
## 运行应用程序
|
||||
|
||||
安装过程完成后,运行以下命令启动服务。您可以在浏览器中预览网站 [http://localhost:9000](http://localhost:9000)
|
||||
|
||||
```shell
|
||||
yarn dev
|
||||
```
|
||||
|
||||
### 低价服务器
|
||||
|
||||
[阿里云、腾讯云、华为云低价云服务器,不限新老](https://cool-js.com/ad/server.html)
|
3475
build/cool/eps.d.ts
vendored
Normal file
1
build/cool/eps.json
Normal file
172
index.html
Normal file
@ -0,0 +1,172 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="referer" content="never" />
|
||||
<meta name="renderer" content="webkit" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0"
|
||||
/>
|
||||
<title></title>
|
||||
<link rel="icon" href="./favicon.ico" />
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
|
||||
"Microsoft YaHei", "微软雅黑", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.preload__wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
letter-spacing: 1px;
|
||||
background-color: #2f3447;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 9999;
|
||||
transition: all 0.3s ease-in;
|
||||
opacity: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.preload__wrap.is-hide {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.preload__container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.preload__name {
|
||||
font-size: 30px;
|
||||
color: #fff;
|
||||
letter-spacing: 5px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.preload__title {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
margin: 30px 0 20px 0;
|
||||
}
|
||||
|
||||
.preload__sub-title {
|
||||
color: #ababab;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.preload__footer {
|
||||
text-align: center;
|
||||
padding: 10px 0 20px 0;
|
||||
}
|
||||
|
||||
.preload__footer a {
|
||||
font-size: 12px;
|
||||
color: #ababab;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.preload__loading {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
border-radius: 30px;
|
||||
border: 7px solid currentColor;
|
||||
border-bottom-color: #2f3447 !important;
|
||||
position: relative;
|
||||
animation: r 1s infinite cubic-bezier(0.17, 0.67, 0.83, 0.67),
|
||||
bc 2s infinite ease-in;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
@keyframes r {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.preload__loading::after,
|
||||
.preload__loading::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
height: 7px;
|
||||
width: 7px;
|
||||
border-radius: 10px;
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
.preload__loading::after {
|
||||
left: -1px;
|
||||
}
|
||||
|
||||
.preload__loading::before {
|
||||
right: -1px;
|
||||
}
|
||||
|
||||
@keyframes bc {
|
||||
0% {
|
||||
color: #689cc5;
|
||||
}
|
||||
|
||||
25% {
|
||||
color: #b3b7e2;
|
||||
}
|
||||
|
||||
50% {
|
||||
color: #93dbe9;
|
||||
}
|
||||
|
||||
75% {
|
||||
color: #abbd81;
|
||||
}
|
||||
|
||||
100% {
|
||||
color: #689cc5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="preload__wrap" id="Loading">
|
||||
<div class="preload__container">
|
||||
<p class="preload__name">%VITE_NAME%</p>
|
||||
<div class="preload__loading"></div>
|
||||
<p class="preload__title">正在加载资源...</p>
|
||||
<p class="preload__sub-title">初次加载资源可能需要较多时间 请耐心等待</p>
|
||||
</div>
|
||||
|
||||
<div class="preload__footer">
|
||||
<a href="https://cool-js.com" target="_blank"> https://cool-js.com </a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
123
nginx.conf
Normal file
@ -0,0 +1,123 @@
|
||||
user nginx;
|
||||
worker_processes 1;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
access_log /var/log/nginx/access.log main;
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
upstream backend {
|
||||
server midway:8001;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
location / {
|
||||
root /app;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
location /api/
|
||||
{
|
||||
proxy_pass http://backend/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header REMOTE-HOST $remote_addr;
|
||||
|
||||
#缓存相关配置
|
||||
#proxy_cache cache_one;
|
||||
#proxy_cache_key $host$request_uri$is_args$args;
|
||||
#proxy_cache_valid 200 304 301 302 1h;
|
||||
|
||||
#持久化连接相关配置
|
||||
proxy_connect_timeout 3000s;
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 3000s;
|
||||
#proxy_http_version 1.1;
|
||||
#proxy_set_header Upgrade $http_upgrade;
|
||||
#proxy_set_header Connection "upgrade";
|
||||
|
||||
add_header X-Cache $upstream_cache_status;
|
||||
|
||||
#expires 12h;
|
||||
}
|
||||
# location /im {
|
||||
# proxy_pass http://backend/im;
|
||||
# proxy_connect_timeout 3600s; #配置点1
|
||||
# proxy_read_timeout 3600s; #配置点2,如果没效,可以考虑这个时间配置长一点
|
||||
# proxy_send_timeout 3600s; #配置点3
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header REMOTE-HOST $remote_addr;
|
||||
# #proxy_bind $remote_addr transparent;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Upgrade $http_upgrade;
|
||||
# proxy_set_header Connection "upgrade";
|
||||
# # rewrite /socket/(.*) /$1 break;
|
||||
# proxy_redirect off;
|
||||
|
||||
# }
|
||||
|
||||
# location /socket {
|
||||
# proxy_pass http://backend/socket;
|
||||
# proxy_connect_timeout 3600s; #配置点1
|
||||
# proxy_read_timeout 3600s; #配置点2,如果没效,可以考虑这个时间配置长一点
|
||||
# proxy_send_timeout 3600s; #配置点3
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header REMOTE-HOST $remote_addr;
|
||||
# #proxy_bind $remote_addr transparent;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Upgrade $http_upgrade;
|
||||
# proxy_set_header Connection "upgrade";
|
||||
# rewrite /socket/(.*) /$1 break;
|
||||
# proxy_redirect off;
|
||||
|
||||
# }
|
||||
|
||||
|
||||
location /adminer/
|
||||
{
|
||||
proxy_pass http://adminer:8080/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header REMOTE-HOST $remote_addr;
|
||||
|
||||
#缓存相关配置
|
||||
#proxy_cache cache_one;
|
||||
#proxy_cache_key $host$request_uri$is_args$args;
|
||||
#proxy_cache_valid 200 304 301 302 1h;
|
||||
|
||||
#持久化连接相关配置
|
||||
proxy_connect_timeout 3000s;
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 3000s;
|
||||
#proxy_http_version 1.1;
|
||||
#proxy_set_header Upgrade $http_upgrade;
|
||||
#proxy_set_header Connection "upgrade";
|
||||
|
||||
add_header X-Cache $upstream_cache_status;
|
||||
|
||||
#expires 12h;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
64
package.json
Normal file
@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "cool-admin",
|
||||
"version": "7.2.0",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"format": "prettier --write src/",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .eslintignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cool-vue/crud": "^7.2.0",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@vueuse/core": "^10.4.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"axios": "^1.7.2",
|
||||
"chardet": "^2.0.0",
|
||||
"core-js": "^3.32.1",
|
||||
"dayjs": "^1.11.10",
|
||||
"echarts": "^5.4.3",
|
||||
"element-plus": "^2.7.7",
|
||||
"file-saver": "^2.0.5",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^11.1.1",
|
||||
"mitt": "^3.0.1",
|
||||
"mockjs": "^1.1.0",
|
||||
"monaco-editor": "0.49.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.1.7",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"store": "^2.0.12",
|
||||
"vue": "^3.4.15",
|
||||
"vue-echarts": "^6.6.1",
|
||||
"vue-router": "^4.4.0",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cool-vue/vite-plugin": "^7.2.1",
|
||||
"@rushstack/eslint-patch": "^1.8.0",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/lodash-es": "^4.17.8",
|
||||
"@types/mockjs": "^1.0.7",
|
||||
"@types/node": "^20.14.5",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/store": "^2.0.2",
|
||||
"@vitejs/plugin-vue": "^5.0.3",
|
||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||
"@vue/compiler-sfc": "^3.4.15",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"@vue/eslint-config-typescript": "^13.0.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.23.0",
|
||||
"prettier": "^3.3.3",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"sass": "^1.53.0",
|
||||
"terser": "^5.27.0",
|
||||
"typescript": "^5.4.0",
|
||||
"vite": "^5.3.4",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-vue-devtools": "^7.3.1"
|
||||
}
|
||||
}
|
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
public/logo.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
public/用户导入模版.xlsx
Normal file
10
src/App.vue
Normal file
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<el-config-provider :locale="zhCn">
|
||||
<router-view />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ElConfigProvider } from "element-plus";
|
||||
import zhCn from "element-plus/dist/locale/zh-cn.mjs";
|
||||
</script>
|
20
src/config/dev.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { getUrlParam, storage } from "/@/cool/utils";
|
||||
import { proxy } from "./proxy";
|
||||
|
||||
export default {
|
||||
// 根地址
|
||||
host: proxy["/dev/"].target,
|
||||
|
||||
// 请求地址
|
||||
get baseUrl() {
|
||||
let proxy = getUrlParam("proxy");
|
||||
|
||||
if (proxy) {
|
||||
storage.set("proxy", proxy);
|
||||
} else {
|
||||
proxy = storage.get("proxy") || "dev";
|
||||
}
|
||||
|
||||
return `/${proxy}`;
|
||||
}
|
||||
};
|
60
src/config/index.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import dev from "./dev";
|
||||
import prod from "./prod";
|
||||
|
||||
// 是否开发模式
|
||||
export const isDev = import.meta.env.DEV;
|
||||
|
||||
// 配置
|
||||
export const config = {
|
||||
// 项目信息
|
||||
app: {
|
||||
name: import.meta.env.VITE_NAME,
|
||||
|
||||
// 菜单
|
||||
menu: {
|
||||
// 是否分组显示
|
||||
isGroup: false,
|
||||
// 自定义菜单列表
|
||||
list: []
|
||||
},
|
||||
|
||||
// 路由
|
||||
router: {
|
||||
// 模式
|
||||
mode: "history",
|
||||
// 转场动画
|
||||
transition: "slide"
|
||||
},
|
||||
|
||||
// 字体图标库
|
||||
iconfont: []
|
||||
},
|
||||
|
||||
// 忽略规则
|
||||
ignore: {
|
||||
// 不显示请求进度条
|
||||
NProgress: [
|
||||
"/__cool_eps",
|
||||
"/base/open/eps",
|
||||
"/base/comm/person",
|
||||
"/base/comm/permmenu",
|
||||
"/base/comm/upload",
|
||||
"/base/comm/uploadMode",
|
||||
"/dict/info/data",
|
||||
"/space/info/add"
|
||||
],
|
||||
// 页面不需要登录验证
|
||||
token: ["/login", "/401", "/403", "/404", "/500", "/502"]
|
||||
},
|
||||
|
||||
// 调试
|
||||
test: {
|
||||
token: "",
|
||||
eps: true
|
||||
},
|
||||
|
||||
// 当前环境
|
||||
...(isDev ? dev : prod)
|
||||
};
|
||||
|
||||
export * from "./proxy";
|
9
src/config/prod.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { proxy } from "./proxy";
|
||||
|
||||
export default {
|
||||
// 根地址
|
||||
host: proxy["/prod/"].target,
|
||||
|
||||
// 请求地址
|
||||
baseUrl: "/api"
|
||||
};
|
13
src/config/proxy.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export const proxy = {
|
||||
"/dev/": {
|
||||
target: "http://127.0.0.1:8001",
|
||||
changeOrigin: true,
|
||||
rewrite: (path: string) => path.replace(/^\/dev/, "")
|
||||
},
|
||||
|
||||
"/prod/": {
|
||||
target: "https://show.cool-admin.com",
|
||||
changeOrigin: true,
|
||||
rewrite: (path: string) => path.replace(/^\/prod/, "/api")
|
||||
}
|
||||
};
|
129
src/cool/bootstrap/eps.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import { cloneDeep, merge } from "lodash-es";
|
||||
import { BaseService, service } from "../service";
|
||||
import { Module } from "../types";
|
||||
import { path2Obj } from "../utils";
|
||||
import { config, isDev } from "/@/config";
|
||||
import { eps } from "virtual:eps";
|
||||
import { hmr } from "../hooks";
|
||||
import { module } from "../module";
|
||||
|
||||
// 更新事件
|
||||
function onUpdate() {
|
||||
// 设置 request 方法
|
||||
function set(d: any) {
|
||||
if (d.namespace) {
|
||||
const a = new BaseService(d.namespace);
|
||||
|
||||
for (const i in d) {
|
||||
const { path, method = "get" } = d[i];
|
||||
|
||||
if (path) {
|
||||
a.request = a.request;
|
||||
|
||||
a[i] = function (data?: any) {
|
||||
return this.request({
|
||||
url: path,
|
||||
method,
|
||||
[method.toLocaleLowerCase() == "post" ? "data" : "params"]: data
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (const i in a) {
|
||||
d[i] = a[i];
|
||||
}
|
||||
} else {
|
||||
for (const i in d) {
|
||||
set(d[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历每一个方法
|
||||
set(eps.service);
|
||||
|
||||
// 合并 eps
|
||||
merge(service, eps.service);
|
||||
|
||||
// 合并[local]
|
||||
merge(
|
||||
service,
|
||||
cloneDeep(
|
||||
path2Obj(
|
||||
module.list.reduce((a, b) => {
|
||||
return a.concat(...((b.services as any[]) || []));
|
||||
}, [])
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// 热更新处理
|
||||
hmr.setData("service", service);
|
||||
|
||||
// 提示
|
||||
if (isDev) {
|
||||
console.log("[cool-eps] updated");
|
||||
}
|
||||
}
|
||||
|
||||
export function createEps(modules: Module[]) {
|
||||
// 更新 eps
|
||||
onUpdate();
|
||||
|
||||
// 开发环境下,生成本地 service 的类型描述文件
|
||||
if (isDev && config.test.eps) {
|
||||
const list: any[] = [];
|
||||
|
||||
// 模拟 eps 数据
|
||||
modules.forEach((m) => {
|
||||
m.services?.forEach((s) => {
|
||||
const api = Array.from(
|
||||
new Set([
|
||||
...Object.getOwnPropertyNames(s.value.constructor.prototype),
|
||||
"page",
|
||||
"list",
|
||||
"info",
|
||||
"delete",
|
||||
"update",
|
||||
"add"
|
||||
])
|
||||
)
|
||||
.filter((e) => !["constructor", "namespace"].includes(e))
|
||||
.map((e) => {
|
||||
return {
|
||||
path: `/${e}`
|
||||
};
|
||||
});
|
||||
|
||||
list.push({
|
||||
api,
|
||||
module: m.name,
|
||||
name: s.value.constructor.name + "Entity",
|
||||
prefix: `/admin/${s.path}`,
|
||||
isLocal: true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 生成文件
|
||||
service.request({
|
||||
url: "/__cool_eps",
|
||||
method: "POST",
|
||||
proxy: false,
|
||||
data: {
|
||||
list
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 vite 触发事件
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.on("eps-update", ({ service }) => {
|
||||
if (service) {
|
||||
eps.service = service;
|
||||
}
|
||||
onUpdate();
|
||||
});
|
||||
}
|
24
src/cool/bootstrap/index.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { createPinia } from "pinia";
|
||||
import { App } from "vue";
|
||||
import { createModule } from "./module";
|
||||
import { router } from "../router";
|
||||
import { Loading } from "../utils";
|
||||
import { createEps } from "./eps";
|
||||
import "virtual:svg-register";
|
||||
|
||||
export async function bootstrap(app: App) {
|
||||
// pinia
|
||||
app.use(createPinia());
|
||||
|
||||
// 路由
|
||||
app.use(router);
|
||||
|
||||
// 模块
|
||||
const { eventLoop, list } = createModule(app);
|
||||
|
||||
// eps
|
||||
createEps(list);
|
||||
|
||||
// 加载
|
||||
Loading.set([eventLoop()]);
|
||||
}
|
116
src/cool/bootstrap/module.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import { type App, type Directive } from "vue";
|
||||
import { assign, isFunction, orderBy } from "lodash-es";
|
||||
import { filename } from "../utils";
|
||||
import { module } from "../module";
|
||||
import { hmr } from "../hooks";
|
||||
|
||||
// 扫描文件
|
||||
const files = import.meta.glob("/src/{modules,plugins}/*/{config.ts,service/**,directives/**}", {
|
||||
eager: true,
|
||||
import: "default"
|
||||
});
|
||||
|
||||
// 模块列表
|
||||
module.list = hmr.getData("modules", []);
|
||||
|
||||
// 解析
|
||||
for (const i in files) {
|
||||
// 分割
|
||||
const [, , type, name, action] = i.split("/");
|
||||
|
||||
// 文件名
|
||||
const n = filename(i);
|
||||
|
||||
// 文件内容
|
||||
const v = files[i];
|
||||
|
||||
// 模块是否存在
|
||||
const m = module.get(name);
|
||||
|
||||
// 数据
|
||||
const d = m || {
|
||||
name,
|
||||
type,
|
||||
value: null,
|
||||
services: [],
|
||||
directives: []
|
||||
};
|
||||
|
||||
// 配置
|
||||
if (action == "config.ts") {
|
||||
d.value = v;
|
||||
}
|
||||
// 服务
|
||||
else if (action == "service") {
|
||||
const s = new (v as any)();
|
||||
|
||||
if (s) {
|
||||
d.services?.push({
|
||||
path: s.namespace,
|
||||
value: s
|
||||
});
|
||||
}
|
||||
}
|
||||
// 指令
|
||||
else if (action == "directives") {
|
||||
d.directives?.push({ name: n, value: v as Directive });
|
||||
}
|
||||
|
||||
if (!m) {
|
||||
module.add(d);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建
|
||||
export function createModule(app: App) {
|
||||
// 排序
|
||||
module.list.forEach((e) => {
|
||||
const d = isFunction(e.value) ? e.value(app) : e.value;
|
||||
|
||||
if (d) {
|
||||
assign(e, d);
|
||||
}
|
||||
|
||||
if (!d.order) {
|
||||
e.order = 0;
|
||||
}
|
||||
});
|
||||
|
||||
const list = orderBy(module.list, "order", "desc").map((e) => {
|
||||
// 初始化
|
||||
e.install?.(app, e.options);
|
||||
|
||||
// 注册组件
|
||||
e.components?.forEach(async (c) => {
|
||||
// @ts-ignore
|
||||
const v = await (isFunction(c) ? c() : c);
|
||||
const n = v.default || v;
|
||||
|
||||
if (n.name) {
|
||||
app.component(n.name, n);
|
||||
}
|
||||
});
|
||||
|
||||
// 注册指令
|
||||
e.directives?.forEach((v) => {
|
||||
app.directive(v.name, v.value);
|
||||
});
|
||||
|
||||
return e;
|
||||
});
|
||||
|
||||
return {
|
||||
// 模块列表
|
||||
list,
|
||||
// 事件加载
|
||||
async eventLoop() {
|
||||
const events: any = {};
|
||||
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (list[i].onLoad) {
|
||||
assign(events, await list[i]?.onLoad?.(events));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
30
src/cool/hooks/browser.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { useEventListener } from "@vueuse/core";
|
||||
import { reactive, watch } from "vue";
|
||||
import { getBrowser } from "../utils";
|
||||
|
||||
const browser = reactive(getBrowser());
|
||||
const events: (() => void)[] = [];
|
||||
|
||||
watch(
|
||||
() => browser.screen,
|
||||
() => {
|
||||
events.forEach((ev) => ev());
|
||||
}
|
||||
);
|
||||
|
||||
useEventListener(window, "resize", () => {
|
||||
Object.assign(browser, getBrowser());
|
||||
});
|
||||
|
||||
export function useBrowser() {
|
||||
return {
|
||||
browser,
|
||||
onScreenChange(ev: () => void, immediate = true) {
|
||||
events.push(ev);
|
||||
|
||||
if (immediate) {
|
||||
ev();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
23
src/cool/hooks/hmr.ts
Normal file
@ -0,0 +1,23 @@
|
||||
// 解决热更新后失效问题;
|
||||
const data = import.meta.hot?.data.getData?.() || {};
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.data.getData = () => {
|
||||
return data;
|
||||
};
|
||||
}
|
||||
|
||||
export const hmr = {
|
||||
data,
|
||||
|
||||
setData(key: string, value: any) {
|
||||
data[key] = value;
|
||||
},
|
||||
|
||||
getData(key: string, defaultValue?: any) {
|
||||
if (defaultValue !== undefined && !data[key]) {
|
||||
this.setData(key, defaultValue);
|
||||
}
|
||||
return data[key];
|
||||
}
|
||||
};
|
54
src/cool/hooks/index.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { getCurrentInstance, Ref, reactive } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { service } from "../service";
|
||||
import { useBrowser } from "./browser";
|
||||
import { useMitt } from "./mitt";
|
||||
|
||||
export function useRefs() {
|
||||
const refs = reactive<{ [key: string]: any }>({});
|
||||
|
||||
function setRefs(name: string) {
|
||||
return (el: any) => {
|
||||
refs[name] = el;
|
||||
return () => refs[name];
|
||||
};
|
||||
}
|
||||
|
||||
return { refs, setRefs };
|
||||
}
|
||||
|
||||
export function useParent(name: string, r: Ref) {
|
||||
const d = getCurrentInstance();
|
||||
|
||||
if (d) {
|
||||
let parent = d.proxy?.$.parent;
|
||||
|
||||
if (parent) {
|
||||
while (parent && parent.type?.name != name) {
|
||||
parent = parent?.parent;
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
if (parent.type.name == name) {
|
||||
r.value = parent.exposed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
export function useCool() {
|
||||
return {
|
||||
service,
|
||||
route: useRoute(),
|
||||
router: useRouter(),
|
||||
mitt: useMitt(),
|
||||
...useBrowser(),
|
||||
...useRefs()
|
||||
};
|
||||
}
|
||||
|
||||
export * from "./browser";
|
||||
export * from "./hmr";
|
8
src/cool/hooks/mitt.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import Mitt, { Emitter } from "mitt";
|
||||
import { hmr } from "./hmr";
|
||||
|
||||
const mitt: Emitter<any> = hmr.getData("mitt", Mitt());
|
||||
|
||||
export function useMitt() {
|
||||
return mitt;
|
||||
}
|
7
src/cool/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export * from "./service";
|
||||
export * from "./bootstrap";
|
||||
export * from "./hooks";
|
||||
export * from "./module";
|
||||
export * from "./router";
|
||||
export * from "./types";
|
||||
export { storage } from "./utils";
|
27
src/cool/module/index.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Module } from "../types";
|
||||
import { hmr } from "../hooks";
|
||||
import { ctx } from "virtual:ctx";
|
||||
|
||||
// 模块列表
|
||||
const list: Module[] = hmr.getData("modules", []);
|
||||
|
||||
// 模块对象
|
||||
const module = {
|
||||
list,
|
||||
dirs: ctx.modules,
|
||||
req: Promise.resolve(),
|
||||
get(name: string): Module {
|
||||
return this.list.find((e) => e.name == name)!;
|
||||
},
|
||||
config(name: string) {
|
||||
return this.get(name).options || {};
|
||||
},
|
||||
add(data: Module) {
|
||||
this.list.push(data);
|
||||
},
|
||||
wait() {
|
||||
return this.req;
|
||||
}
|
||||
};
|
||||
|
||||
export { module };
|
217
src/cool/router/index.ts
Normal file
@ -0,0 +1,217 @@
|
||||
import { ElMessage } from "element-plus";
|
||||
import { createRouter, createWebHashHistory, createWebHistory, RouteRecordRaw } from "vue-router";
|
||||
import { Router, storage, module } from "/@/cool";
|
||||
import { isArray } from "lodash-es";
|
||||
import { useBase } from "/$/base";
|
||||
import { Loading } from "../utils";
|
||||
import { config } from "/@/config";
|
||||
|
||||
// 基本路径
|
||||
const baseUrl = import.meta.env.BASE_URL;
|
||||
|
||||
// 扫描文件
|
||||
const files = import.meta.glob(["/src/modules/*/{views,pages}/**/*", "!**/components"]);
|
||||
|
||||
// 默认路由
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: "/",
|
||||
name: "index",
|
||||
component: () => import("/$/base/pages/main/index.vue"),
|
||||
children: []
|
||||
},
|
||||
{
|
||||
path: "/:catchAll(.*)",
|
||||
name: "404",
|
||||
component: () => import("/$/base/pages/error/404.vue")
|
||||
}
|
||||
];
|
||||
|
||||
// 创建路由器
|
||||
const router = createRouter({
|
||||
history:
|
||||
config.app.router.mode == "history"
|
||||
? createWebHistory(baseUrl)
|
||||
: createWebHashHistory(baseUrl),
|
||||
routes
|
||||
}) as Router;
|
||||
|
||||
// 组件加载后
|
||||
router.beforeResolve(() => {
|
||||
Loading.close();
|
||||
});
|
||||
|
||||
let lock = false;
|
||||
|
||||
// 错误监听
|
||||
router.onError((err: Error) => {
|
||||
if (!lock) {
|
||||
lock = true;
|
||||
|
||||
ElMessage.error(`页面存在错误:${err.message}`);
|
||||
console.error(err);
|
||||
|
||||
// 动态加载组件错误,刷新页面
|
||||
if (err.message?.includes("Failed to fetch dynamically imported module")) {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
lock = false;
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加试图,页面路由
|
||||
router.append = function (data) {
|
||||
const list = isArray(data) ? data : [data];
|
||||
|
||||
list.forEach((d) => {
|
||||
if (!d.meta) {
|
||||
d.meta = {};
|
||||
}
|
||||
|
||||
// 组件路径
|
||||
if (!d.component) {
|
||||
const url = d.viewPath;
|
||||
|
||||
if (url) {
|
||||
if (url.indexOf("http") == 0) {
|
||||
if (d.meta) {
|
||||
d.meta.iframeUrl = url;
|
||||
}
|
||||
|
||||
d.component = () => import("/$/base/views/frame.vue");
|
||||
} else {
|
||||
d.component = files["/src/" + url.replace("cool/", "")];
|
||||
}
|
||||
} else {
|
||||
d.redirect = "/404";
|
||||
}
|
||||
}
|
||||
|
||||
// 是否动态添加
|
||||
d.meta.dynamic = true;
|
||||
|
||||
if (d.isPage) {
|
||||
router.addRoute(d as any);
|
||||
} else {
|
||||
router.addRoute("index", d as any);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 清空路由
|
||||
router.clear = function () {
|
||||
const rs = router.getRoutes();
|
||||
|
||||
rs.forEach((e) => {
|
||||
if (e.name && e.meta?.dynamic) {
|
||||
router.removeRoute(e.name);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 找路由
|
||||
router.find = function (path: string) {
|
||||
return router.getRoutes().find((e) => {
|
||||
if (path == "/") {
|
||||
return e.path == path && e.name != "index";
|
||||
} else {
|
||||
return e.path == path;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 注册
|
||||
router.register = async function (path: string) {
|
||||
// 当前路由是否注册
|
||||
const isReg = Boolean(router.find(path));
|
||||
|
||||
if (!isReg) {
|
||||
const { menu } = useBase();
|
||||
|
||||
// 等待应用配置加载完
|
||||
await Loading.wait();
|
||||
|
||||
// 待注册列表
|
||||
const list: any[] = [];
|
||||
|
||||
// 动态菜单数据
|
||||
menu.routes.find((e) => {
|
||||
list.push({
|
||||
...e,
|
||||
isPage: e.viewPath?.includes("/pages")
|
||||
});
|
||||
});
|
||||
|
||||
// 本地模块数据
|
||||
module.list.forEach((e) => {
|
||||
if (e.views) {
|
||||
list.push(...e.views);
|
||||
}
|
||||
|
||||
if (e.pages) {
|
||||
list.push(
|
||||
...e.pages.map((d) => {
|
||||
return {
|
||||
...d,
|
||||
isPage: true
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 需要注册的路由
|
||||
const r = list.find((e) => e.path == path);
|
||||
|
||||
if (r) {
|
||||
router.append(r);
|
||||
}
|
||||
}
|
||||
|
||||
return { route: router.find(path), isReg };
|
||||
};
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
// 数据缓存
|
||||
const { user, process } = useBase();
|
||||
|
||||
// 预先注册路由
|
||||
const { isReg, route } = await router.register(to.path);
|
||||
|
||||
// 组件不存在、路由不存在
|
||||
if (!route?.components) {
|
||||
next(user.token ? "/404" : "/login");
|
||||
} else {
|
||||
if (!isReg) {
|
||||
next(to.fullPath);
|
||||
} else {
|
||||
// 登录成功
|
||||
if (user.token) {
|
||||
// 在登录页
|
||||
if (to.path.includes("/login")) {
|
||||
// Token 未过期
|
||||
if (!storage.isExpired("token")) {
|
||||
// 回到首页
|
||||
return next("/");
|
||||
}
|
||||
} else {
|
||||
// 添加路由进程
|
||||
process.add(to);
|
||||
}
|
||||
} else {
|
||||
// 忽略部分 Token 验证
|
||||
if (!config.ignore.token.find((e) => to.path == e)) {
|
||||
return next("/login");
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export { router };
|
117
src/cool/service/base.ts
Normal file
@ -0,0 +1,117 @@
|
||||
// @ts-nocheck
|
||||
import { isDev, config, proxy } from "../../config";
|
||||
import { isObject } from "lodash-es";
|
||||
import { request } from "./request";
|
||||
import { AxiosRequestConfig } from "axios";
|
||||
|
||||
export function Service(
|
||||
value:
|
||||
| string
|
||||
| {
|
||||
proxy?: string;
|
||||
namespace?: string;
|
||||
url?: string;
|
||||
}
|
||||
) {
|
||||
return function (target: any) {
|
||||
// 命名
|
||||
if (typeof value == "string") {
|
||||
target.prototype.namespace = value;
|
||||
}
|
||||
|
||||
// 复杂项
|
||||
if (isObject(value)) {
|
||||
const { namespace, proxy: proxyName, url } = value;
|
||||
|
||||
target.prototype.namespace = namespace;
|
||||
|
||||
if (proxyName) {
|
||||
target.prototype.url = proxy[proxyName]?.target || url;
|
||||
} else {
|
||||
target.prototype.url = url;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class BaseService {
|
||||
constructor(namespace?: string) {
|
||||
if (namespace) {
|
||||
this.namespace = namespace;
|
||||
}
|
||||
}
|
||||
|
||||
async request(options: AxiosRequestConfig = {}) {
|
||||
if (options.url) {
|
||||
// 过滤 http 开头的地址
|
||||
if (options.url.indexOf("http") < 0) {
|
||||
let ns = "";
|
||||
|
||||
if (isDev) {
|
||||
ns = this.proxy || config.baseUrl;
|
||||
} else {
|
||||
ns = this.proxy ? this.url : config.baseUrl;
|
||||
}
|
||||
|
||||
// 拼接前缀
|
||||
if (this.namespace) {
|
||||
ns += "/" + this.namespace;
|
||||
}
|
||||
|
||||
// 处理地址
|
||||
if (options.proxy === undefined || options.proxy) {
|
||||
options.url = ns + options.url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return request(options);
|
||||
}
|
||||
|
||||
async list(data: any) {
|
||||
return this.request({
|
||||
url: "/list",
|
||||
method: "POST",
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
async page(data: any) {
|
||||
return this.request({
|
||||
url: "/page",
|
||||
method: "POST",
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
async info(params: any) {
|
||||
return this.request({
|
||||
url: "/info",
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
async update(data: any) {
|
||||
return this.request({
|
||||
url: "/update",
|
||||
method: "POST",
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
async delete(data: any) {
|
||||
return this.request({
|
||||
url: "/delete",
|
||||
method: "POST",
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
async add(data: any) {
|
||||
return this.request({
|
||||
url: "/add",
|
||||
method: "POST",
|
||||
data
|
||||
});
|
||||
}
|
||||
}
|
9
src/cool/service/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { hmr } from "../hooks";
|
||||
import { BaseService } from "./base";
|
||||
|
||||
// service 数据集合
|
||||
export const service: Eps.Service = hmr.getData("service", {
|
||||
request: new BaseService().request
|
||||
});
|
||||
|
||||
export * from "./base";
|
158
src/cool/service/request.ts
Normal file
@ -0,0 +1,158 @@
|
||||
import axios from "axios";
|
||||
import NProgress from "nprogress";
|
||||
import "nprogress/nprogress.css";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { endsWith } from "lodash-es";
|
||||
import { storage } from "/@/cool/utils";
|
||||
import { useBase } from "/$/base";
|
||||
import { router } from "../router";
|
||||
import { config, isDev } from "/@/config";
|
||||
|
||||
const request = axios.create({
|
||||
timeout: import.meta.env.VITE_TIMEOUT,
|
||||
withCredentials: false
|
||||
});
|
||||
|
||||
NProgress.configure({
|
||||
showSpinner: true
|
||||
});
|
||||
|
||||
// 请求队列
|
||||
let queue: Array<(token: string) => void> = [];
|
||||
|
||||
// 是否刷新中
|
||||
let isRefreshing = false;
|
||||
|
||||
// 请求
|
||||
request.interceptors.request.use(
|
||||
(req: any) => {
|
||||
const { user } = useBase();
|
||||
|
||||
if (req.url) {
|
||||
// 请求进度条
|
||||
if (
|
||||
!config.ignore.NProgress.some((e) => req.url.match(new RegExp(`${e}.*`))) &&
|
||||
(req.NProgress ?? true)
|
||||
) {
|
||||
NProgress.start();
|
||||
}
|
||||
}
|
||||
|
||||
// 请求信息
|
||||
if (isDev) {
|
||||
console.group(req.url);
|
||||
console.log("method:", req.method);
|
||||
console.table("data:", req.method == "get" ? req.params : req.data);
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
// 验证 token
|
||||
if (user.token) {
|
||||
// 请求标识
|
||||
if (req.headers && req.headers["Authorization"] !== null) {
|
||||
req.headers["Authorization"] = user.token;
|
||||
}
|
||||
|
||||
// 忽略
|
||||
if (["eps", "refreshToken"].some((e) => endsWith(req.url, e))) {
|
||||
return req;
|
||||
}
|
||||
|
||||
// 判断 token 是否过期
|
||||
if (storage.isExpired("token")) {
|
||||
// 判断 refreshToken 是否过期
|
||||
if (storage.isExpired("refreshToken")) {
|
||||
ElMessage.error("登录状态已失效,请重新登录");
|
||||
user.logout();
|
||||
} else {
|
||||
// 是否在刷新中
|
||||
if (!isRefreshing) {
|
||||
isRefreshing = true;
|
||||
|
||||
user.refreshToken()
|
||||
.then((token) => {
|
||||
queue.forEach((cb) => cb(token));
|
||||
queue = [];
|
||||
isRefreshing = false;
|
||||
})
|
||||
.catch(() => {
|
||||
user.logout();
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// 继续请求
|
||||
queue.push((token) => {
|
||||
// 重新设置 token
|
||||
if (req.headers) {
|
||||
req.headers["Authorization"] = token;
|
||||
}
|
||||
resolve(req);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return req;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 响应
|
||||
request.interceptors.response.use(
|
||||
(res) => {
|
||||
NProgress.done();
|
||||
|
||||
if (!res?.data) {
|
||||
return res;
|
||||
}
|
||||
|
||||
const { code, data, message } = res.data;
|
||||
|
||||
if (!code) {
|
||||
return res.data;
|
||||
}
|
||||
|
||||
switch (code) {
|
||||
case 1000:
|
||||
return data;
|
||||
default:
|
||||
return Promise.reject({ code, message });
|
||||
}
|
||||
},
|
||||
async (error) => {
|
||||
NProgress.done();
|
||||
|
||||
if (error.response) {
|
||||
const { status } = error.response;
|
||||
const { user } = useBase();
|
||||
|
||||
if (status == 401) {
|
||||
user.logout();
|
||||
} else {
|
||||
if (!isDev) {
|
||||
switch (status) {
|
||||
case 403:
|
||||
router.push("/403");
|
||||
break;
|
||||
|
||||
case 500:
|
||||
router.push("/500");
|
||||
break;
|
||||
|
||||
case 502:
|
||||
router.push("/502");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject({ message: error.message });
|
||||
}
|
||||
);
|
||||
|
||||
export { request };
|
60
src/cool/types/index.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { Component, Directive, App } from "vue";
|
||||
import { Router as VueRouter, RouteRecordRaw } from "vue-router";
|
||||
|
||||
export declare type Merge<A, B> = Omit<A, keyof B> & B;
|
||||
|
||||
export declare interface ModuleConfig {
|
||||
name?: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
order?: number;
|
||||
version?: string;
|
||||
logo?: string;
|
||||
author?: string;
|
||||
updateTime?: string;
|
||||
demo?: { name: string; component: Component }[] | string;
|
||||
options?: {
|
||||
[key: string]: any;
|
||||
};
|
||||
toolbar?: {
|
||||
order?: number;
|
||||
pc?: boolean;
|
||||
h5?: boolean;
|
||||
component: Promise<any>;
|
||||
};
|
||||
components?: Component[];
|
||||
views?: RouteRecordRaw[];
|
||||
pages?: RouteRecordRaw[];
|
||||
install?(app: App, options?: any): any;
|
||||
onLoad?(events: {
|
||||
hasToken: (cb: () => Promise<any> | void) => Promise<any> | void;
|
||||
[key: string]: any;
|
||||
}): Promise<{ [key: string]: any }> | Promise<void> | void;
|
||||
}
|
||||
|
||||
export declare interface Module extends ModuleConfig {
|
||||
name: string;
|
||||
options: {
|
||||
[key: string]: any;
|
||||
};
|
||||
value?: any;
|
||||
services?: { path: string; value: any }[];
|
||||
directives?: { name: string; value: Directive }[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export declare interface Router extends VueRouter {
|
||||
find(path: string): RouteRecordRaw | undefined;
|
||||
append(
|
||||
data: {
|
||||
name?: string;
|
||||
path: string;
|
||||
component?: any;
|
||||
viewPath?: string;
|
||||
isPage?: boolean;
|
||||
[key: string]: any;
|
||||
}[]
|
||||
): void;
|
||||
register(path: string): Promise<{ route: RouteRecordRaw | undefined; isReg: boolean }>;
|
||||
[key: string]: any;
|
||||
}
|
301
src/cool/utils/index.ts
Normal file
@ -0,0 +1,301 @@
|
||||
import { isArray, isNumber, isString, orderBy } from "lodash-es";
|
||||
import { resolveComponent } from "vue";
|
||||
import storage from "./storage";
|
||||
|
||||
// 首字母大写
|
||||
export function firstUpperCase(value: string): string {
|
||||
return value.replace(/\b(\w)(\w*)/g, function ($0, $1, $2) {
|
||||
return $1.toUpperCase() + $2;
|
||||
});
|
||||
}
|
||||
|
||||
// 获取方法名
|
||||
export function getNames(value: any) {
|
||||
return Object.getOwnPropertyNames(value.constructor.prototype);
|
||||
}
|
||||
|
||||
// 获取地址栏参数
|
||||
export function getUrlParam(name: string): string | null {
|
||||
const reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
|
||||
const r = window.location.search.substr(1).match(reg);
|
||||
if (r != null) return decodeURIComponent(r[2]);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 文件名
|
||||
export function filename(path: string): string {
|
||||
return basename(path.substring(0, path.lastIndexOf(".")));
|
||||
}
|
||||
|
||||
// 路径名称
|
||||
export function basename(path: string): string {
|
||||
let index = path.lastIndexOf("/");
|
||||
index = index > -1 ? index : path.lastIndexOf("\\");
|
||||
if (index < 0) {
|
||||
return path;
|
||||
}
|
||||
return path.substring(index + 1);
|
||||
}
|
||||
|
||||
// 文件扩展名
|
||||
export function extname(path: string): string {
|
||||
return path.substring(path.lastIndexOf(".") + 1).split(/(\?|&)/)[0];
|
||||
}
|
||||
|
||||
// 横杠转驼峰
|
||||
export function toCamel(str: string): string {
|
||||
return str.replace(/([^-])(?:-+([^-]))/g, function ($0, $1, $2) {
|
||||
return $1 + $2.toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
// uuid
|
||||
export function uuid(separator = "-"): string {
|
||||
const s: any[] = [];
|
||||
const hexDigits = "0123456789abcdef";
|
||||
for (let i = 0; i < 36; i++) {
|
||||
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
|
||||
}
|
||||
s[14] = "4";
|
||||
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1);
|
||||
s[8] = s[13] = s[18] = s[23] = separator;
|
||||
|
||||
return s.join("");
|
||||
}
|
||||
|
||||
// 浏览器信息
|
||||
export function getBrowser() {
|
||||
const { clientHeight, clientWidth } = document.documentElement;
|
||||
|
||||
// 浏览器信息
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
|
||||
// 浏览器类型
|
||||
let type = (ua.match(/firefox|chrome|safari|opera/g) || "other")[0];
|
||||
|
||||
if ((ua.match(/msie|trident/g) || [])[0]) {
|
||||
type = "msie";
|
||||
}
|
||||
|
||||
// 平台标签
|
||||
let tag = "";
|
||||
|
||||
const isTocuh =
|
||||
"ontouchstart" in window || ua.indexOf("touch") !== -1 || ua.indexOf("mobile") !== -1;
|
||||
if (isTocuh) {
|
||||
if (ua.indexOf("ipad") !== -1) {
|
||||
tag = "pad";
|
||||
} else if (ua.indexOf("mobile") !== -1) {
|
||||
tag = "mobile";
|
||||
} else if (ua.indexOf("android") !== -1) {
|
||||
tag = "androidPad";
|
||||
} else {
|
||||
tag = "pc";
|
||||
}
|
||||
} else {
|
||||
tag = "pc";
|
||||
}
|
||||
|
||||
// 浏览器内核
|
||||
let prefix = "";
|
||||
|
||||
switch (type) {
|
||||
case "chrome":
|
||||
case "safari":
|
||||
case "mobile":
|
||||
prefix = "webkit";
|
||||
break;
|
||||
case "msie":
|
||||
prefix = "ms";
|
||||
break;
|
||||
case "firefox":
|
||||
prefix = "Moz";
|
||||
break;
|
||||
case "opera":
|
||||
prefix = "O";
|
||||
break;
|
||||
default:
|
||||
prefix = "webkit";
|
||||
break;
|
||||
}
|
||||
|
||||
// 操作平台
|
||||
const plat = ua.indexOf("android") > 0 ? "android" : navigator.platform.toLowerCase();
|
||||
|
||||
// 屏幕信息
|
||||
let screen = "full";
|
||||
|
||||
if (clientWidth < 768) {
|
||||
screen = "xs";
|
||||
} else if (clientWidth < 992) {
|
||||
screen = "sm";
|
||||
} else if (clientWidth < 1200) {
|
||||
screen = "md";
|
||||
} else if (clientWidth < 1920) {
|
||||
screen = "xl";
|
||||
} else {
|
||||
screen = "full";
|
||||
}
|
||||
|
||||
// 是否 ios
|
||||
const isIOS = !!navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/);
|
||||
|
||||
// 是否 PC 端
|
||||
const isPC = tag === "pc";
|
||||
|
||||
// 是否移动端
|
||||
const isMobile = isPC ? false : true;
|
||||
|
||||
// 是否移动端 + 屏幕宽过小
|
||||
const isMini = screen === "xs" || isMobile;
|
||||
|
||||
return {
|
||||
height: clientHeight,
|
||||
width: clientWidth,
|
||||
type,
|
||||
plat,
|
||||
tag,
|
||||
prefix,
|
||||
isMobile,
|
||||
isIOS,
|
||||
isPC,
|
||||
isMini,
|
||||
screen
|
||||
};
|
||||
}
|
||||
|
||||
// 路径转数组
|
||||
export function deepPaths(paths: string[], splitor?: string) {
|
||||
const list: any[] = [];
|
||||
|
||||
paths.forEach((e) => {
|
||||
const arr: string[] = e.split(splitor || "/").filter(Boolean);
|
||||
|
||||
let c = list;
|
||||
|
||||
arr.forEach((a, i) => {
|
||||
let d = c.find((e) => e.label == a);
|
||||
|
||||
if (!d) {
|
||||
d = {
|
||||
label: a,
|
||||
value: a,
|
||||
children: arr[i + 1] ? [] : null
|
||||
};
|
||||
|
||||
c.push(d);
|
||||
}
|
||||
|
||||
if (d.children) {
|
||||
c = d.children;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
// 列表转树形
|
||||
export function deepTree(list: any[], sort?: "desc" | "asc"): any[] {
|
||||
const newList: any[] = [];
|
||||
const map: any = {};
|
||||
|
||||
orderBy(list, "orderNum", sort)
|
||||
.map((e) => {
|
||||
map[e.id] = e;
|
||||
return e;
|
||||
})
|
||||
.forEach((e) => {
|
||||
const parent = map[e.parentId];
|
||||
|
||||
if (parent) {
|
||||
(parent.children || (parent.children = [])).push(e);
|
||||
} else {
|
||||
newList.push(e);
|
||||
}
|
||||
});
|
||||
|
||||
return newList;
|
||||
}
|
||||
|
||||
// 树形转列表
|
||||
export function revDeepTree(list: any[]) {
|
||||
const arr: any[] = [];
|
||||
let id = 0;
|
||||
|
||||
function deep(list: any[], parentId: number) {
|
||||
list.forEach((e) => {
|
||||
if (!e.id) {
|
||||
e.id = ++id;
|
||||
}
|
||||
|
||||
if (!e.parentId) {
|
||||
e.parentId = parentId;
|
||||
}
|
||||
|
||||
arr.push(e);
|
||||
|
||||
if (e.children && isArray(e.children) && e.id) {
|
||||
deep(e.children, e.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deep(list || [], 0);
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
// 路径转对象
|
||||
export function path2Obj(list: any[]) {
|
||||
const data: any = {};
|
||||
|
||||
list.forEach(({ path, value }) => {
|
||||
if (path) {
|
||||
const arr: string[] = path.split("/");
|
||||
const parents = arr.slice(0, arr.length - 1);
|
||||
const name = basename(path).replace(".ts", "");
|
||||
|
||||
let curr = data;
|
||||
|
||||
parents.forEach((k) => {
|
||||
if (!curr[k]) {
|
||||
curr[k] = {};
|
||||
}
|
||||
|
||||
curr = curr[k];
|
||||
});
|
||||
|
||||
curr[name] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// 是否是组件
|
||||
export function isComponent(name: string) {
|
||||
return !isString(resolveComponent(name));
|
||||
}
|
||||
|
||||
// 是否Promise
|
||||
export function isPromise(val: any) {
|
||||
return val && Object.prototype.toString.call(val) === "[object Promise]";
|
||||
}
|
||||
|
||||
// 单位转换
|
||||
export function parsePx(val: string | number) {
|
||||
return isNumber(val) ? `${val}px` : val;
|
||||
}
|
||||
|
||||
// 延迟
|
||||
export function sleep(duration: number) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(true);
|
||||
}, duration);
|
||||
});
|
||||
}
|
||||
|
||||
export { storage };
|
||||
export * from "./loading";
|
37
src/cool/utils/loading.ts
Normal file
@ -0,0 +1,37 @@
|
||||
export const Loading = {
|
||||
resolve: null as (() => void) | null,
|
||||
next: null as Promise<void> | null,
|
||||
|
||||
async set(list: Promise<any>[]) {
|
||||
try {
|
||||
await Promise.all(list);
|
||||
} catch (e) {
|
||||
console.error("[Loading] Error: ", e);
|
||||
}
|
||||
|
||||
if (this.resolve) {
|
||||
this.resolve();
|
||||
}
|
||||
},
|
||||
|
||||
async wait() {
|
||||
if (this.next) {
|
||||
return this.next;
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
close() {
|
||||
const el = document.getElementById("Loading");
|
||||
|
||||
if (el) {
|
||||
setTimeout(() => {
|
||||
el.className += " is-hide";
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Loading.next = new Promise<void>((resolve) => {
|
||||
Loading.resolve = resolve;
|
||||
});
|
81
src/cool/utils/storage.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import store from "store";
|
||||
|
||||
export default {
|
||||
// 后缀标识
|
||||
suffix: "_deadtime",
|
||||
|
||||
/**
|
||||
* 获取
|
||||
* @param {string} key 关键字
|
||||
*/
|
||||
get(key: string) {
|
||||
return store.get(key);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取全部
|
||||
*/
|
||||
info() {
|
||||
const d: any = {};
|
||||
|
||||
store.each(function (value: any, key: any) {
|
||||
d[key] = value;
|
||||
});
|
||||
|
||||
return d;
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置
|
||||
* @param {string} key 关键字
|
||||
* @param {*} value 值
|
||||
* @param {number} expires 过期时间
|
||||
*/
|
||||
set(key: string, value: any, expires?: any) {
|
||||
store.set(key, value);
|
||||
|
||||
if (expires) {
|
||||
store.set(`${key}${this.suffix}`, Date.parse(String(new Date())) + expires * 1000);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 是否过期
|
||||
* @param {string} key 关键字
|
||||
*/
|
||||
isExpired(key: string) {
|
||||
return (this.getExpiration(key) || 0) - Date.parse(String(new Date())) <= 2000;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取到期时间
|
||||
* @param {string} key 关键字
|
||||
*/
|
||||
getExpiration(key: string) {
|
||||
return this.get(key + this.suffix);
|
||||
},
|
||||
|
||||
/**
|
||||
* 移除
|
||||
* @param {string} key 关键字
|
||||
*/
|
||||
remove(key: string) {
|
||||
store.remove(key);
|
||||
this.removeExpiration(key);
|
||||
},
|
||||
|
||||
/**
|
||||
* 移除到期时间
|
||||
* @param {string} key 关键字
|
||||
*/
|
||||
removeExpiration(key: string) {
|
||||
store.remove(key + this.suffix);
|
||||
},
|
||||
|
||||
/**
|
||||
* 清理
|
||||
*/
|
||||
clearAll() {
|
||||
store.clearAll();
|
||||
}
|
||||
};
|
17
src/main.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import { bootstrap } from "./cool";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
// 启动
|
||||
bootstrap(app)
|
||||
.then(() => {
|
||||
app.mount("#app");
|
||||
|
||||
// 加载图表
|
||||
import("echarts");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("COOL-ADMIN 启动失败", err);
|
||||
});
|
148
src/modules/app/views/complain.vue
Normal file
@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<cl-crud ref="Crud">
|
||||
<cl-row>
|
||||
<!-- 刷新按钮 -->
|
||||
<cl-refresh-btn />
|
||||
|
||||
<cl-filter label="状态">
|
||||
<cl-select :options="options.status" prop="status" :width="120" />
|
||||
</cl-filter>
|
||||
|
||||
<cl-filter label="投诉类型">
|
||||
<cl-select :options="options.type" prop="type" :width="120" />
|
||||
</cl-filter>
|
||||
|
||||
<cl-flex1 />
|
||||
<!-- 关键字搜索 -->
|
||||
<cl-search-key placeholder="搜索联系方式、用户、处理人" />
|
||||
</cl-row>
|
||||
|
||||
<cl-row>
|
||||
<!-- 数据表格 -->
|
||||
<cl-table ref="Table" />
|
||||
</cl-row>
|
||||
|
||||
<cl-row>
|
||||
<cl-flex1 />
|
||||
<!-- 分页控件 -->
|
||||
<cl-pagination />
|
||||
</cl-row>
|
||||
|
||||
<!-- 新增、编辑 -->
|
||||
<cl-upsert ref="Upsert" />
|
||||
</cl-crud>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="app-complain" setup>
|
||||
import { useCrud, useTable, useUpsert } from "@cool-vue/crud";
|
||||
import { useCool } from "/@/cool";
|
||||
import { reactive, computed } from "vue";
|
||||
import { useDict } from "/$/dict";
|
||||
|
||||
const { service } = useCool();
|
||||
const { dict } = useDict();
|
||||
|
||||
const options = reactive({
|
||||
status: [
|
||||
{
|
||||
label: "未处理",
|
||||
value: 0,
|
||||
type: "warning"
|
||||
},
|
||||
{
|
||||
label: "已处理",
|
||||
value: 1
|
||||
}
|
||||
],
|
||||
type: computed(() => {
|
||||
return dict.get("complainType");
|
||||
})
|
||||
});
|
||||
|
||||
// cl-upsert
|
||||
const Upsert = useUpsert({
|
||||
props: {
|
||||
labelWidth: 80
|
||||
},
|
||||
items: [
|
||||
{
|
||||
prop: "status",
|
||||
label: "状态",
|
||||
component: { name: "el-radio-group", options: options.status },
|
||||
required: true
|
||||
},
|
||||
{
|
||||
prop: "remark",
|
||||
label: "处理结果",
|
||||
component: {
|
||||
name: "el-input",
|
||||
props: {
|
||||
type: "textarea",
|
||||
rows: 4
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// cl-table
|
||||
const Table = useTable({
|
||||
columns: [
|
||||
{ type: "index", label: "#", width: 60 },
|
||||
{ prop: "avatarUrl", label: "头像", component: { name: "cl-avatar" }, minWidth: 80 },
|
||||
{ prop: "nickName", label: "用户", minWidth: 120 },
|
||||
{ prop: "contact", label: "联系方式", minWidth: 120 },
|
||||
{
|
||||
prop: "content",
|
||||
label: "内容",
|
||||
component: { name: "cl-editor-preview", props: { name: "wang" } },
|
||||
minWidth: 120
|
||||
},
|
||||
{
|
||||
prop: "images",
|
||||
label: "图片",
|
||||
component: { name: "cl-image", props: { size: 60 } },
|
||||
minWidth: 100
|
||||
},
|
||||
{
|
||||
prop: "type",
|
||||
label: "类型",
|
||||
dict: options.type,
|
||||
dictFormatter(arr) {
|
||||
return arr.map((e) => e.label).join("、");
|
||||
},
|
||||
minWidth: 120
|
||||
},
|
||||
{ prop: "status", label: "状态", dict: options.status, minWidth: 100 },
|
||||
{ prop: "handlerName", label: "处理人", minWidth: 120 },
|
||||
{ prop: "remark", label: "处理结果", showOverflowTooltip: true, minWidth: 200 },
|
||||
{ prop: "createTime", label: "创建时间", sortable: "desc", minWidth: 160 },
|
||||
{ prop: "updateTime", label: "更新时间", sortable: "custom", minWidth: 160 },
|
||||
{
|
||||
type: "op",
|
||||
width: 120,
|
||||
buttons({ scope }) {
|
||||
if (scope.row.status == 0) {
|
||||
return ["edit"];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// cl-crud
|
||||
const Crud = useCrud(
|
||||
{
|
||||
dict: {
|
||||
label: {
|
||||
update: "处理"
|
||||
}
|
||||
},
|
||||
service: service.app.complain
|
||||
},
|
||||
(app) => {
|
||||
app.refresh();
|
||||
}
|
||||
);
|
||||
</script>
|
148
src/modules/app/views/feedback.vue
Normal file
@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<cl-crud ref="Crud">
|
||||
<cl-row>
|
||||
<!-- 刷新按钮 -->
|
||||
<cl-refresh-btn />
|
||||
|
||||
<cl-filter label="状态">
|
||||
<cl-select :options="options.status" prop="status" :width="120" />
|
||||
</cl-filter>
|
||||
|
||||
<cl-filter label="反馈类型">
|
||||
<cl-select :options="options.type" prop="type" :width="120" />
|
||||
</cl-filter>
|
||||
|
||||
<cl-flex1 />
|
||||
<!-- 关键字搜索 -->
|
||||
<cl-search-key placeholder="搜索联系方式、用户、处理人" />
|
||||
</cl-row>
|
||||
|
||||
<cl-row>
|
||||
<!-- 数据表格 -->
|
||||
<cl-table ref="Table" />
|
||||
</cl-row>
|
||||
|
||||
<cl-row>
|
||||
<cl-flex1 />
|
||||
<!-- 分页控件 -->
|
||||
<cl-pagination />
|
||||
</cl-row>
|
||||
|
||||
<!-- 新增、编辑 -->
|
||||
<cl-upsert ref="Upsert" />
|
||||
</cl-crud>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="app-feedback" setup>
|
||||
import { useCrud, useTable, useUpsert } from "@cool-vue/crud";
|
||||
import { useCool } from "/@/cool";
|
||||
import { computed, reactive } from "vue";
|
||||
import { useDict } from "/$/dict";
|
||||
|
||||
const { service } = useCool();
|
||||
const { dict } = useDict();
|
||||
|
||||
const options = reactive({
|
||||
status: [
|
||||
{
|
||||
label: "未处理",
|
||||
value: 0,
|
||||
type: "warning"
|
||||
},
|
||||
{
|
||||
label: "已处理",
|
||||
value: 1
|
||||
}
|
||||
],
|
||||
type: computed(() => {
|
||||
return dict.get("feedbackType");
|
||||
})
|
||||
});
|
||||
|
||||
// cl-upsert
|
||||
const Upsert = useUpsert({
|
||||
props: {
|
||||
labelWidth: 80
|
||||
},
|
||||
items: [
|
||||
{
|
||||
prop: "status",
|
||||
label: "状态",
|
||||
component: { name: "el-radio-group", options: options.status },
|
||||
required: true
|
||||
},
|
||||
{
|
||||
prop: "remark",
|
||||
label: "处理结果",
|
||||
component: {
|
||||
name: "el-input",
|
||||
props: {
|
||||
type: "textarea",
|
||||
rows: 4
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// cl-table
|
||||
const Table = useTable({
|
||||
columns: [
|
||||
{ type: "index", label: "#", width: 60 },
|
||||
{ prop: "avatarUrl", label: "头像", component: { name: "cl-avatar" }, minWidth: 80 },
|
||||
{ prop: "nickName", label: "用户", minWidth: 120 },
|
||||
{ prop: "contact", label: "联系方式", minWidth: 120 },
|
||||
{
|
||||
prop: "content",
|
||||
label: "内容",
|
||||
component: { name: "cl-editor-preview", props: { name: "wang" } },
|
||||
minWidth: 120
|
||||
},
|
||||
{
|
||||
prop: "images",
|
||||
label: "图片",
|
||||
component: { name: "cl-image", props: { size: 60 } },
|
||||
minWidth: 100
|
||||
},
|
||||
{
|
||||
prop: "type",
|
||||
label: "类型",
|
||||
dict: options.type,
|
||||
dictFormatter(arr) {
|
||||
return arr.map((e) => e.label).join("、");
|
||||
},
|
||||
minWidth: 120
|
||||
},
|
||||
{ prop: "status", label: "状态", dict: options.status, minWidth: 100 },
|
||||
{ prop: "handlerName", label: "处理人", minWidth: 120 },
|
||||
{ prop: "remark", label: "处理结果", showOverflowTooltip: true, minWidth: 200 },
|
||||
{ prop: "createTime", label: "创建时间", sortable: "desc", minWidth: 160 },
|
||||
{ prop: "updateTime", label: "更新时间", sortable: "custom", minWidth: 160 },
|
||||
{
|
||||
type: "op",
|
||||
width: 120,
|
||||
buttons({ scope }) {
|
||||
if (scope.row.status == 0) {
|
||||
return ["edit"];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// cl-crud
|
||||
const Crud = useCrud(
|
||||
{
|
||||
service: service.app.feedback,
|
||||
dict: {
|
||||
label: {
|
||||
update: "处理"
|
||||
}
|
||||
}
|
||||
},
|
||||
(app) => {
|
||||
app.refresh();
|
||||
}
|
||||
);
|
||||
</script>
|
188
src/modules/app/views/goods.vue
Normal file
@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<cl-crud ref="Crud">
|
||||
<cl-row>
|
||||
<!-- 刷新按钮 -->
|
||||
<cl-refresh-btn />
|
||||
<!-- 新增按钮 -->
|
||||
<cl-add-btn />
|
||||
<!-- 删除按钮 -->
|
||||
<cl-multi-delete-btn />
|
||||
|
||||
<cl-filter label="状态">
|
||||
<cl-select :options="options.status" prop="status" :width="120" />
|
||||
</cl-filter>
|
||||
|
||||
<cl-filter label="类型">
|
||||
<cl-select :options="options.type" prop="type" :width="120" />
|
||||
</cl-filter>
|
||||
|
||||
<cl-flex1 />
|
||||
<!-- 关键字搜索 -->
|
||||
<cl-search-key placeholder="搜索标题" />
|
||||
</cl-row>
|
||||
|
||||
<cl-row>
|
||||
<!-- 数据表格 -->
|
||||
<cl-table ref="Table">
|
||||
<template #column-tagColor="{ scope }">
|
||||
<!-- 显示颜色 -->
|
||||
<div
|
||||
:style="{
|
||||
backgroundColor: scope.row.tagColor,
|
||||
height: '10px',
|
||||
width: '10px',
|
||||
borderRadius: '100%',
|
||||
margin: 'auto'
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
</cl-table>
|
||||
</cl-row>
|
||||
|
||||
<cl-row>
|
||||
<cl-flex1 />
|
||||
<!-- 分页控件 -->
|
||||
<cl-pagination />
|
||||
</cl-row>
|
||||
|
||||
<!-- 新增、编辑 -->
|
||||
<cl-upsert ref="Upsert" />
|
||||
</cl-crud>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="app-goods" setup>
|
||||
import { useCrud, useTable, useUpsert } from "@cool-vue/crud";
|
||||
import { useCool } from "/@/cool";
|
||||
import { reactive } from "vue";
|
||||
|
||||
const { service } = useCool();
|
||||
|
||||
const options = reactive({
|
||||
status: [
|
||||
{
|
||||
label: "启用",
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
label: "禁用",
|
||||
value: 0
|
||||
}
|
||||
],
|
||||
type: [
|
||||
{ label: "天", value: 0 },
|
||||
{ label: "月", value: 1, type: "success" },
|
||||
{ label: "年", value: 2, type: "warning" }
|
||||
]
|
||||
});
|
||||
|
||||
// cl-upsert
|
||||
const Upsert = useUpsert({
|
||||
items: [
|
||||
{ prop: "title", label: "标题", required: true, component: { name: "el-input" } },
|
||||
{
|
||||
prop: "description",
|
||||
label: "描述",
|
||||
component: {
|
||||
name: "el-input",
|
||||
props: {
|
||||
type: "textarea",
|
||||
rows: 3
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
span: 12,
|
||||
required: true,
|
||||
prop: "price",
|
||||
label: "价格",
|
||||
hook: { bind: ["number"] },
|
||||
component: { name: "el-input-number", props: { min: 0 } }
|
||||
},
|
||||
{
|
||||
required: true,
|
||||
span: 12,
|
||||
prop: "originalPrice",
|
||||
label: "原价",
|
||||
hook: { bind: ["number"] },
|
||||
component: { name: "el-input-number", props: { min: 0 } }
|
||||
},
|
||||
{
|
||||
span: 12,
|
||||
prop: "type",
|
||||
label: "类型",
|
||||
component: {
|
||||
name: "el-radio-group",
|
||||
options: options.type
|
||||
},
|
||||
value: 0,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
span: 12,
|
||||
prop: "duration",
|
||||
label: "时长",
|
||||
required: true,
|
||||
component: { name: "el-input-number", props: { min: 0 } }
|
||||
},
|
||||
{ span: 12, prop: "tag", label: "标签", component: { name: "el-input" } },
|
||||
{
|
||||
span: 12,
|
||||
prop: "tagColor",
|
||||
label: "标签颜色",
|
||||
value: "#26A7FD",
|
||||
component: { name: "el-color-picker" }
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
prop: "sort",
|
||||
label: "排序",
|
||||
hook: { bind: ["number"] },
|
||||
component: { name: "el-input-number", props: { min: 0 } },
|
||||
required: true
|
||||
},
|
||||
{
|
||||
prop: "status",
|
||||
flex: false,
|
||||
value: 1,
|
||||
label: "状态",
|
||||
component: { name: "cl-switch" },
|
||||
required: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// cl-table
|
||||
const Table = useTable({
|
||||
columns: [
|
||||
{ type: "selection" },
|
||||
{ prop: "title", label: "标题", minWidth: 150 },
|
||||
{ prop: "price", label: "价格", minWidth: 120 },
|
||||
{ prop: "originalPrice", label: "原价", minWidth: 120 },
|
||||
{ prop: "description", label: "描述", showOverflowTooltip: true, minWidth: 200 },
|
||||
{ prop: "status", label: "状态", component: { name: "cl-switch" }, minWidth: 80 },
|
||||
{ prop: "sort", label: "排序", minWidth: 80 },
|
||||
{
|
||||
prop: "type",
|
||||
label: "类型",
|
||||
dict: options.type,
|
||||
minWidth: 80
|
||||
},
|
||||
{ prop: "duration", label: "时长", minWidth: 80 },
|
||||
{ prop: "tag", label: "标签", minWidth: 150 },
|
||||
{ prop: "tagColor", label: "标签颜色", minWidth: 100 },
|
||||
{ prop: "createTime", label: "创建时间", sortable: "desc", minWidth: 160 },
|
||||
{ prop: "updateTime", label: "更新时间", sortable: "custom", minWidth: 160 },
|
||||
{ type: "op", buttons: ["edit", "delete"] }
|
||||
]
|
||||
});
|
||||
|
||||
// cl-crud
|
||||
const Crud = useCrud(
|
||||
{
|
||||
service: service.app.goods
|
||||
},
|
||||
(app) => {
|
||||
app.refresh();
|
||||
}
|
||||
);
|
||||
</script>
|
177
src/modules/app/views/version.vue
Normal file
@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<cl-crud ref="Crud">
|
||||
<cl-row>
|
||||
<!-- 刷新按钮 -->
|
||||
<cl-refresh-btn />
|
||||
<!-- 新增按钮 -->
|
||||
<cl-add-btn />
|
||||
<!-- 删除按钮 -->
|
||||
<cl-multi-delete-btn />
|
||||
|
||||
<cl-filter label="状态">
|
||||
<cl-select :options="options.status" prop="status" :width="120" />
|
||||
</cl-filter>
|
||||
|
||||
<cl-filter label="类型">
|
||||
<cl-select :options="options.type" prop="type" :width="120" />
|
||||
</cl-filter>
|
||||
|
||||
<cl-flex1 />
|
||||
<!-- 关键字搜索 -->
|
||||
<cl-search-key placeholder="搜索名称、版本" />
|
||||
</cl-row>
|
||||
|
||||
<cl-row>
|
||||
<!-- 数据表格 -->
|
||||
<cl-table ref="Table" />
|
||||
</cl-row>
|
||||
|
||||
<cl-row>
|
||||
<cl-flex1 />
|
||||
<!-- 分页控件 -->
|
||||
<cl-pagination />
|
||||
</cl-row>
|
||||
|
||||
<!-- 新增、编辑 -->
|
||||
<cl-upsert ref="Upsert" />
|
||||
</cl-crud>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="app-version" setup>
|
||||
import { useCrud, useTable, useUpsert } from "@cool-vue/crud";
|
||||
import { useCool } from "/@/cool";
|
||||
import { useDict } from "/$/dict";
|
||||
import { computed, reactive } from "vue";
|
||||
|
||||
const { service } = useCool();
|
||||
const { dict } = useDict();
|
||||
|
||||
const options = reactive({
|
||||
// 状态
|
||||
status: [
|
||||
{
|
||||
label: "启用",
|
||||
value: 1,
|
||||
type: "success"
|
||||
},
|
||||
{
|
||||
label: "禁用",
|
||||
value: 0,
|
||||
type: "danger"
|
||||
}
|
||||
],
|
||||
// 类型
|
||||
type: computed(() => {
|
||||
return dict.get("upgradeType");
|
||||
})
|
||||
});
|
||||
|
||||
// cl-upsert
|
||||
const Upsert = useUpsert({
|
||||
items: [
|
||||
{ span: 12, prop: "name", label: "名称", required: true, component: { name: "el-input" } },
|
||||
{
|
||||
span: 12,
|
||||
prop: "version",
|
||||
label: "版本号",
|
||||
required: true,
|
||||
component: { name: "el-input" }
|
||||
},
|
||||
{
|
||||
prop: "description",
|
||||
label: "描述",
|
||||
required: true,
|
||||
component: {
|
||||
name: "el-input",
|
||||
props: {
|
||||
type: "textarea",
|
||||
rows: 4
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: "type",
|
||||
label: "类型",
|
||||
value: 0,
|
||||
component: { name: "el-radio-group", options: options.type },
|
||||
required: true
|
||||
},
|
||||
{
|
||||
prop: "url",
|
||||
label: "下载地址",
|
||||
component: { name: "cl-upload", props: { type: "file", limit: 1 } },
|
||||
required: true
|
||||
},
|
||||
{
|
||||
prop: "hotUpdate",
|
||||
label: "热更新",
|
||||
flex: false,
|
||||
value: 0,
|
||||
component: {
|
||||
name: "cl-switch"
|
||||
},
|
||||
required: true
|
||||
},
|
||||
{
|
||||
prop: "forceUpdate",
|
||||
label: "强制更新",
|
||||
flex: false,
|
||||
value: 0,
|
||||
component: {
|
||||
name: "cl-switch"
|
||||
},
|
||||
required: true
|
||||
},
|
||||
{
|
||||
prop: "status",
|
||||
label: "状态",
|
||||
value: 1,
|
||||
flex: false,
|
||||
component: { name: "cl-switch" },
|
||||
required: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// cl-table
|
||||
const Table = useTable({
|
||||
columns: [
|
||||
{ type: "selection" },
|
||||
{ prop: "name", label: "名称", minWidth: 150 },
|
||||
{ prop: "version", label: "版本号", minWidth: 100 },
|
||||
{ prop: "type", label: "类型", dict: options.type, minWidth: 100 },
|
||||
{ prop: "url", label: "下载地址", component: { name: "cl-link" }, minWidth: 120 },
|
||||
{
|
||||
prop: "forceUpdate",
|
||||
label: "强制更新",
|
||||
formatter(row) {
|
||||
return row.forceUpdate ? "是" : "否";
|
||||
},
|
||||
minWidth: 100
|
||||
},
|
||||
{
|
||||
prop: "hotUpdate",
|
||||
label: "热更新",
|
||||
formatter(row) {
|
||||
return row.hotUpdate ? "是" : "否";
|
||||
},
|
||||
minWidth: 100
|
||||
},
|
||||
{ prop: "status", label: "状态", component: { name: "cl-switch" }, minWidth: 100 },
|
||||
{ prop: "description", label: "描述", showOverflowTooltip: true, minWidth: 200 },
|
||||
{ prop: "createTime", label: "创建时间", sortable: "desc", minWidth: 160 },
|
||||
{ prop: "updateTime", label: "更新时间", sortable: "custom", minWidth: 160 },
|
||||
{ type: "op", buttons: ["edit", "delete"] }
|
||||
]
|
||||
});
|
||||
|
||||
// cl-crud
|
||||
const Crud = useCrud(
|
||||
{
|
||||
service: service.app.version
|
||||
},
|
||||
(app) => {
|
||||
app.refresh();
|
||||
}
|
||||
);
|
||||
</script>
|
2
src/modules/base/common/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./theme";
|
||||
export * from "./permission";
|
30
src/modules/base/common/permission.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { useStore } from "../store";
|
||||
import { isObject } from "lodash-es";
|
||||
|
||||
function parse(value: any) {
|
||||
const { menu } = useStore();
|
||||
|
||||
if (typeof value == "string") {
|
||||
return value ? menu.perms.some((e: any) => e.includes(value.replace(/\s/g, ""))) : false;
|
||||
} else {
|
||||
return Boolean(value);
|
||||
}
|
||||
}
|
||||
|
||||
export function checkPerm(value: string | { or?: string[]; and?: string[] }) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isObject(value)) {
|
||||
if (value.or) {
|
||||
return value.or.some(parse);
|
||||
}
|
||||
|
||||
if (value.and) {
|
||||
return value.and.some((e: any) => !parse(e)) ? false : true;
|
||||
}
|
||||
}
|
||||
|
||||
return parse(value);
|
||||
}
|
12
src/modules/base/common/theme.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { config } from "/@/config";
|
||||
import { createLink } from "../utils";
|
||||
|
||||
// 字体图标库加载
|
||||
if (config.app.iconfont) {
|
||||
config.app.iconfont.forEach((e: string) => {
|
||||
createLink(e);
|
||||
});
|
||||
}
|
||||
|
||||
// 默认
|
||||
createLink("//at.alicdn.com/t/c/font_3254019_h02ghb7ckt5.css");
|
53
src/modules/base/components/avatar/index.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import { UserFilled } from "@element-plus/icons-vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "cl-avatar",
|
||||
|
||||
props: {
|
||||
modelValue: String,
|
||||
src: String,
|
||||
icon: {
|
||||
type: null,
|
||||
default: UserFilled
|
||||
},
|
||||
size: {
|
||||
type: [String, Number] as PropType<"large" | "default" | "small" | number>,
|
||||
default: 40
|
||||
},
|
||||
shape: {
|
||||
type: String as PropType<"circle" | "square">,
|
||||
default: "square"
|
||||
},
|
||||
fit: {
|
||||
type: String as PropType<"fill" | "contain" | "cover" | "none" | "scale-down">,
|
||||
default: "cover"
|
||||
}
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
return () => {
|
||||
const height = props.size + "px";
|
||||
|
||||
return (
|
||||
<div
|
||||
class="cl-avatar"
|
||||
style={{
|
||||
height
|
||||
}}
|
||||
>
|
||||
<el-avatar
|
||||
style={{
|
||||
height,
|
||||
width: props.size + "px"
|
||||
}}
|
||||
{...{
|
||||
...props,
|
||||
src: props.modelValue || props.src
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
134
src/modules/base/components/code/json.vue
Normal file
@ -0,0 +1,134 @@
|
||||
<template v-if="text">
|
||||
<div class="cl-code-json__wrap" v-if="popover">
|
||||
<el-popover
|
||||
width="auto"
|
||||
placement="right"
|
||||
popper-class="cl-code-json__popper"
|
||||
effect="dark"
|
||||
>
|
||||
<template #reference>
|
||||
<span class="text">{{ text }}</span>
|
||||
</template>
|
||||
|
||||
<viewer />
|
||||
</el-popover>
|
||||
</div>
|
||||
|
||||
<viewer v-else>
|
||||
<template #op>
|
||||
<slot name="op"> </slot>
|
||||
</template>
|
||||
</viewer>
|
||||
</template>
|
||||
|
||||
<script lang="tsx" name="cl-code-json" setup>
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { isObject, isString } from "lodash-es";
|
||||
import { computed, defineComponent } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: [String, Object],
|
||||
popover: Boolean,
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: "100%"
|
||||
},
|
||||
maxHeight: {
|
||||
type: [Number, String],
|
||||
default: 300
|
||||
}
|
||||
});
|
||||
|
||||
const { copy } = useClipboard();
|
||||
|
||||
// 文本
|
||||
const text = computed(() => {
|
||||
const v = props.modelValue;
|
||||
|
||||
if (isString(v)) {
|
||||
return v;
|
||||
} else if (isObject(v)) {
|
||||
return JSON.stringify(v, null, 4);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
// 视图
|
||||
const viewer = defineComponent({
|
||||
setup(_, { slots }) {
|
||||
function toCopy() {
|
||||
copy(text.value);
|
||||
ElMessage.success("复制成功");
|
||||
}
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div class="cl-code-json">
|
||||
<div class="op">
|
||||
<el-button type="success" size="small" onClick={toCopy}>
|
||||
copy
|
||||
</el-button>
|
||||
|
||||
{slots.op && slots.op()}
|
||||
</div>
|
||||
|
||||
<el-scrollbar
|
||||
class="scrollbar"
|
||||
max-height={props.maxHeight}
|
||||
height={props.height}
|
||||
>
|
||||
<pre>
|
||||
<code>{text.value}</code>
|
||||
</pre>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.cl-code-json {
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
min-width: 200px;
|
||||
max-width: 500px;
|
||||
|
||||
.op {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
code {
|
||||
display: block;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
&__wrap {
|
||||
.text {
|
||||
display: block;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__popper {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
104
src/modules/base/components/dept/check.vue
Normal file
@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div class="cl-dept-check">
|
||||
<div class="cl-dept-check__search">
|
||||
<el-input v-model="keyword" placeholder="输入关键字进行过滤" />
|
||||
</div>
|
||||
|
||||
<div class="cl-dept-check__tree">
|
||||
<el-scrollbar max-height="200px">
|
||||
<el-tree
|
||||
ref="Tree"
|
||||
node-key="id"
|
||||
show-checkbox
|
||||
:data="list"
|
||||
:props="{
|
||||
label: 'name',
|
||||
children: 'children'
|
||||
}"
|
||||
:filter-node-method="filterNode"
|
||||
:check-strictly="checkStrictly"
|
||||
@check="onCheckChange"
|
||||
/>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="cl-dept-check" setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { deepTree } from "/@/cool/utils";
|
||||
import { useCool } from "/@/cool";
|
||||
import { useUpsert } from "@cool-vue/crud";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
checkStrictly: Boolean
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const { service } = useCool();
|
||||
|
||||
// el-tree
|
||||
const Tree = ref();
|
||||
|
||||
// 树形列表
|
||||
const list = ref();
|
||||
|
||||
// 关键字搜素
|
||||
const keyword = ref("");
|
||||
|
||||
// 刷新树形列表
|
||||
async function refresh() {
|
||||
return service.base.sys.department.list().then((res) => {
|
||||
list.value = deepTree(res);
|
||||
});
|
||||
}
|
||||
|
||||
// 过滤节点
|
||||
function filterNode(val: string, data: any) {
|
||||
if (!val) return true;
|
||||
return data.name.includes(val);
|
||||
}
|
||||
|
||||
// 值改变
|
||||
function onCheckChange(_: any, { checkedKeys }: any) {
|
||||
emit("update:modelValue", checkedKeys);
|
||||
}
|
||||
|
||||
// 监听过滤
|
||||
watch(keyword, (val: string) => {
|
||||
Tree.value?.filter(val);
|
||||
});
|
||||
|
||||
useUpsert({
|
||||
async onOpened() {
|
||||
await refresh();
|
||||
Tree.value?.setCheckedKeys(props.modelValue || []);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cl-dept-check {
|
||||
&__search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.el-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__tree {
|
||||
border: 1px solid var(--el-border-color);
|
||||
margin-top: 5px;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
padding: 5px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
83
src/modules/base/components/dept/select.vue
Normal file
@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div class="cl-dept-select">
|
||||
<el-tree-select
|
||||
v-model="value"
|
||||
node-key="id"
|
||||
:data="list"
|
||||
:props="{
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
children: 'children'
|
||||
}"
|
||||
:multiple="multiple"
|
||||
:check-strictly="checkStrictly"
|
||||
:show-checkbox="multiple"
|
||||
default-expand-all
|
||||
@change="onChange"
|
||||
@check="onCheckChange"
|
||||
></el-tree-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="cl-dept-select" setup>
|
||||
import { ElMessage } from "element-plus";
|
||||
import { onMounted, ref, useModel } from "vue";
|
||||
import { useCool } from "/@/cool";
|
||||
import { deepTree } from "/@/cool/utils";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: [Array, Number, String],
|
||||
multiple: Boolean,
|
||||
checkStrictly: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "change"]);
|
||||
|
||||
const { service } = useCool();
|
||||
|
||||
const value = useModel(props, "modelValue");
|
||||
|
||||
const list = ref();
|
||||
|
||||
// 单选改变
|
||||
function onChange(val: string) {
|
||||
if (!props.multiple) {
|
||||
emit("update:modelValue", val);
|
||||
}
|
||||
}
|
||||
|
||||
// 多选改变
|
||||
function onCheckChange(_: any, { checkedKeys }: any) {
|
||||
if (props.multiple) {
|
||||
emit("update:modelValue", checkedKeys);
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新
|
||||
function refresh() {
|
||||
service.base.sys.department
|
||||
.list()
|
||||
.then((res) => {
|
||||
list.value = deepTree(res);
|
||||
})
|
||||
.catch((err) => {
|
||||
list.value = [];
|
||||
ElMessage.error(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refresh();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cl-dept-select {
|
||||
:deep(.el-select) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
42
src/modules/base/components/editor/index.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { defineComponent, h, resolveComponent, ref, reactive, watch } from "vue";
|
||||
import { isComponent } from "/@/cool/utils";
|
||||
|
||||
export default defineComponent({
|
||||
name: "cl-editor",
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
setup(props, { slots, expose }) {
|
||||
const Editor = ref();
|
||||
const ex = reactive({});
|
||||
|
||||
watch(Editor, (v) => {
|
||||
if (v) {
|
||||
Object.assign(ex, v);
|
||||
}
|
||||
});
|
||||
|
||||
expose(ex);
|
||||
|
||||
return () => {
|
||||
return isComponent(props.name) ? (
|
||||
h(
|
||||
// @ts-ignore
|
||||
resolveComponent(props.name),
|
||||
{
|
||||
...props,
|
||||
ref: Editor
|
||||
},
|
||||
slots
|
||||
)
|
||||
) : (
|
||||
<el-input type="textarea" rows={4} placeholder="请输入" {...props} />
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
48
src/modules/base/components/icon/svg.vue
Normal file
@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<svg :class="svgClass" :style="style" aria-hidden="true">
|
||||
<use :xlink:href="iconName" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, reactive } from "vue";
|
||||
import { parsePx } from "/@/cool/utils";
|
||||
|
||||
export default defineComponent({
|
||||
name: "cl-svg",
|
||||
|
||||
props: {
|
||||
name: String,
|
||||
className: String,
|
||||
color: String,
|
||||
size: [String, Number]
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const style = reactive({
|
||||
fontSize: parsePx(props.size!)
|
||||
});
|
||||
|
||||
const iconName = computed(() => `#icon-${props.name}`);
|
||||
const svgClass = computed(() => {
|
||||
return ["cl-svg", `cl-svg__${props.name}`, String(props.className || "")];
|
||||
});
|
||||
|
||||
return {
|
||||
style,
|
||||
iconName,
|
||||
svgClass
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cl-svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
98
src/modules/base/components/image/index.vue
Normal file
@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div
|
||||
class="cl-image"
|
||||
:style="{
|
||||
height: style.h
|
||||
}"
|
||||
>
|
||||
<el-image
|
||||
:src="urls[0]"
|
||||
:fit="fit"
|
||||
:lazy="lazy"
|
||||
:preview-src-list="urls"
|
||||
:style="{
|
||||
height: style.h,
|
||||
width: style.w
|
||||
}"
|
||||
preview-teleported
|
||||
>
|
||||
<template #error>
|
||||
<div class="cl-image__slot">
|
||||
<el-icon :size="20"><picture-filled /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { type PropType, computed, defineComponent } from "vue";
|
||||
import { isArray, isNumber, isString } from "lodash-es";
|
||||
import { PictureFilled } from "@element-plus/icons-vue";
|
||||
import { parsePx } from "/@/cool/utils";
|
||||
|
||||
export default defineComponent({
|
||||
name: "cl-image",
|
||||
|
||||
components: {
|
||||
PictureFilled
|
||||
},
|
||||
|
||||
props: {
|
||||
modelValue: [String, Array],
|
||||
src: [String, Array],
|
||||
size: {
|
||||
type: [Number, Array],
|
||||
default: 100
|
||||
},
|
||||
lazy: Boolean,
|
||||
fit: {
|
||||
type: String as PropType<"" | "contain" | "cover" | "none" | "fill" | "scale-down">,
|
||||
default: "cover"
|
||||
}
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const urls = computed(() => {
|
||||
const urls: any = props.modelValue || props.src;
|
||||
|
||||
if (isArray(urls)) {
|
||||
return urls;
|
||||
}
|
||||
|
||||
if (isString(urls)) {
|
||||
return (urls || "").split(",").filter(Boolean);
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
|
||||
const style = computed(() => {
|
||||
const [h, w]: any = isNumber(props.size) ? [props.size, props.size] : props.size;
|
||||
|
||||
return {
|
||||
h: parsePx(h),
|
||||
w: parsePx(w)
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
urls,
|
||||
style
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cl-image {
|
||||
&__slot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
background-color: #f7f7f7;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
86
src/modules/base/components/link/index.vue
Normal file
@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="cl-link">
|
||||
<a v-for="item in urls" :key="item" class="cl-link__item" :href="item" :target="target">
|
||||
<el-icon><icon-link /></el-icon>
|
||||
<span>{{ text || filename(item) }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from "vue";
|
||||
import { isArray, isString, last } from "lodash-es";
|
||||
import { Link } from "@element-plus/icons-vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "cl-link",
|
||||
|
||||
components: {
|
||||
"icon-link": Link
|
||||
},
|
||||
|
||||
props: {
|
||||
modelValue: [String, Array],
|
||||
href: [String, Array],
|
||||
text: String,
|
||||
target: {
|
||||
type: String,
|
||||
default: "_blank"
|
||||
}
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const urls = computed(() => {
|
||||
const urls: any = props.modelValue || props.href;
|
||||
|
||||
if (isArray(urls)) {
|
||||
return urls;
|
||||
}
|
||||
|
||||
if (isString(urls)) {
|
||||
return (urls || "").split(",").filter(Boolean);
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
|
||||
function filename(url: string) {
|
||||
return last(url.split("/"));
|
||||
}
|
||||
|
||||
return {
|
||||
urls,
|
||||
filename
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cl-link {
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--el-color-primary);
|
||||
padding: 0 5px;
|
||||
border-radius: 6px;
|
||||
margin: 2px;
|
||||
text-decoration: none;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
92
src/modules/base/components/menu/check.vue
Normal file
@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div class="cl-menu-check">
|
||||
<el-input v-model="keyword" placeholder="输入关键字进行过滤" />
|
||||
|
||||
<div class="cl-menu-check__scroller">
|
||||
<el-scrollbar max-height="200px">
|
||||
<el-tree
|
||||
ref="Tree"
|
||||
node-key="id"
|
||||
show-checkbox
|
||||
:data="list"
|
||||
:props="{
|
||||
label: 'name',
|
||||
children: 'children'
|
||||
}"
|
||||
:filter-node-method="filterNode"
|
||||
@check="onCheckChange"
|
||||
/>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="cl-menu-check" setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { deepTree } from "/@/cool/utils";
|
||||
import { useCool } from "/@/cool";
|
||||
import { useUpsert } from "@cool-vue/crud";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const { service } = useCool();
|
||||
|
||||
// el-tree 组件
|
||||
const Tree = ref();
|
||||
|
||||
// 树形列表
|
||||
const list = ref();
|
||||
|
||||
// 搜索关键字
|
||||
const keyword = ref("");
|
||||
|
||||
// 刷新列表
|
||||
async function refresh() {
|
||||
return service.base.sys.menu.list().then((res) => {
|
||||
list.value = deepTree(res);
|
||||
});
|
||||
}
|
||||
|
||||
// 过滤节点
|
||||
function filterNode(val: string, data: any) {
|
||||
if (!val) return true;
|
||||
return data.name.includes(val);
|
||||
}
|
||||
|
||||
// 值改变
|
||||
function onCheckChange(_: any, { checkedKeys, halfCheckedKeys }: any) {
|
||||
emit("update:modelValue", [...checkedKeys, ...halfCheckedKeys]);
|
||||
}
|
||||
|
||||
// 过滤监听
|
||||
watch(keyword, (val: string) => {
|
||||
Tree.value.filter(val);
|
||||
});
|
||||
|
||||
useUpsert({
|
||||
async onOpened() {
|
||||
await refresh();
|
||||
Tree.value?.setCheckedKeys(
|
||||
(props.modelValue || []).filter((e) => Tree.value.getNode(e)?.isLeaf)
|
||||
);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cl-menu-check {
|
||||
&__scroller {
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
padding: 5px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
128
src/modules/base/components/menu/file.vue
Normal file
@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div class="cl-menu-file">
|
||||
<template v-if="isEdit">
|
||||
<el-input placeholder="请输入" v-model="text" @change="onTextChange"></el-input>
|
||||
|
||||
<el-tooltip content="选择文件">
|
||||
<el-icon @click="toggle(false)">
|
||||
<folder-checked />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<el-cascader
|
||||
v-model="path"
|
||||
:options="data"
|
||||
clearable
|
||||
filterable
|
||||
allow-create
|
||||
@change="onPathChange"
|
||||
/>
|
||||
|
||||
<el-tooltip content="输入编辑">
|
||||
<el-icon @click="toggle(true)">
|
||||
<edit />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="cl-menu-file" setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { deepPaths } from "/@/cool/utils";
|
||||
import { FolderChecked, Edit } from "@element-plus/icons-vue";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "change"]);
|
||||
|
||||
// 扫描文件
|
||||
function findFiles() {
|
||||
const files = import.meta.glob(["/src/modules/*/{views,pages}/**/*", "!**/components"]);
|
||||
const list: string[] = [];
|
||||
|
||||
for (const i in files) {
|
||||
if (!i.includes("base/pages")) {
|
||||
list.push(i.substring(13));
|
||||
}
|
||||
}
|
||||
|
||||
return deepPaths(list);
|
||||
}
|
||||
|
||||
// 路径
|
||||
const path = ref();
|
||||
|
||||
// 文本
|
||||
const text = ref();
|
||||
|
||||
// 是否编辑
|
||||
const isEdit = ref(false);
|
||||
|
||||
// 数据列表
|
||||
const data = ref(findFiles());
|
||||
|
||||
// 路径值改变
|
||||
function onPathChange(arr: string[]) {
|
||||
const v = "modules/" + (arr || []).join("/");
|
||||
emit("update:modelValue", v);
|
||||
emit("change", v);
|
||||
}
|
||||
|
||||
// 文本值改变
|
||||
function onTextChange(v: string) {
|
||||
emit("update:modelValue", v);
|
||||
emit("change", v);
|
||||
}
|
||||
|
||||
// 切换
|
||||
function toggle(f: boolean) {
|
||||
isEdit.value = f;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val) {
|
||||
if (val.includes("http")) {
|
||||
text.value = val;
|
||||
isEdit.value = true;
|
||||
} else {
|
||||
path.value = val.replace(/(modules\/|cool\/)/g, "").split("/");
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cl-menu-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-cascader) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
margin: 0 0 0 10px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
51
src/modules/base/components/menu/icon.vue
Normal file
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<el-select filterable v-model="value" fit-input-width>
|
||||
<div class="cl-menu-icon">
|
||||
<el-option :value="item" v-for="item in list" :key="item">
|
||||
<cl-svg :name="item" />
|
||||
</el-option>
|
||||
</div>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="cl-menu-icon" setup>
|
||||
import { ref, useModel } from "vue";
|
||||
import { svgIcons } from "virtual:svg-icons";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
// 图标列表
|
||||
const list = ref(svgIcons.filter((e) => e.indexOf("icon-") === 0));
|
||||
|
||||
// 已选图标
|
||||
const value = useModel(props, "modelValue");
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cl-menu-icon {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding-left: 5px;
|
||||
|
||||
.el-select-dropdown__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.cl-svg {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
91
src/modules/base/components/menu/perms.vue
Normal file
@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div class="cl-menu-perms">
|
||||
<el-cascader
|
||||
v-model="value"
|
||||
separator=":"
|
||||
clearable
|
||||
filterable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
:disabled="disabled"
|
||||
:options="data"
|
||||
:props="cascaderProps"
|
||||
@change="onChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="cl-menu-perms" setup>
|
||||
import { onMounted, ref, watch, reactive } from "vue";
|
||||
import { useCool } from "/@/cool";
|
||||
import { deepPaths } from "/@/cool/utils";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
disabled: Boolean
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const { service } = useCool();
|
||||
|
||||
// 绑定值
|
||||
const value = ref<string[][]>([]);
|
||||
|
||||
// 权限列表
|
||||
const data = ref<any[]>([]);
|
||||
|
||||
// elm BUG
|
||||
const cascaderProps = reactive({ multiple: true });
|
||||
|
||||
// 监听改变
|
||||
function onChange(arr: any) {
|
||||
emit("update:modelValue", arr.map((e: string[]) => e.join(":")).join(","));
|
||||
}
|
||||
|
||||
// 监听值
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
value.value = val ? val.split(",").map((e) => e.split(":")) : [];
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
const list: any[] = [];
|
||||
|
||||
function deep(s: any) {
|
||||
if (typeof s == "object") {
|
||||
for (const i in s) {
|
||||
const { permission } = s[i];
|
||||
|
||||
if (permission) {
|
||||
list.push(...Object.values(permission));
|
||||
} else {
|
||||
deep(s[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deep(service);
|
||||
|
||||
data.value = deepPaths(list, ":");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cl-menu-perms {
|
||||
line-height: 0;
|
||||
|
||||
:deep(.el-cascader) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
83
src/modules/base/components/menu/select.vue
Normal file
@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div class="cl-menu-select">
|
||||
<el-tree-select
|
||||
v-model="value"
|
||||
:data="tree"
|
||||
:props="{
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
disabled: 'disabled',
|
||||
children: 'children'
|
||||
}"
|
||||
clearable
|
||||
default-expand-all
|
||||
check-strictly
|
||||
filterable
|
||||
:size="size"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="cl-menu-select" setup>
|
||||
import { useForm } from "@cool-vue/crud";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { computed, ref, useModel, onMounted } from "vue";
|
||||
import { useCool } from "/@/cool";
|
||||
import { deepTree } from "/@/cool/utils";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: [Number, String],
|
||||
type: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
placeholder: String,
|
||||
size: String
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const { service } = useCool();
|
||||
const Form = useForm();
|
||||
|
||||
// 绑定值
|
||||
const value = useModel(props, "modelValue", {
|
||||
get(val) {
|
||||
return val ? Number(val) : val;
|
||||
}
|
||||
});
|
||||
|
||||
// 菜单列表
|
||||
const list = ref<any[]>([]);
|
||||
|
||||
// 树形列表
|
||||
const tree = computed(() => {
|
||||
// 过滤掉自己和下级的数据
|
||||
const data = list.value.filter(
|
||||
(e) =>
|
||||
e.id != Form.value?.form.id && (props.type === 0 ? e.type == 0 : props.type > e.type!)
|
||||
);
|
||||
|
||||
return deepTree(cloneDeep(data)).filter((e) => !e.parentId);
|
||||
});
|
||||
|
||||
// 刷新列表
|
||||
function refresh() {
|
||||
service.base.sys.menu.list().then((res) => {
|
||||
list.value = res;
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refresh();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cl-menu-select {
|
||||
:deep(.el-select) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
90
src/modules/base/config.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import type { ModuleConfig } from "/@/cool";
|
||||
import { useStore } from "./store";
|
||||
import { config } from "/@/config";
|
||||
import "./static/css/index.scss";
|
||||
|
||||
export default (): ModuleConfig => {
|
||||
return {
|
||||
order: 99,
|
||||
components: Object.values(import.meta.glob("./components/**/*.{vue,tsx}")),
|
||||
views: [
|
||||
{
|
||||
path: "/my/info",
|
||||
meta: {
|
||||
label: "个人中心"
|
||||
},
|
||||
component: () => import("./views/info.vue")
|
||||
}
|
||||
],
|
||||
pages: [
|
||||
{
|
||||
path: "/login",
|
||||
component: () => import("./pages/login/index.vue")
|
||||
},
|
||||
{
|
||||
path: "/401",
|
||||
meta: {
|
||||
process: false
|
||||
},
|
||||
component: () => import("./pages/error/401.vue")
|
||||
},
|
||||
{
|
||||
path: "/403",
|
||||
meta: {
|
||||
process: false
|
||||
},
|
||||
component: () => import("./pages/error/403.vue")
|
||||
},
|
||||
{
|
||||
path: "/404",
|
||||
meta: {
|
||||
process: false
|
||||
},
|
||||
component: () => import("./pages/error/404.vue")
|
||||
},
|
||||
{
|
||||
path: "/500",
|
||||
meta: {
|
||||
process: false
|
||||
},
|
||||
component: () => import("./pages/error/500.vue")
|
||||
},
|
||||
{
|
||||
path: "/502",
|
||||
meta: {
|
||||
process: false
|
||||
},
|
||||
component: () => import("./pages/error/502.vue")
|
||||
}
|
||||
],
|
||||
install() {
|
||||
// 设置标题
|
||||
document.title = config.app.name;
|
||||
},
|
||||
async onLoad() {
|
||||
const { user, menu, app } = useStore();
|
||||
|
||||
// token 事件
|
||||
async function hasToken(cb: () => Promise<any> | void) {
|
||||
if (cb) {
|
||||
app.addEvent("hasToken", cb);
|
||||
|
||||
if (user.token) {
|
||||
await cb();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await hasToken(async () => {
|
||||
// 获取用户信息
|
||||
user.get();
|
||||
// 获取菜单权限
|
||||
await menu.get();
|
||||
});
|
||||
|
||||
return {
|
||||
hasToken
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
13
src/modules/base/directives/permission.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { checkPerm } from "../common/permission";
|
||||
|
||||
function change(el: any, binding: any) {
|
||||
el.style.display = checkPerm(binding.value) ? el.getAttribute("_display") : "none";
|
||||
}
|
||||
|
||||
export default {
|
||||
created(el: any, binding: any) {
|
||||
el.setAttribute("_display", el.style.display || "");
|
||||
change(el, binding);
|
||||
},
|
||||
updated: change
|
||||
};
|
43
src/modules/base/hooks/dept.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { TreeData } from "element-plus/es/components/tree/src/tree.type";
|
||||
import { service } from "/@/cool";
|
||||
import Node from "element-plus/es/components/tree/src/model/node";
|
||||
import ClAvatar from "../components/avatar/index";
|
||||
import { type ClViewGroup, useViewGroup } from "/@/plugins/view";
|
||||
|
||||
export function useDeptViewGroup(options: DeepPartial<ClViewGroup.Options>) {
|
||||
const { ViewGroup } = useViewGroup({
|
||||
label: "员工列表",
|
||||
service: service.base.sys.department,
|
||||
enableAdd: false,
|
||||
enableRefresh: false,
|
||||
enableContextMenu: false,
|
||||
tree: {
|
||||
lazy: true,
|
||||
onLoad(node: Node, resolve: (data: TreeData) => void) {
|
||||
if (node.data.id) {
|
||||
service.base.sys.user.list({ departmentId: node.data.id }).then((res) => {
|
||||
res.forEach((e) => {
|
||||
e.isLeaf = true;
|
||||
e.icon = (
|
||||
<ClAvatar
|
||||
size={22}
|
||||
src={e.headImg}
|
||||
style={{ marginRight: "6px" }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
res.unshift(...(node.data.children || []));
|
||||
|
||||
resolve(res);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
...options
|
||||
});
|
||||
|
||||
return {
|
||||
ViewGroup
|
||||
};
|
||||
}
|
1
src/modules/base/hooks/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./dept";
|
11
src/modules/base/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { useStore } from "./store";
|
||||
|
||||
export function useBase() {
|
||||
return {
|
||||
...useStore()
|
||||
};
|
||||
}
|
||||
|
||||
export * from "./common";
|
||||
export * from "./hooks";
|
||||
export * from "./types/index.d";
|
7
src/modules/base/pages/error/401.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<error-page :code="401" desc="认证失败,请重新登录!" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="401" setup>
|
||||
import ErrorPage from "./components/error-page.vue";
|
||||
</script>
|
7
src/modules/base/pages/error/403.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<error-page :code="403" desc="您无权访问此页面" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="403" setup>
|
||||
import ErrorPage from "./components/error-page.vue";
|
||||
</script>
|
7
src/modules/base/pages/error/404.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<error-page :code="404" desc="找不到您要查找的页面" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="404" setup>
|
||||
import ErrorPage from "./components/error-page.vue";
|
||||
</script>
|
7
src/modules/base/pages/error/500.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<error-page :code="500" desc="糟糕,出了点问题" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="500" setup>
|
||||
import ErrorPage from "./components/error-page.vue";
|
||||
</script>
|
7
src/modules/base/pages/error/502.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<error-page :code="502" desc="马上回来" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="502" setup>
|
||||
import ErrorPage from "./components/error-page.vue";
|
||||
</script>
|
136
src/modules/base/pages/error/components/error-page.vue
Normal file
@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div class="error-page">
|
||||
<div class="error-page__wrap">
|
||||
<h1 class="error-page__code">
|
||||
<span v-for="c in codes" :key="c">
|
||||
{{ c }}
|
||||
</span>
|
||||
</h1>
|
||||
<p class="error-page__desc">{{ desc }}</p>
|
||||
|
||||
<template v-if="user.token || isLogout">
|
||||
<div class="error-page__btns">
|
||||
<el-button @click="home">回到首页</el-button>
|
||||
<el-button type="primary" @click="reLogin">重新登录</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="error-page__btns">
|
||||
<el-button type="primary" @click="toLogin">返回登录页</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="error-page__bg is-tl">
|
||||
<cl-svg name="bg" />
|
||||
</div>
|
||||
|
||||
<div class="error-page__bg is-br">
|
||||
<cl-svg name="bg" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { useCool } from "/@/cool";
|
||||
import { useBase } from "/$/base";
|
||||
|
||||
const props = defineProps({
|
||||
code: Number,
|
||||
desc: String
|
||||
});
|
||||
|
||||
const { router } = useCool();
|
||||
const { user } = useBase();
|
||||
|
||||
const isLogout = ref(false);
|
||||
|
||||
const codes = computed(() => {
|
||||
return (props.code || "").toString().split("");
|
||||
});
|
||||
|
||||
function toLogin() {
|
||||
router.push("/login");
|
||||
}
|
||||
|
||||
async function reLogin() {
|
||||
isLogout.value = true;
|
||||
user.logout();
|
||||
}
|
||||
|
||||
function home() {
|
||||
router.push("/");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.error-page {
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
&__wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
&__bg {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 50%;
|
||||
pointer-events: none;
|
||||
transform: rotate(180deg) scaleY(-1);
|
||||
|
||||
.cl-svg {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
fill: #2c3142;
|
||||
}
|
||||
|
||||
&.is-tl {
|
||||
left: 0;
|
||||
top: 0;
|
||||
transform: rotate(180deg) scaleY(-1);
|
||||
}
|
||||
|
||||
&.is-br {
|
||||
top: 0;
|
||||
right: 0;
|
||||
transform: scaleY(-1);
|
||||
}
|
||||
}
|
||||
|
||||
&__code {
|
||||
font-size: 120px;
|
||||
font-weight: normal;
|
||||
color: #6c757d;
|
||||
font-family: Consolas;
|
||||
margin-top: -40px;
|
||||
animation: dou 1s infinite linear;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
color: #6c757d;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
&__btns {
|
||||
display: flex;
|
||||
margin-top: 40px;
|
||||
|
||||
.el-button {
|
||||
margin: 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
110
src/modules/base/pages/login/components/pic-captcha.vue
Normal file
@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div class="pic-captcha" @click="refresh">
|
||||
<div v-if="svg" class="svg" v-html="svg" />
|
||||
<img v-else-if="base64" class="base64" :src="base64" alt="" />
|
||||
|
||||
<template v-else-if="isError">
|
||||
<el-text type="danger"> 后端未启动 </el-text>
|
||||
<el-icon color="#f56c6c" :size="16">
|
||||
<warning-filled />
|
||||
</el-icon>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<el-icon class="is-loading" :size="18">
|
||||
<loading />
|
||||
</el-icon>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Loading, WarningFilled } from "@element-plus/icons-vue";
|
||||
import { useCool } from "/@/cool";
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "change"]);
|
||||
|
||||
const { service } = useCool();
|
||||
|
||||
// 是否异常
|
||||
const isError = ref(false);
|
||||
|
||||
// base64
|
||||
const base64 = ref("");
|
||||
|
||||
// svg
|
||||
const svg = ref("");
|
||||
|
||||
// 刷新
|
||||
async function refresh() {
|
||||
isError.value = false;
|
||||
svg.value = "";
|
||||
base64.value = "";
|
||||
|
||||
await service.base.open
|
||||
.captcha({
|
||||
height: 45,
|
||||
width: 150,
|
||||
color: "#2c3142"
|
||||
})
|
||||
.then(({ captchaId, data }) => {
|
||||
if (data) {
|
||||
if (data.includes(";base64,")) {
|
||||
base64.value = data;
|
||||
} else {
|
||||
svg.value = data;
|
||||
}
|
||||
|
||||
emit("update:modelValue", captchaId);
|
||||
emit("change", {
|
||||
base64,
|
||||
svg,
|
||||
captchaId
|
||||
});
|
||||
} else {
|
||||
ElMessage.error("验证码获取失败");
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
ElMessage.error(err.message);
|
||||
isError.value = true;
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refresh();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
refresh
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pic-captcha {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
height: 45px;
|
||||
width: 150px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
|
||||
.svg {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.base64 {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
297
src/modules/base/pages/login/index.vue
Normal file
@ -0,0 +1,297 @@
|
||||
<template>
|
||||
<div class="page-login">
|
||||
<div class="box">
|
||||
<div class="logo">
|
||||
<img src="/logo.png" alt="Logo" />
|
||||
<div class="name">
|
||||
<span v-for="text in app.info.name" :key="text">{{ text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="desc">快速开发后台权限管理系统</p>
|
||||
|
||||
<div class="form">
|
||||
<el-form label-position="top" class="form" :disabled="saving">
|
||||
<el-form-item label="用户名">
|
||||
<input
|
||||
v-model="form.username"
|
||||
placeholder="请输入用户名"
|
||||
maxlength="20"
|
||||
type="text"
|
||||
:readonly="readonly"
|
||||
autocomplete="off"
|
||||
@focus="readonly = false"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="密码">
|
||||
<input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
maxlength="20"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="验证码">
|
||||
<div class="row">
|
||||
<input
|
||||
v-model="form.verifyCode"
|
||||
placeholder="图片验证码"
|
||||
maxlength="4"
|
||||
@keyup.enter="toLogin"
|
||||
/>
|
||||
|
||||
<pic-captcha
|
||||
:ref="setRefs('picCaptcha')"
|
||||
v-model="form.captchaId"
|
||||
@change="
|
||||
() => {
|
||||
form.verifyCode = '';
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<div class="op">
|
||||
<el-button type="primary" :loading="saving" @click="toLogin"
|
||||
>登录</el-button
|
||||
>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg">
|
||||
<cl-svg name="bg"></cl-svg>
|
||||
</div>
|
||||
|
||||
<a href="https://cool-js.com" class="copyright"> Copyright © COOL </a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="login" setup>
|
||||
import { reactive, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { useCool } from "/@/cool";
|
||||
import { useBase } from "/$/base";
|
||||
import PicCaptcha from "./components/pic-captcha.vue";
|
||||
import { storage } from "/@/cool/utils";
|
||||
|
||||
const { refs, setRefs, router, service } = useCool();
|
||||
const { user, app } = useBase();
|
||||
|
||||
// 状态
|
||||
const saving = ref(false);
|
||||
|
||||
// 避免自动填充
|
||||
const readonly = ref(true);
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
username: storage.get("username") || "",
|
||||
password: "",
|
||||
captchaId: "",
|
||||
verifyCode: ""
|
||||
});
|
||||
|
||||
// 登录
|
||||
async function toLogin() {
|
||||
if (!form.username) {
|
||||
return ElMessage.error("用户名不能为空");
|
||||
}
|
||||
|
||||
if (!form.password) {
|
||||
return ElMessage.error("密码不能为空");
|
||||
}
|
||||
|
||||
if (!form.verifyCode) {
|
||||
return ElMessage.error("图片验证码不能为空");
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
|
||||
try {
|
||||
// 登录
|
||||
await service.base.open.login(form).then(user.setToken);
|
||||
|
||||
// token 事件
|
||||
await Promise.all(app.events.hasToken.map((e) => e()));
|
||||
|
||||
// 设置缓存
|
||||
storage.set("username", form.username);
|
||||
|
||||
// 跳转首页
|
||||
router.push("/");
|
||||
} catch (err: any) {
|
||||
// 刷新验证码
|
||||
refs.picCaptcha.refresh();
|
||||
|
||||
// 提示错误
|
||||
ElMessage.error(err.message);
|
||||
}
|
||||
|
||||
saving.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$color: #2c3142;
|
||||
|
||||
.page-login {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
color: $color;
|
||||
|
||||
.bg {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 90%;
|
||||
pointer-events: none;
|
||||
transform: rotate(180deg) scaleY(-1);
|
||||
|
||||
.cl-svg {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
fill: $color;
|
||||
}
|
||||
}
|
||||
|
||||
.copyright {
|
||||
position: absolute;
|
||||
bottom: 15px;
|
||||
left: 0;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 50%;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 9;
|
||||
|
||||
.logo {
|
||||
height: 50px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
height: 46px;
|
||||
background-color: $color;
|
||||
border-radius: 50px;
|
||||
border: 3px solid $color;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
font-size: 38px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
letter-spacing: 3px;
|
||||
|
||||
&:nth-child(6) {
|
||||
animation: dou 1s infinite linear;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 15px;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.form {
|
||||
width: 300px;
|
||||
|
||||
:deep(.el-form) {
|
||||
.el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
input {
|
||||
height: 45px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font-size: 17px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background-color: #f8f8f8;
|
||||
padding: 0 15px;
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
|
||||
&:-webkit-autofill {
|
||||
box-shadow: none;
|
||||
-webkit-box-shadow: 0 0 0 1000px #f8f8f8 inset;
|
||||
box-shadow: 0 0 0 1000px #f8f8f8 inset;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
.pic-captcha {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.op {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 40px;
|
||||
|
||||
:deep(.el-button) {
|
||||
height: 45px;
|
||||
width: 100%;
|
||||
font-size: 15px;
|
||||
border-radius: 6px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
.page-login {
|
||||
.box {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
39
src/modules/base/pages/login/static/bg.svg
Normal file
@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 500 350"
|
||||
>
|
||||
<g transform="">
|
||||
<g transform="translate(628,-17) scale(100)" opacity="0.3">
|
||||
<path
|
||||
d="M4.10125 0 C4.10125 0.5525 4.3542 0.8338 4.1835 1.3593 S3.6427 1.9637 3.318 2.4107 S3.0325 3.2339 2.5855 3.5587 S1.7928 3.7298 1.2674 3.9005 S0.5525 4.3988 0 4.3988 S-0.7419 4.0713 -1.2674 3.9005 S-2.1385 3.8834 -2.5855 3.5587 S-2.9932 2.8576 -3.318 2.4107 S-4.0127 1.8847 -4.1835 1.3593 S-4.1013 0.5525 -4.1013 0 S-4.3542 -0.8338 -4.1835 -1.3593 S-3.6427 -1.9637 -3.318 -2.4107 S-3.0325 -3.2339 -2.5855 -3.5587 S-1.7928 -3.7298 -1.2674 -3.9005 S-0.5525 -4.3988 0 -4.3988 S0.7419 -4.0713 1.2674 -3.9005 S2.1385 -3.8834 2.5855 -3.5587 S2.9932 -2.8576 3.318 -2.4107 S4.0127 -1.8847 4.1835 -1.3593 S4.1013 -0.5525 4.1013 0"
|
||||
stroke-width="0"
|
||||
transform="rotate(19)"
|
||||
>
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
dur="10s"
|
||||
repeatCount="indefinite"
|
||||
values="0;36"
|
||||
></animateTransform>
|
||||
</path>
|
||||
</g>
|
||||
<g transform="translate(704,-56) scale(100)" opacity="0.9">
|
||||
<path
|
||||
d="M4.9215 0 C4.9215 0.663 5.225 1.0006 5.0202 1.6311 S4.3713 2.3564 3.9816 2.8928 S3.639 3.8807 3.1026 4.2704 S2.1514 4.4757 1.5208 4.6806 S0.663 5.2785 0 5.2785 S-0.8903 4.8855 -1.5208 4.6806 S-2.5662 4.6601 -3.1026 4.2704 S-3.5919 3.4292 -3.9816 2.8928 S-4.8153 2.2617 -5.0202 1.6311 S-4.9215 0.663 -4.9215 0 S-5.225 -1.0006 -5.0202 -1.6311 S-4.3713 -2.3564 -3.9816 -2.8928 S-3.639 -3.8807 -3.1026 -4.2704 S-2.1514 -4.4757 -1.5208 -4.6806 S-0.663 -5.2785 0 -5.2785 S0.8903 -4.8855 1.5208 -4.6806 S2.5662 -4.6601 3.1026 -4.2704 S3.5919 -3.4292 3.9816 -2.8928 S4.8153 -2.2617 5.0202 -1.6311 S4.9215 -0.663 4.9215 0"
|
||||
stroke-width="0"
|
||||
transform="rotate(2.04427)"
|
||||
>
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
dur="6s"
|
||||
repeatCount="indefinite"
|
||||
values="0;36"
|
||||
></animateTransform>
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
154
src/modules/base/pages/main/components/amenu.vue
Normal file
@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<div class="a-menu">
|
||||
<el-menu
|
||||
:default-active="active"
|
||||
mode="horizontal"
|
||||
background-color="transparent"
|
||||
@select="select"
|
||||
>
|
||||
<template v-for="(item, index) in list" :key="item.id">
|
||||
<el-menu-item :index="`${index}`">
|
||||
<cl-svg v-if="item.icon" :name="item.icon" :size="18" />
|
||||
<span class="a-menu__name">{{ item.meta?.label }}</span>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="a-menu" setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useBase, Menu } from "/$/base";
|
||||
import { useCool } from "/@/cool";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
const { router, route } = useCool();
|
||||
const { menu } = useBase();
|
||||
|
||||
// 选中标识
|
||||
const active = ref("0");
|
||||
|
||||
// 组列表
|
||||
const list = computed(() => {
|
||||
return menu.group.filter((e) => e.isShow);
|
||||
});
|
||||
|
||||
// 选择导航
|
||||
function select(index: any) {
|
||||
if (String(index) == active.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 选中的组
|
||||
const item = list.value[index];
|
||||
|
||||
// 获取第一个菜单地址
|
||||
const url = menu.getPath(item);
|
||||
|
||||
if (url) {
|
||||
// 设置左侧菜单
|
||||
menu.setMenu(index);
|
||||
|
||||
// 跳转
|
||||
router.push(url);
|
||||
} else {
|
||||
ElMessage.warning(`“${item.meta?.label}”下没有菜单,请先添加`);
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新
|
||||
function refresh() {
|
||||
let index = 0;
|
||||
|
||||
function deep(e: Menu.Item, i: number) {
|
||||
switch (e.type) {
|
||||
case 0:
|
||||
if (e.children) {
|
||||
e.children.forEach((e) => {
|
||||
deep(e, i);
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
case 1:
|
||||
if (route.path.includes(e.path)) {
|
||||
index = i;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历所有分组
|
||||
list.value.forEach(deep);
|
||||
|
||||
// 确认选择
|
||||
active.value = String(index);
|
||||
|
||||
// 设置该分组下的菜单
|
||||
menu.setMenu(index);
|
||||
}
|
||||
|
||||
// 监听变化
|
||||
watch(
|
||||
() => [route.path, menu.group.length],
|
||||
() => {
|
||||
refresh();
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.a-menu {
|
||||
margin: 5px 0 0 10px;
|
||||
|
||||
.el-menu {
|
||||
height: 40px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
|
||||
:deep(.el-sub-menu__title) {
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
padding: 0 15px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: #999;
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
margin-left: 3px;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: var(--color-primary);
|
||||
border-radius: 6px 6px 0 0;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.cl-svg {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__name {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
97
src/modules/base/pages/main/components/bmenu.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import { defineComponent, h } from "vue";
|
||||
import { useBase, Menu } from "/$/base";
|
||||
import { useCool } from "/@/cool";
|
||||
|
||||
export default defineComponent({
|
||||
name: "b-menu",
|
||||
|
||||
setup() {
|
||||
const { router, route, browser } = useCool();
|
||||
const { menu, app } = useBase();
|
||||
|
||||
// 页面跳转
|
||||
function toView(url: string) {
|
||||
if (url != route.path) {
|
||||
router.push(url);
|
||||
}
|
||||
|
||||
// 小屏下点击收起左侧菜单
|
||||
if (browser.isMini) {
|
||||
app.fold(true);
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染子菜单
|
||||
function renderMenu() {
|
||||
function deep(list: Menu.Item[], index: number) {
|
||||
return list
|
||||
.filter((e) => e.isShow)
|
||||
.map((e) => {
|
||||
const item = (e: Menu.Item) => {
|
||||
return [
|
||||
<el-icon>
|
||||
<cl-svg name={e.icon} />
|
||||
</el-icon>,
|
||||
<span v-show={!app.isFold || index != 1}>{e.meta?.label}</span>
|
||||
];
|
||||
};
|
||||
|
||||
if (e.type == 0) {
|
||||
return h(
|
||||
<el-sub-menu />,
|
||||
{
|
||||
index: String(e.id),
|
||||
key: e.id,
|
||||
popperClass: "app-slider__menu"
|
||||
},
|
||||
{
|
||||
title() {
|
||||
return item(e);
|
||||
},
|
||||
default() {
|
||||
return deep(e.children || [], index + 1);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return h(
|
||||
<el-menu-item />,
|
||||
{
|
||||
index:
|
||||
route.path == "/"
|
||||
? e.meta?.isHome
|
||||
? "/"
|
||||
: e.path
|
||||
: e.path,
|
||||
key: e.id
|
||||
},
|
||||
{
|
||||
default() {
|
||||
return item(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return deep(menu.list, 1);
|
||||
}
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div class="app-slider__menu">
|
||||
<el-menu
|
||||
default-active={route.path}
|
||||
background-color="transparent"
|
||||
collapse-transition={false}
|
||||
collapse={browser.isMini ? false : app.isFold}
|
||||
onSelect={toView}
|
||||
>
|
||||
{renderMenu()}
|
||||
</el-menu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
278
src/modules/base/pages/main/components/process.vue
Normal file
@ -0,0 +1,278 @@
|
||||
<template>
|
||||
<div class="app-process">
|
||||
<ul class="app-process__op">
|
||||
<li class="item" @click="toBack">
|
||||
<i class="cl-iconfont cl-icon-back"></i>
|
||||
</li>
|
||||
<li class="item" @click="toRefresh">
|
||||
<i class="cl-iconfont cl-icon-refresh"></i>
|
||||
</li>
|
||||
<li class="item" @click="toHome">
|
||||
<i class="cl-iconfont cl-icon-home"></i>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="app-process__container">
|
||||
<el-scrollbar :ref="setRefs('scroller')" class="app-process__scroller">
|
||||
<div
|
||||
v-for="(item, index) in process.list"
|
||||
:key="index"
|
||||
:ref="setRefs(`item-${index}`)"
|
||||
class="app-process__item"
|
||||
:class="{ active: item.active }"
|
||||
:data-index="index"
|
||||
@click="onTap(item, Number(index))"
|
||||
@contextmenu.stop.prevent="openCM($event, item)"
|
||||
>
|
||||
<span>{{ item.meta?.label || item.name || item.path }}</span>
|
||||
<el-icon @mousedown.stop="onDel(Number(index))">
|
||||
<close-bold />
|
||||
</el-icon>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="app-process" setup>
|
||||
import { onMounted, watch } from "vue";
|
||||
import { last } from "lodash-es";
|
||||
import { useCool } from "/@/cool";
|
||||
import { CloseBold } from "@element-plus/icons-vue";
|
||||
import { ContextMenu } from "@cool-vue/crud";
|
||||
import { useBase, Process } from "/$/base";
|
||||
|
||||
const { refs, setRefs, route, router, mitt } = useCool();
|
||||
const { process } = useBase();
|
||||
|
||||
// 刷新当前路由
|
||||
function toRefresh() {
|
||||
mitt.emit("view.refresh");
|
||||
}
|
||||
|
||||
// 回首页
|
||||
function toHome() {
|
||||
router.push("/");
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
function toBack() {
|
||||
router.back();
|
||||
}
|
||||
|
||||
// 跳转
|
||||
function toPath() {
|
||||
const d = process.list.find((e) => e.active);
|
||||
|
||||
if (!d) {
|
||||
const next = last(process.list);
|
||||
router.push(next ? next.fullPath : "/");
|
||||
}
|
||||
}
|
||||
|
||||
// 移动到
|
||||
function scrollTo(left: number) {
|
||||
refs.scroller.wrapRef.scrollTo({
|
||||
left,
|
||||
behavior: "smooth"
|
||||
});
|
||||
}
|
||||
|
||||
// 调整滚动位置
|
||||
function adScroll(index: number) {
|
||||
const el = refs[`item-${index}`];
|
||||
|
||||
if (el) {
|
||||
scrollTo(el.offsetLeft - (refs.scroller.wrapRef.clientWidth + el.clientWidth) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
// 选择
|
||||
function onTap(item: Process.Item, index: number) {
|
||||
adScroll(index);
|
||||
router.push(item.fullPath);
|
||||
}
|
||||
|
||||
// 删除
|
||||
function onDel(index: number) {
|
||||
process.remove(index);
|
||||
toPath();
|
||||
}
|
||||
|
||||
// 右键菜单
|
||||
function openCM(e: any, item: Process.Item) {
|
||||
ContextMenu.open(e, {
|
||||
hover: {
|
||||
target: "app-process__item"
|
||||
},
|
||||
list: [
|
||||
{
|
||||
label: "关闭当前",
|
||||
hidden: item.fullPath !== route.path,
|
||||
callback(done) {
|
||||
onDel(process.list.findIndex((e) => e.fullPath == item.fullPath));
|
||||
done();
|
||||
toPath();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "关闭其他",
|
||||
callback(done) {
|
||||
process.set(process.list.filter((e) => e.fullPath == item.fullPath));
|
||||
done();
|
||||
toPath();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "关闭所有",
|
||||
callback(done) {
|
||||
process.clear();
|
||||
done();
|
||||
toPath();
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
function (val) {
|
||||
adScroll(process.list.findIndex((e) => e.fullPath === val) || 0);
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
// 添加滚轮事件监听器
|
||||
refs.scroller.wrapRef?.addEventListener("wheel", function (event: WheelEvent) {
|
||||
// 阻止默认滚动行为
|
||||
event.preventDefault();
|
||||
|
||||
// 滚动的速度因子,可以根据需要调整
|
||||
const scrollSpeed = 2;
|
||||
|
||||
// 计算滚动的距离
|
||||
const distance = event.deltaY * scrollSpeed;
|
||||
|
||||
scrollTo(refs.scroller.wrapRef.scrollLeft + distance);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-process {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
position: relative;
|
||||
margin: 0 0 10px 0;
|
||||
padding: 0 10px;
|
||||
user-select: none;
|
||||
|
||||
&__op {
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
height: 30px;
|
||||
border-radius: 4px;
|
||||
margin-right: 10px;
|
||||
list-style: none;
|
||||
|
||||
.item {
|
||||
position: relative;
|
||||
padding: 0 10px;
|
||||
line-height: 30px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
|
||||
&:not(:last-child)::after {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: calc(50% - 5px);
|
||||
height: 10px;
|
||||
width: 1px;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__container {
|
||||
height: 30px;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__scroller {
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
height: 30px;
|
||||
padding: 0 10px;
|
||||
background-color: #fff;
|
||||
font-size: 12px;
|
||||
margin-right: 10px;
|
||||
color: #909399;
|
||||
cursor: pointer;
|
||||
|
||||
.el-icon {
|
||||
font-size: 13px;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
color: #909399;
|
||||
opacity: 0;
|
||||
|
||||
&:hover {
|
||||
color: #f56c6c !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:not(.active) {
|
||||
background-color: #eee;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--color-primary);
|
||||
|
||||
span {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
.el-icon {
|
||||
opacity: 1;
|
||||
width: 13px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
72
src/modules/base/pages/main/components/route-nav.vue
Normal file
@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="route-nav">
|
||||
<p v-if="browser.isMini" class="route-nav__title">
|
||||
{{ lastName }}
|
||||
</p>
|
||||
|
||||
<template v-else>
|
||||
<el-breadcrumb :separator-icon="ArrowRight">
|
||||
<el-breadcrumb-item v-for="(item, index) in list" :key="index">
|
||||
{{ item.meta?.label || item.name }}
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="route-nav" setup>
|
||||
import { computed } from "vue";
|
||||
import { flattenDeep, last } from "lodash-es";
|
||||
import { ArrowRight } from "@element-plus/icons-vue";
|
||||
import { useCool } from "/@/cool";
|
||||
import { useBase } from "/$/base";
|
||||
|
||||
const { route, browser } = useCool();
|
||||
const { menu } = useBase();
|
||||
|
||||
// 数据列表
|
||||
const list = computed(() => {
|
||||
function deep(item: any) {
|
||||
if (route.path === "/") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item.path == route.path) {
|
||||
return item;
|
||||
} else {
|
||||
if (item.children) {
|
||||
const ret = item.children.map(deep).find(Boolean);
|
||||
|
||||
if (ret) {
|
||||
return [item, ret];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return flattenDeep(menu.group.map(deep).filter(Boolean));
|
||||
});
|
||||
|
||||
// 最后一个节点名称
|
||||
const lastName = computed(() => last(list.value)?.name);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.route-nav {
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
|
||||
:deep(.el-breadcrumb) {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-weight: 500;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
95
src/modules/base/pages/main/components/slider.vue
Normal file
@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="app-slider">
|
||||
<div class="app-slider__logo">
|
||||
<img src="/logo.png" />
|
||||
<span v-if="!app.isFold || browser.isMini">{{ app.info.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="app-slider__container">
|
||||
<el-scrollbar>
|
||||
<b-menu />
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="app-slider" setup>
|
||||
import { useBase } from "/$/base";
|
||||
import { useBrowser } from "/@/cool";
|
||||
import BMenu from "./bmenu";
|
||||
|
||||
const { browser } = useBrowser();
|
||||
const { app } = useBase();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.app-slider {
|
||||
height: 100%;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
background-color: #2f3447;
|
||||
|
||||
&__logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 80px;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
font-size: 26px;
|
||||
margin-left: 10px;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&__container {
|
||||
height: calc(100% - 80px);
|
||||
}
|
||||
|
||||
&__menu {
|
||||
.el-menu {
|
||||
border-right: 0;
|
||||
background-color: transparent;
|
||||
|
||||
.el-menu-item,
|
||||
.el-menu__title,
|
||||
.el-sub-menu__title {
|
||||
height: 50px;
|
||||
color: #fff;
|
||||
|
||||
.cl-svg {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
user-select: none;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
&.is-active,
|
||||
&:hover {
|
||||
background-color: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&--popup {
|
||||
.el-menu-item,
|
||||
.el-menu__title,
|
||||
.el-sub-menu__title {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
186
src/modules/base/pages/main/components/topbar.vue
Normal file
@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<a-menu v-if="app.info.menu.isGroup" />
|
||||
|
||||
<div class="app-topbar">
|
||||
<div
|
||||
class="app-topbar__collapse"
|
||||
:class="{
|
||||
unfold: !app.isFold
|
||||
}"
|
||||
@click="app.fold()"
|
||||
>
|
||||
<i class="cl-iconfont cl-icon-fold"></i>
|
||||
</div>
|
||||
|
||||
<!-- 路由导航 -->
|
||||
<route-nav />
|
||||
|
||||
<div class="flex1"></div>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<ul class="app-topbar__tools">
|
||||
<li v-for="(item, index) in toolbarComponents" :key="index">
|
||||
<component :is="item.component" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<div class="app-topbar__user" v-if="user.info">
|
||||
<el-dropdown trigger="click" hide-on-click @command="onCommand">
|
||||
<span class="el-dropdown-link">
|
||||
<span class="name">{{ user.info.nickName }}</span>
|
||||
<cl-avatar :size="32" :src="user.info.headImg" />
|
||||
</span>
|
||||
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="my">
|
||||
<i class="cl-iconfont cl-icon-user"></i>
|
||||
<span>个人中心</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="exit">
|
||||
<i class="cl-iconfont cl-icon-exit"></i>
|
||||
<span>退出</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="app-topbar" setup>
|
||||
import { computed, markRaw, onMounted, reactive } from "vue";
|
||||
import { isFunction, orderBy } from "lodash-es";
|
||||
import { useBase } from "/$/base";
|
||||
import { module, useCool } from "/@/cool";
|
||||
import RouteNav from "./route-nav.vue";
|
||||
import AMenu from "./amenu.vue";
|
||||
import { ElMessageBox } from "element-plus";
|
||||
|
||||
const { router, service, browser } = useCool();
|
||||
const { user, app } = useBase();
|
||||
|
||||
// 命令事件
|
||||
async function onCommand(name: string) {
|
||||
switch (name) {
|
||||
case "my":
|
||||
router.push("/my/info");
|
||||
break;
|
||||
case "exit":
|
||||
ElMessageBox.confirm("确定退出登录吗?", "提示", {
|
||||
type: "warning"
|
||||
})
|
||||
.then(async () => {
|
||||
await service.base.comm.logout();
|
||||
user.logout();
|
||||
})
|
||||
.catch(() => null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 工具栏
|
||||
const toolbar = reactive({
|
||||
list: [] as any[],
|
||||
|
||||
async init() {
|
||||
const arr = orderBy(module.list.map((e) => e.toolbar).filter(Boolean), "order");
|
||||
|
||||
this.list = await Promise.all(
|
||||
arr.map(async (e) => {
|
||||
if (e) {
|
||||
const c = await (isFunction(e.component) ? e.component() : e.component);
|
||||
|
||||
return {
|
||||
...e,
|
||||
component: markRaw(c.default)
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 工具栏组件
|
||||
const toolbarComponents = computed(() => {
|
||||
return toolbar.list.filter((e) => {
|
||||
if (browser.isMini) {
|
||||
return e?.h5 ?? true;
|
||||
}
|
||||
|
||||
return e?.pc ?? true;
|
||||
});
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
toolbar.init();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
padding: 0 10px;
|
||||
background-color: #fff;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&__collapse {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
cursor: pointer;
|
||||
transform: rotateY(180deg);
|
||||
|
||||
&.unfold {
|
||||
transform: rotateY(0);
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.flex1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__tools {
|
||||
display: flex;
|
||||
margin-right: 20px;
|
||||
|
||||
& > li {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
height: 45px;
|
||||
padding: 0 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__user {
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
.el-dropdown-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
white-space: nowrap;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.el-icon-arrow-down {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
95
src/modules/base/pages/main/components/views.vue
Normal file
@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="app-views">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition :name="app.info.router.transition || 'none'">
|
||||
<keep-alive :include="caches" :key="key">
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="app-views" setup>
|
||||
import { computed, onMounted, onUnmounted, ref } from "vue";
|
||||
import { useBase } from "/$/base";
|
||||
import { useCool } from "/@/cool";
|
||||
|
||||
const { mitt } = useCool();
|
||||
const { process, app } = useBase();
|
||||
|
||||
// 缓存数
|
||||
const key = ref(1);
|
||||
|
||||
// 缓存列表
|
||||
const caches = computed(() => {
|
||||
return process.list
|
||||
.filter((e) => e.meta?.keepAlive)
|
||||
.map((e) => {
|
||||
return e.path.substring(1, e.path.length).replace(/\//g, "-");
|
||||
});
|
||||
});
|
||||
|
||||
// 刷新页面
|
||||
function refresh() {
|
||||
key.value += 1;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
mitt.on("view.refresh", refresh);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
mitt.off("view.refresh");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-views {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
margin: 0 10px 10px 10px;
|
||||
width: calc(100% - 20px);
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
|
||||
.none-enter-active {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.slide-enter-active {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
transition: all 0.4s ease-in-out 0.2s;
|
||||
}
|
||||
|
||||
.slide-leave-active {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
transition: all 0.4s ease-in-out;
|
||||
}
|
||||
|
||||
.slide-enter-to {
|
||||
transform: translate3d(0, 0, 0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.slide-enter-from {
|
||||
transform: translate3d(-5%, 0, 0);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-leave-to {
|
||||
transform: translate3d(5%, 0, 0);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-leave-from {
|
||||
transform: translate3d(0, 0, 0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
105
src/modules/base/pages/main/index.vue
Normal file
@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div class="app-layout" :class="{ collapse: app.isFold }">
|
||||
<div class="app-layout__mask" @click="app.fold(true)"></div>
|
||||
|
||||
<div class="app-layout__left">
|
||||
<slider />
|
||||
</div>
|
||||
|
||||
<div class="app-layout__right">
|
||||
<topbar />
|
||||
<process />
|
||||
<views />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="app-layout" setup>
|
||||
import Topbar from "./components/topbar.vue";
|
||||
import Slider from "./components/slider.vue";
|
||||
import process from "./components/process.vue";
|
||||
import Views from "./components/views.vue";
|
||||
import { useBase } from "/$/base";
|
||||
|
||||
const { app } = useBase();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-layout {
|
||||
display: flex;
|
||||
background-color: #f7f7f7;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
&__left {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 255px;
|
||||
transition: left 0.2s;
|
||||
}
|
||||
|
||||
&__right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: calc(100% - 255px);
|
||||
}
|
||||
|
||||
&__mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.app-layout__left {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
z-index: 9999;
|
||||
transition:
|
||||
transform 0.3s cubic-bezier(0.7, 0.3, 0.1, 1),
|
||||
box-shadow 0.3s cubic-bezier(0.7, 0.3, 0.1, 1);
|
||||
}
|
||||
|
||||
.app-layout__right {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.collapse {
|
||||
.app-layout__left {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.app-layout__mask {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 768px) {
|
||||
.app-layout__left,
|
||||
.app-layout__right {
|
||||
transition: width 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.app-layout__mask {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.collapse {
|
||||
.app-layout__left {
|
||||
width: 64px;
|
||||
}
|
||||
|
||||
.app-layout__right {
|
||||
width: calc(100% - 64px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
29
src/modules/base/static/css/animation.scss
Normal file
@ -0,0 +1,29 @@
|
||||
@keyframes dou {
|
||||
0% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
11% {
|
||||
transform: rotate(7.61deg);
|
||||
}
|
||||
23% {
|
||||
transform: rotate(-5.8deg);
|
||||
}
|
||||
36% {
|
||||
transform: rotate(3.35deg);
|
||||
}
|
||||
49% {
|
||||
transform: rotate(-1.9deg);
|
||||
}
|
||||
62% {
|
||||
transform: rotate(1.12deg);
|
||||
}
|
||||
75% {
|
||||
transform: rotate(-0.64deg);
|
||||
}
|
||||
88% {
|
||||
transform: rotate(0.37deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(-0.28deg);
|
||||
}
|
||||
}
|
26
src/modules/base/static/css/index.scss
Normal file
@ -0,0 +1,26 @@
|
||||
#app {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:root {
|
||||
--view-bg-color: #f7f7f7;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input {
|
||||
&:-webkit-autofill {
|
||||
box-shadow: 0 0 0px 1000px white inset;
|
||||
}
|
||||
}
|
||||
|
||||
@import "./animation.scss";
|
1
src/modules/base/static/svg/icon-activity.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1614441341007" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11418" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M848.896 132.192c-10.048-5.664-22.4-5.408-32.352 0.544-0.448 0.288-45.664 27.328-98.688 27.328-53.184 0-99.776-27.232-100.16-27.424a29.488 29.488 0 0 0-3.456-1.792c-3.2-1.408-79.008-34.752-149.696-34.752-70.08 0-151.936 32.96-155.36 34.368-12.032 4.896-19.936 16.64-19.936 29.632v416a32.041 32.041 0 0 0 14.24 26.624c8.928 5.984 20.192 7.04 30.08 2.912 19.68-8.224 80.992-29.536 127.68-29.536 51.52 0 115.776 24.768 126.208 28.928 11.712 6.304 68.544 35.072 133.088 35.072 72.032 0 127.776-35.616 130.08-37.152 9.088-5.92 14.592-16 14.592-26.848v-416c0.032-11.584-6.24-22.208-16.32-27.904z m-271.68 763.744H224.768V128.064c0-17.664-14.336-32-32-32s-32 14.336-32 32v768c0 0.064 0.032 0.096 0.032 0.16-16.96 0.8-30.56 14.528-30.56 31.712 0 17.696 14.336 32 32 32h414.976c17.696 0 32-14.304 32-32s-14.304-32-32-32z" p-id="11419"></path></svg>
|
After Width: | Height: | Size: 1.2 KiB |
9
src/modules/base/static/svg/icon-amount.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg t="1676622023768" class="icon" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1470" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="64" height="64">
|
||||
<path
|
||||
d="M373.9996544 320.53819093l298.27051947 0c20.11542827 0 39.48256427-8.5139072 52.3939168-23.95154773 30.781264-36.58213013 59.972352-81.49120213 66.52151146-121.72205867 13.19190933-80.5555168-21.33188373-121.44150293-121.44150293-121.44150293s-66.2407424 65.67941653-105.16197973 71.1059232c-29.003696 4.02308587-27.9746336-34.24302293-73.53840747-71.01233387-32.74622507-26.2904416-69.42173227 47.99647147-112.6468256 48.464208-50.42895573 0.65491627-121.34791253-13.28549867-121.34791253 72.8834912 0 34.80456213 30.5007072 81.86534827 63.80825813 120.87996374C334.2365344 311.36915413 353.50986667 320.53819093 373.9996544 320.53819093zM746.08935147 397.25758827c-15.90516267-18.05709013-39.0146144-28.16181333-63.05975254-28.16181334L364.7372416 369.09577493c-23.0158624 0-45.18962773 9.26241387-61.09500373 25.91629547C203.62599467 499.79990507 111.3755808 649.40300693 111.3755808 758.4944832c0 96.18012373 96.6478592 217.6216256 215.84384427 217.6216256l369.5636224 0c119.19598507 0 215.84384427-120.4122272 215.84384426-217.6216256C912.62710507 647.4382592 838.43356907 502.04542187 746.08935147 397.25758827zM648.8801664 529.645696l-63.99522453 116.10858453c-5.61368747 10.1980992 1.777568 22.73509227 13.37908906 22.73509227l18.15068054 0c8.4205312 0 15.3438368 6.8299296 15.3438368 15.3438368l0 0c0 8.4205312-6.8299296 15.3438368-15.3438368 15.3438368l-45.6575776 0c-8.88826667 0-15.99875307 7.11069867-16.0923424 15.99875307l0 7.5784352c0 8.4205312 6.8299296 15.3438368 15.3438368 15.3438368l46.40586986 0c8.4205312 0 15.3438368 6.8299296 15.3438368 15.3438368s-6.8299296 15.3438368-15.3438368 15.3438368l-46.40586986 0c-8.4205312 0-15.3438368 6.8299296-15.3438368 15.3438368l0 39.0146144c0 8.4205312-6.8299296 15.3438368-15.3438368 15.3438368l-27.78745387 0c-8.4205312 0-15.3438368-6.8299296-15.3438368-15.3438368l0-39.0146144c0-8.4205312-6.8299296-15.3438368-15.3438368-15.3438368l-45.9381344 0c-8.4205312 0-15.3438368-6.8299296-15.3438368-15.3438368s6.8299296-15.3438368 15.3438368-15.3438368l45.844544 0c8.51412053 0 15.34405013-6.8299296 15.4374272-15.3438368l0.0935904-7.4848448c0.0935904-8.88826667-7.11069867-16.1859328-16.0923424-16.1859328l-45.28321813 0c-8.4205312 0-15.3438368-6.8299296-15.3438368-15.3438368l0 0c0-8.4205312 6.8299296-15.3438368 15.3438368-15.3438368l17.682944 0c11.6951104 0 18.99277653-12.53699307 13.37908906-22.73509227l-63.99543786-116.10837227c-5.61368747-10.1980992 1.777568-22.7353056 13.37908906-22.7353056l30.5007072 0c5.80065387 0 11.13378453 3.27458027 13.65985814 8.4205312l52.11314773 103.94552534c5.61368747 11.2271616 21.70602987 11.2271616 27.41330667 0l52.11314773-103.94552534c2.619664-5.1457376 7.858992-8.4205312 13.65985813-8.4205312l30.5007072 0C647.10238507 507.0039808 654.4938528 519.4475968 648.8801664 529.645696z"
|
||||
p-id="1471"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
1
src/modules/base/static/svg/icon-app.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1614441249036" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4552" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M364.8 590.62857143H182.85714286c-60.34285714 0-109.71428571-49.37142857-109.71428572-109.71428572V182.85714286c0-60.34285714 49.37142857-109.71428571 109.71428572-109.71428572h181.94285714c60.34285714 0 109.71428571 49.37142857 109.71428571 109.71428572v298.05714285c0 60.34285714-49.37142857 109.71428571-109.71428571 109.71428572zM841.14285714 358.4H659.2c-60.34285714 0-109.71428571-49.37142857-109.71428571-109.71428571V182.85714286c0-60.34285714 49.37142857-109.71428571 109.71428571-109.71428572H841.14285714c60.34285714 0 109.71428571 49.37142857 109.71428572 109.71428572v65.82857143c0 60.34285714-49.37142857 109.71428571-109.71428572 109.71428571zM364.8 950.85714286H182.85714286c-60.34285714 0-109.71428571-49.37142857-109.71428572-109.71428572v-65.82857143c0-60.34285714 49.37142857-109.71428571 109.71428572-109.71428571h181.94285714c60.34285714 0 109.71428571 49.37142857 109.71428571 109.71428571v65.82857143c0 60.34285714-49.37142857 109.71428571-109.71428571 109.71428572z m476.34285714 0H659.2c-60.34285714 0-109.71428571-49.37142857-109.71428571-109.71428572V543.08571429c0-60.34285714 49.37142857-109.71428571 109.71428571-109.71428572H841.14285714c60.34285714 0 109.71428571 49.37142857 109.71428572 109.71428572V841.14285714c0 60.34285714-49.37142857 109.71428571-109.71428572 109.71428572z" p-id="4553"></path></svg>
|
After Width: | Height: | Size: 1.7 KiB |
1
src/modules/base/static/svg/icon-approve.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1614441250987" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4692" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M825.42250667 584.80298667H628.39239111l-29.53557333-99.30865778c74.05568-33.86026667 125.62545778-108.54172445 125.62545777-195.44291556 0-118.69297778-95.98862222-214.90005333-214.37553777-214.90005333-118.40170667 0-214.37553778 96.22186667-214.37553778 214.90005333 0 86.91484445 51.56977778 161.59744 125.62545778 195.44291556l-29.53557333 99.30865778h-197.02897778a70.59000889 70.59000889 0 0 0-70.59000889 70.59000888v131.0572089c0 17.31697778 14.03904 31.34122667 31.34008889 31.34122666h709.05628444a31.36967111 31.36967111 0 0 0 31.36967111-31.37080889V655.36341333a70.54563555 70.54563555 0 0 0-70.54563555-70.56042666z m48.71509333 291.24266666h-728.07537778a21.84533333 21.84533333 0 0 0-21.84533333 21.84533334v29.12711111a21.84533333 21.84533333 0 0 0 21.84533333 21.84533333h728.09016889a21.84533333 21.84533333 0 0 0 21.84533334-21.84533333v-29.12711111a21.84533333 21.84533333 0 0 0-21.86012445-21.84533334z" p-id="4693"></path></svg>
|
After Width: | Height: | Size: 1.3 KiB |
1
src/modules/base/static/svg/icon-auth.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1614441301181" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8337" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M864.35351563 176.78515625c-22.93945313 3.95507813-46.40625 6.06445313-70.48828126 6.06445313-101.42578125 0-194.32617188-36.82617188-265.95703124-97.73437501-9.31640625-7.91015625-23.02734375-7.91015625-32.34375001 0-71.63085938 60.99609375-164.53125 97.734375-265.95703124 97.734375-23.73046875 0-47.02148438-2.02148438-69.60937501-5.88867187-15.1171875-2.54882813-28.91601563 9.140625-29.00390625 24.52148437-0.17578125 95.88867188-0.3515625 209.26757813-0.3515625 213.13476563 0 413.96484375 346.72851563 520.92773438 378.80859376 529.98046875 1.58203125 0.43945313 3.1640625 0.43945313 4.74609374 0C546.18945313 935.6328125 890.80859375 829.28515625 893.00585938 418.57226562l0.35156249-217.08984374c0-15.46875-13.88671875-27.33398438-29.00390625-24.69726563zM541.8828125 562.00976563v135.79101562c0 16.5234375-13.359375 29.8828125-29.8828125 29.8828125s-29.8828125-13.359375-29.8828125-29.8828125V562.00976563c-59.94140625-13.62304688-104.67773438-67.1484375-104.67773438-131.22070313 0-74.26757813 60.20507813-134.56054688 134.56054688-134.56054688S646.56054688 356.43359375 646.56054688 430.7890625c0 64.07226563-44.73632813 117.68554688-104.67773438 131.22070313z" p-id="8338"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
1
src/modules/base/static/svg/icon-ban.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1614441322440" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10017" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M509.27 62.598c-248.668 0-450.254 199.753-450.254 446.161s201.586 446.162 450.255 446.162 450.255-199.754 450.255-446.162S757.939 62.598 509.27 62.598z m0 148.72c55.666 0 107.78 15.033 152.468 41.2L250.676 659.842c-26.404-44.281-41.575-95.922-41.575-151.084 0-164.27 134.386-297.441 300.17-297.441z m0 594.883c-55.666 0-107.78-15.033-152.469-41.198l411.062-407.326C794.27 401.96 809.44 453.6 809.44 508.76c0 164.277-134.394 297.442-300.17 297.442z" p-id="10018"></path></svg>
|
After Width: | Height: | Size: 853 B |
1
src/modules/base/static/svg/icon-camera.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1614441302655" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8477" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M284.22599111 249.97250845c11.41782755-31.32742163 22.64147437-62.24099555 33.94279348-93.11573334 5.90309452-16.13035141 16.45317689-23.63907792 33.554432-23.65121422 106.48871822-0.05218608 212.97743645-0.05218608 319.46615466 0 17.70928355 0.0121363 28.54456889 7.85703822 34.61635793 24.69979022 10.16172089 28.2338797 20.42781392 56.42892325 30.26549571 84.76596148 1.99399348 5.76109985 4.88121837 7.46989037 10.84863526 7.43105423 45.03779555-0.25971675 90.06102755-0.1432083 135.09760948-0.1432083 46.33152475 0 84.05477452 36.93317689 84.11909689 83.25256533 0.19418075 158.4126483 0.44054755 316.81194667-0.12864474 475.22459497-0.19418075 53.15333689-45.21862637 83.49771852-83.951616 83.43339615-247.67025303-0.46603378-495.35506963-0.23301689-743.02532267-0.23301689-43.83387497 0-81.34959408-37.54241897-81.36173036-81.46610253-0.02669985-159.4612243-0.01334992-318.93458489 0-478.39580918 0-44.29990875 37.55455525-81.7767917 81.90543643-81.80227792 47.92380682-0.0121363 95.85974992 0 144.65130193 0z m395.04129897 308.73524147c0.75123675-92.05502103-75.58849422-164.60094578-163.6555283-166.58280295-93.00043852-2.09593837-171.21522725 72.74131911-171.65577481 165.14465185-0.42719763 88.27456475 69.05067141 167.64351525 167.87531851 167.43598459 100.13293985-0.21966697 167.90201837-80.54617125 167.4359846-165.99904711z" p-id="8478"></path></svg>
|
After Width: | Height: | Size: 1.7 KiB |
1
src/modules/base/static/svg/icon-card.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1614441255776" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4972" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M891.733 170.667H132.267c-38.4 0-68.267 29.866-68.267 68.266v102.4h896v-102.4c0-38.4-29.867-68.266-68.267-68.266z m0 704c38.4 0 68.267-29.867 68.267-68.267V426.667H64V806.4c0 38.4 29.867 68.267 68.267 68.267h759.466zM192 554.667h253.867c19.2 0 32 12.8 32 32s-12.8 32-32 32H187.733c-17.066-2.134-27.733-14.934-27.733-32 0-19.2 12.8-32 32-32z m0 128h128c19.2 0 32 12.8 32 32s-12.8 32-32 32H187.733c-17.066-2.134-27.733-14.934-27.733-32 0-19.2 12.8-32 32-32z" p-id="4973"></path></svg>
|
After Width: | Height: | Size: 859 B |
1
src/modules/base/static/svg/icon-cart.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1614441257585" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5112" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M259.2 896a64 64 0 1 0 128 0 64 64 0 1 0-128 0zM768 896a64 64 0 1 0 128 0 64 64 0 1 0-128 0zM940.8 284.8c-16-19.2-38.4-28.8-64-28.8H249.6l-22.4-112c-6.4-44.8-44.8-80-92.8-80H96c-19.2 0-32 16-32 32s12.8 32 32 32h38.4c12.8 0 25.6 9.6 28.8 25.6L192 310.4l51.2 371.2c6.4 48 44.8 86.4 89.6 86.4h486.4c44.8 0 83.2-32 89.6-73.6L960 355.2c3.2-25.6-3.2-51.2-19.2-70.4z" p-id="5113"></path></svg>
|
After Width: | Height: | Size: 763 B |
1
src/modules/base/static/svg/icon-command.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1614441314533" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9317" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M478.176 454.848c-17.12 0-33.184 6.656-45.248 18.72-24.928 24.96-24.928 65.568 0 90.592 24.128 24.096 66.304 24.064 90.496-0.032 12.096-12.096 18.752-28.16 18.752-45.28s-6.656-33.184-18.752-45.28c-12.064-12.096-28.096-18.72-45.248-18.72zM512 63.968c-247.04 0-448 172.256-448 384 0 116.48 63.008 226.048 170.592 298.944l87.328 71.52c-3.904 19.328-17.408 60.096-32.64 97.536-4.928 12.096-1.984 25.984 7.36 35.072a32.049 32.049 0 0 0 22.272 8.992c4.384 0 8.8-0.896 12.992-2.752 36.416-16.224 147.488-68.96 187.584-125.376C763.104 828.448 960 657.568 960 447.968c0-211.744-200.96-384-448-384z m214.624 334.656c12.512 12.512 12.512 32.736 0 45.248-6.24 6.24-14.432 9.376-22.624 9.376s-16.384-3.136-22.624-9.376l-39.84-39.84-52.288 52.288c10.688 18.944 16.896 40.128 16.928 62.496 0.032 34.24-13.312 66.4-37.504 90.592-24.16 24.16-56.288 37.472-90.496 37.472-34.176 0-66.336-13.312-90.496-37.472-49.888-49.984-49.888-131.2 0-181.056 24.16-24.16 56.288-37.472 90.496-37.472 24.032 0 46.912 6.912 66.848 19.2l136.352-136.352c12.512-12.512 32.736-12.512 45.248 0s12.512 32.736 0 45.248l-39.84 39.84 39.84 39.808z" p-id="9318"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |