feat: 新建web

master
JINGYJ
commit 7991f8fc60

@ -0,0 +1,21 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.eslintcache
report.html
yarn.lock
npm-debug.log*
.pnpm-error.log*
.pnpm-debug.log
tests/**/coverage/
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
tsconfig.tsbuildinfo

@ -0,0 +1,14 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

@ -0,0 +1,5 @@
# 平台本地运行端口号
VITE_PORT = 8848
# 是否隐藏首页 隐藏 true 不隐藏 false 勿删除VITE_HIDE_HOME只需在.env文件配置
VITE_HIDE_HOME = false

@ -0,0 +1,11 @@
# 平台本地运行端口号
VITE_PORT = 8848
# 开发环境读取配置文件路径
VITE_PUBLIC_PATH = /
# 开发环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY = "hash"
# 开发环境后端地址
VITE_APP_BASE_URL = 'http://192.168.10.13:8000'

@ -0,0 +1,13 @@
# 线上环境平台打包路径
VITE_PUBLIC_PATH = /
# 线上环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY = "hash"
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
VITE_CDN = false
# 是否启用gzip压缩或brotli压缩分两种情况删除原始文件和不删除原始文件
# 压缩时不删除原始文件的配置gzip、brotli、both同时开启 gzip 与 brotli 压缩、none不开启压缩默认
# 压缩时删除原始文件的配置gzip-clear、brotli-clear、both-clear同时开启 gzip 与 brotli 压缩、none不开启压缩默认
VITE_COMPRESSION = "none"

@ -0,0 +1,16 @@
# 预发布也需要生产环境的行为
# https://cn.vitejs.dev/guide/env-and-mode.html#modes
# NODE_ENV = development
VITE_PUBLIC_PATH = /
# 预发布环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY = "hash"
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
VITE_CDN = true
# 是否启用gzip压缩或brotli压缩分两种情况删除原始文件和不删除原始文件
# 压缩时不删除原始文件的配置gzip、brotli、both同时开启 gzip 与 brotli 压缩、none不开启压缩默认
# 压缩时删除原始文件的配置gzip-clear、brotli-clear、both-clear同时开启 gzip 与 brotli 压缩、none不开启压缩默认
VITE_COMPRESSION = "none"

@ -0,0 +1,11 @@
public
dist
*.d.ts
/src/assets
package.json
.eslintrc.js
.prettierrc.js
commitlint.config.js
postcss.config.js
tailwind.config.js
stylelint.config.js

@ -0,0 +1,120 @@
module.exports = {
root: true,
env: {
node: true
},
globals: {
// Ref sugar (take 2)
$: "readonly",
$$: "readonly",
$ref: "readonly",
$shallowRef: "readonly",
$computed: "readonly",
// index.d.ts
// global.d.ts
Fn: "readonly",
PromiseFn: "readonly",
RefType: "readonly",
LabelValueOptions: "readonly",
EmitType: "readonly",
TargetContext: "readonly",
ComponentElRef: "readonly",
ComponentRef: "readonly",
ElRef: "readonly",
global: "readonly",
ForDataType: "readonly",
ComponentRoutes: "readonly",
// script setup
defineProps: "readonly",
defineEmits: "readonly",
defineExpose: "readonly",
withDefaults: "readonly"
},
extends: [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/typescript/recommended",
"@vue/prettier",
"@vue/eslint-config-typescript"
],
parser: "vue-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser",
ecmaVersion: 2020,
sourceType: "module",
jsxPragma: "React",
ecmaFeatures: {
jsx: true
}
},
overrides: [
{
files: ["*.ts", "*.vue"],
rules: {
"no-undef": "off"
}
},
{
files: ["*.vue"],
parser: "vue-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser",
extraFileExtensions: [".vue"],
ecmaVersion: "latest",
ecmaFeatures: {
jsx: true
}
},
rules: {
"no-undef": "off"
}
}
],
rules: {
"vue/no-v-html": "off",
"vue/require-default-prop": "off",
"vue/require-explicit-emits": "off",
"vue/multi-word-component-names": "off",
"@typescript-eslint/no-explicit-any": "off", // any
"no-debugger": "off",
"@typescript-eslint/explicit-module-boundary-types": "off", // setup()
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"vue/html-self-closing": [
"error",
{
html: {
void: "always",
normal: "always",
component: "always"
},
svg: "always",
math: "always"
}
],
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_"
}
],
"no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_"
}
],
"prettier/prettier": [
"error",
{
endOfLine: "auto"
}
]
}
};

21
.gitignore vendored

@ -0,0 +1,21 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.eslintcache
report.html
yarn.lock
npm-debug.log*
.pnpm-error.log*
.pnpm-debug.log
tests/**/coverage/
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
tsconfig.tsbuildinfo

@ -0,0 +1,6 @@
#!/bin/sh
# shellcheck source=./_/husky.sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --edit "$1"

@ -0,0 +1,9 @@
#!/bin/sh
command_exists () {
command -v "$1" >/dev/null 2>&1
}
# Workaround for Windows 10, Git Bash and Pnpm
if command_exists winpty && test -t 1; then
exec < /dev/tty
fi

@ -0,0 +1,8 @@
module.exports = {
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
"{!(package)*.json}": ["prettier --write--parser json"],
"package.json": ["prettier --write"],
"*.vue": ["eslint --fix", "prettier --write", "stylelint --fix"],
"*.{vue,css,scss,postcss,less}": ["stylelint --fix", "prettier --write"],
"*.md": ["prettier --write"]
};

@ -0,0 +1,10 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
. "$(dirname "$0")/common.sh"
[ -n "$CI" ] && exit 0
# Format and submit code according to lintstagedrc.js configuration
npm run lint:lint-staged
npm run lint:pretty

@ -0,0 +1,11 @@
{
"default": true,
"MD003": false,
"MD033": false,
"MD013": false,
"MD001": false,
"MD025": false,
"MD024": false,
"MD007": { "indent": 4 },
"no-hard-tabs": false
}

@ -0,0 +1,3 @@
shamefully-hoist=true
strict-peer-dependencies=false
shell-emulator=true

@ -0,0 +1,6 @@
module.exports = {
bracketSpacing: true,
singleQuote: false,
arrowParens: "avoid",
trailingComma: "none"
};

@ -0,0 +1,4 @@
/dist/*
/public/*
public/*
src/style/reset.scss

@ -0,0 +1,18 @@
{
"recommendations": [
"christian-kohler.path-intellisense",
"vscode-icons-team.vscode-icons",
"davidanson.vscode-markdownlint",
"ms-azuretools.vscode-docker",
"stylelint.vscode-stylelint",
"bradlc.vscode-tailwindcss",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"redhat.vscode-yaml",
"csstools.postcss",
"mikestead.dotenv",
"eamodio.gitlens",
"antfu.iconify",
"Vue.volar"
]
}

@ -0,0 +1,31 @@
{
"editor.formatOnType": true,
"editor.formatOnSave": true,
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.tabSize": 2,
"editor.formatOnPaste": true,
"editor.guides.bracketPairs": "active",
"files.autoSave": "afterDelay",
"git.confirmSync": false,
"workbench.startupEditor": "newUntitledFile",
"editor.suggestSelection": "first",
"editor.acceptSuggestionOnCommitCharacter": false,
"css.lint.propertyIgnoredDueToDisplay": "ignore",
"editor.quickSuggestions": {
"other": true,
"comments": true,
"strings": true
},
"files.associations": {
"editor.snippetSuggestions": "top"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"iconify.excludes": ["el"]
}

@ -0,0 +1,22 @@
{
"Vue3.0快速生成模板": {
"scope": "vue",
"prefix": "Vue3.0",
"body": [
"<template>",
"\t<div>test</div>",
"</template>\n",
"<script lang='ts'>",
"export default {",
"\tsetup() {",
"\t\treturn {}",
"\t}",
"}",
"</script>\n",
"<style lang='scss' scoped>\n",
"</style>",
"$2"
],
"description": "Vue3.0"
}
}

@ -0,0 +1,17 @@
{
"Vue3.2+快速生成模板": {
"scope": "vue",
"prefix": "Vue3.2+",
"body": [
"<script setup lang='ts'>",
"</script>\n",
"<template>",
"\t<div>test</div>",
"</template>\n",
"<style lang='scss' scoped>\n",
"</style>",
"$2"
],
"description": "Vue3.2+"
}
}

@ -0,0 +1,20 @@
{
"Vue3.3+defineOptions快速生成模板": {
"scope": "vue",
"prefix": "Vue3.3+",
"body": [
"<script setup lang='ts'>",
"defineOptions({",
"\tname: ''",
"})",
"</script>\n",
"<template>",
"\t<div>test</div>",
"</template>\n",
"<style lang='scss' scoped>\n",
"</style>",
"$2"
],
"description": "Vue3.3+defineOptions快速生成模板"
}
}

@ -0,0 +1,20 @@
FROM node:16-alpine as build-stage
WORKDIR /app
RUN corepack enable
RUN corepack prepare pnpm@7.32.1 --activate
RUN npm config set registry https://registry.npmmirror.com
COPY .npmrc package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-present, pure-admin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,58 @@
<h1>tp-admin</h1>
## 安装使用
- 获取项目代码
```bash
git clone http://192.168.10.28:3000/Yaxin/Tp_Web2.0.git
```
- 安装依赖
```bash
cd Tp_Web2.0
pnpm install
```
- 运行
```bash
pnpm serve
```
- 打包
```bash
pnpm build
```
## `Git` 贡献提交规范
- 参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
- `feat` 增加新功能
- `fix` 修复问题/BUG
- `style` 代码风格相关无影响运行结果的
- `perf` 优化/性能提升
- `refactor` 重构
- `revert` 撤销修改
- `test` 测试相关
- `docs` 文档/注释
- `chore` 依赖更新/脚手架配置修改等
- `workflow` 工作流改进
- `ci` 持续集成
- `types` 类型定义文件更改
- `wip` 开发中
## 浏览器支持
本地开发推荐使用 `Chrome 80+` 浏览器
支持现代浏览器, 不支持 `IE`
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |

@ -0,0 +1,56 @@
import { Plugin as importToCDN } from "vite-plugin-cdn-import";
/**
* @description `cdn`使cdn .env.production VITE_CDN true
* cdnhttps://www.bootcdn.cn当然你也可以选择 https://unpkg.com 或者 https://www.jsdelivr.com
* mockjscdnmockjs使
* 使jscsscdn
*/
export const cdn = importToCDN({
//prodUrl解释 name: 对应下面modules的nameversion: 自动读取本地package.json中dependencies依赖中对应包的版本号path: 对应下面modules的path当然也可写完整路径会替换prodUrl
prodUrl: "https://cdn.bootcdn.net/ajax/libs/{name}/{version}/{path}",
modules: [
{
name: "vue",
var: "Vue",
path: "vue.global.prod.min.js"
},
{
name: "vue-router",
var: "VueRouter",
path: "vue-router.global.min.js"
},
// 项目中没有直接安装vue-demi但是pinia用到了所以需要在引入pinia前引入vue-demihttps://github.com/vuejs/pinia/blob/v2/packages/pinia/package.json#L77
{
name: "vue-demi",
var: "VueDemi",
path: "index.iife.min.js"
},
{
name: "pinia",
var: "Pinia",
path: "pinia.iife.min.js"
},
{
name: "element-plus",
var: "ElementPlus",
path: "index.full.min.js",
css: "index.min.css"
},
{
name: "axios",
var: "axios",
path: "axios.min.js"
},
{
name: "dayjs",
var: "dayjs",
path: "dayjs.min.js"
},
{
name: "echarts",
var: "echarts",
path: "echarts.min.js"
}
]
});

@ -0,0 +1,63 @@
import type { Plugin } from "vite";
import { isArray } from "@pureadmin/utils";
import compressPlugin from "vite-plugin-compression";
export const configCompressPlugin = (
compress: ViteCompression
): Plugin | Plugin[] => {
if (compress === "none") return null;
const gz = {
// 生成的压缩包后缀
ext: ".gz",
// 体积大于threshold才会被压缩
threshold: 0,
// 默认压缩.js|mjs|json|css|html后缀文件设置成true压缩全部文件
filter: () => true,
// 压缩后是否删除原始文件
deleteOriginFile: false
};
const br = {
ext: ".br",
algorithm: "brotliCompress",
threshold: 0,
filter: () => true,
deleteOriginFile: false
};
const codeList = [
{ k: "gzip", v: gz },
{ k: "brotli", v: br },
{ k: "both", v: [gz, br] }
];
const plugins: Plugin[] = [];
codeList.forEach(item => {
if (compress.includes(item.k)) {
if (compress.includes("clear")) {
if (isArray(item.v)) {
item.v.forEach(vItem => {
plugins.push(
compressPlugin(Object.assign(vItem, { deleteOriginFile: true }))
);
});
} else {
plugins.push(
compressPlugin(Object.assign(item.v, { deleteOriginFile: true }))
);
}
} else {
if (isArray(item.v)) {
item.v.forEach(vItem => {
plugins.push(compressPlugin(vItem));
});
} else {
plugins.push(compressPlugin(item.v));
}
}
}
});
return plugins;
};

@ -0,0 +1,31 @@
/** 处理环境变量 */
const warpperEnv = (envConf: Recordable): ViteEnv => {
/** 此处为默认值 */
const ret: ViteEnv = {
VITE_PORT: 8848,
VITE_PUBLIC_PATH: "",
VITE_ROUTER_HISTORY: "",
VITE_CDN: false,
VITE_HIDE_HOME: "false",
VITE_COMPRESSION: "none"
};
for (const envName of Object.keys(envConf)) {
let realName = envConf[envName].replace(/\\n/g, "\n");
realName =
realName === "true" ? true : realName === "false" ? false : realName;
if (envName === "VITE_PORT") {
realName = Number(realName);
}
ret[envName] = realName;
if (typeof realName === "string") {
process.env[envName] = realName;
} else if (typeof realName === "object") {
process.env[envName] = JSON.stringify(realName);
}
}
return ret;
};
export { warpperEnv };

@ -0,0 +1,45 @@
import type { Plugin } from "vite";
import dayjs, { Dayjs } from "dayjs";
import utils from "@pureadmin/utils";
import duration from "dayjs/plugin/duration";
import { green, blue, bold } from "picocolors";
dayjs.extend(duration);
export function viteBuildInfo(): Plugin {
let config: { command: string };
let startTime: Dayjs;
let endTime: Dayjs;
let outDir: string;
return {
name: "vite:buildInfo",
configResolved(resolvedConfig) {
config = resolvedConfig;
outDir = resolvedConfig.build?.outDir ?? "dist";
},
buildStart() {
console.log(bold(green(`👏欢迎使用${blue("[tp-admin]")}`)));
if (config.command === "build") {
startTime = dayjs(new Date());
}
},
closeBundle() {
if (config.command === "build") {
endTime = dayjs(new Date());
utils.getPackageSize({
folder: outDir,
callback: (size: string) => {
console.log(
bold(
green(
`🎉恭喜打包完成(总用时${dayjs
.duration(endTime.diff(startTime))
.format("mm分ss秒")}${size}`
)
)
);
}
});
}
}
};
}

@ -0,0 +1,30 @@
/**
* `vite.config.ts` `optimizeDeps.include`
* `vite` include esm node_modules/.vite
* includevite 使 node_modules/.vite
* 使 src/main.ts include vite node_modules/.vite
*/
const include = [
"qs",
"mitt",
"dayjs",
"axios",
"pinia",
"js-cookie",
"sortablejs",
"@vueuse/core",
"@pureadmin/utils",
"responsive-storage"
];
/**
*
* `@iconify-icons/` `exclude` 使
*/
const exclude = [
"@iconify-icons/ep",
"@iconify-icons/ri",
"@pureadmin/theme/dist/browser-utils"
];
export { include, exclude };

@ -0,0 +1,56 @@
import { cdn } from "./cdn";
import vue from "@vitejs/plugin-vue";
import { viteBuildInfo } from "./info";
import svgLoader from "vite-svg-loader";
import vueJsx from "@vitejs/plugin-vue-jsx";
import { viteMockServe } from "vite-plugin-mock";
import { configCompressPlugin } from "./compress";
// import ElementPlus from "unplugin-element-plus/vite";
import { visualizer } from "rollup-plugin-visualizer";
import removeConsole from "vite-plugin-remove-console";
import themePreprocessorPlugin from "@pureadmin/theme";
import { genScssMultipleScopeVars } from "../src/layout/theme";
export function getPluginsList(
command: string,
VITE_CDN: boolean,
VITE_COMPRESSION: ViteCompression
) {
const prodMock = true;
const lifecycle = process.env.npm_lifecycle_event;
return [
vue(),
// jsx、tsx语法支持
vueJsx(),
VITE_CDN ? cdn : null,
configCompressPlugin(VITE_COMPRESSION),
// 线上环境删除console
removeConsole({ external: ["src/assets/iconfont/iconfont.js"] }),
viteBuildInfo(),
// 自定义主题
themePreprocessorPlugin({
scss: {
multipleScopeVars: genScssMultipleScopeVars(),
extract: true
}
}),
// svg组件化支持
svgLoader(),
// ElementPlus({}),
// mock支持
viteMockServe({
mockPath: "mock",
localEnabled: command === "serve",
prodEnabled: command !== "serve" && prodMock,
injectCode: `
import { setupProdMockServer } from './mockProdServer';
setupProdMockServer();
`,
logger: false
}),
// 打包分析
lifecycle === "report"
? visualizer({ open: true, brotliSize: true, filename: "report.html" })
: null
];
}

@ -0,0 +1,32 @@
module.exports = {
ignores: [commit => commit.includes("init")],
extends: ["@commitlint/config-conventional"],
rules: {
"body-leading-blank": [2, "always"],
"footer-leading-blank": [1, "always"],
"header-max-length": [2, "always", 108],
"subject-empty": [2, "never"],
"type-empty": [2, "never"],
"type-enum": [
2,
"always",
[
"feat",
"fix",
"perf",
"style",
"docs",
"test",
"refactor",
"build",
"ci",
"chore",
"revert",
"wip",
"workflow",
"types",
"release"
]
]
}
};

@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit" />
<meta
name="viewport"
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
/>
<title>pure-admin-thin</title>
<link rel="icon" href="/favicon.ico" />
<script>
window.process = {};
</script>
</head>
<body>
<div id="app">
<style>
html,
body,
#app {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
overflow: hidden;
}
.loader,
.loader::before,
.loader::after {
width: 2.5em;
height: 2.5em;
border-radius: 50%;
animation: load-animation 1.8s infinite ease-in-out;
animation-fill-mode: both;
}
.loader {
position: relative;
top: 0;
margin: 80px auto;
font-size: 10px;
color: #406eeb;
text-indent: -9999em;
transform: translateZ(0);
transform: translate(-50%, 0);
animation-delay: -0.16s;
}
.loader::before,
.loader::after {
position: absolute;
top: 0;
content: "";
}
.loader::before {
left: -3.5em;
animation-delay: -0.32s;
}
.loader::after {
left: 3.5em;
}
@keyframes load-animation {
0%,
80%,
100% {
box-shadow: 0 2.5em 0 -1.3em;
}
40% {
box-shadow: 0 2.5em 0 0;
}
}
</style>
<div class="loader"></div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

@ -0,0 +1,50 @@
// 模拟后端动态生成路由
import { MockMethod } from "vite-plugin-mock";
/**
* roles "admin""common"
* admin
* common
*/
const permissionRouter = {
path: "/permission",
meta: {
title: "权限管理",
icon: "lollipop",
rank: 10
},
children: [
{
path: "/permission/page/index",
name: "PermissionPage",
meta: {
title: "页面权限",
roles: ["admin", "common"]
}
},
{
path: "/permission/button/index",
name: "PermissionButton",
meta: {
title: "按钮权限",
roles: ["admin", "common"],
auths: ["btn_add", "btn_edit", "btn_delete"]
}
}
]
};
export default [
{
url: "/getAsyncRoutes",
method: "get",
response: () => {
return {
success: true,
// data: [permissionRouter]
data: []
};
}
}
] as MockMethod[];

@ -0,0 +1,36 @@
// 根据角色动态生成路由
import { MockMethod } from "vite-plugin-mock";
export default [
{
url: "/login",
method: "post",
response: ({ body }) => {
if (body.username === "admin") {
return {
success: true,
data: {
username: "admin",
// 一个用户可能有多个角色
roles: ["admin"],
accessToken: "eyJhbGciOiJIUzUxMiJ9.admin",
refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh",
expires: "2023/10/30 00:00:00"
}
};
} else {
return {
success: true,
data: {
username: "common",
// 一个用户可能有多个角色
roles: ["common"],
accessToken: "eyJhbGciOiJIUzUxMiJ9.common",
refreshToken: "eyJhbGciOiJIUzUxMiJ9.commonRefresh",
expires: "2023/10/30 00:00:00"
}
};
}
}
}
] as MockMethod[];

@ -0,0 +1,27 @@
import { MockMethod } from "vite-plugin-mock";
// 模拟刷新token接口
export default [
{
url: "/refreshToken",
method: "post",
response: ({ body }) => {
if (body.refreshToken) {
return {
success: true,
data: {
accessToken: "eyJhbGciOiJIUzUxMiJ9.newAdmin",
refreshToken: "eyJhbGciOiJIUzUxMiJ9.newAdminRefresh",
// `expires`选择这种日期格式是为了方便调试,后端直接设置时间戳或许更方便(每次都应该递增)。如果后端返回的是时间戳格式,前端开发请来到这个目录`src/utils/auth.ts`,把第`38`行的代码换成expires = data.expires即可。
expires: "2023/10/30 23:59:59"
}
};
} else {
return {
success: false,
data: {}
};
}
}
}
] as MockMethod[];

@ -0,0 +1,136 @@
{
"name": "tp-admin",
"version": "2.0.0",
"private": true,
"scripts": {
"dev": "NODE_OPTIONS=--max-old-space-size=4096 vite",
"serve": "pnpm dev",
"build": "rimraf dist && NODE_OPTIONS=--max-old-space-size=8192 vite build",
"build:staging": "rimraf dist && vite build --mode staging",
"report": "rimraf dist && vite build",
"preview": "vite preview",
"preview:build": "pnpm build && vite preview",
"typecheck": "tsc --noEmit && vue-tsc --noEmit --skipLibCheck",
"svgo": "svgo -f src/assets/svg -o src/assets/svg",
"cloc": "NODE_OPTIONS=--max-old-space-size=4096 cloc . --exclude-dir=node_modules --exclude-lang=YAML",
"clean:cache": "rimraf node_modules && rimraf .eslintcache && pnpm install",
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock,build}/**/*.{vue,js,ts,tsx}\" --fix",
"lint:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,scss,vue,html,md}\"",
"lint:stylelint": "stylelint \"**/*.{html,vue,css,scss}\" --fix --cache --cache-location node_modules/.cache/stylelint/",
"lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js",
"lint:pretty": "pretty-quick --staged",
"lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint",
"prepare": "husky install",
"preinstall": "npx only-allow pnpm"
},
"browserslist": [
"> 1%",
"not ie 11",
"not op_mini all"
],
"dependencies": {
"@pureadmin/descriptions": "^1.1.1",
"@pureadmin/table": "^2.3.2",
"@pureadmin/utils": "^1.9.6",
"@vueuse/core": "^10.2.0",
"@vueuse/motion": "^2.0.0",
"animate.css": "^4.1.1",
"axios": "^1.4.0",
"dayjs": "^1.11.8",
"dplayer": "^1.27.1",
"echarts": "^5.4.2",
"element-plus": "^2.3.6",
"js-cookie": "^3.0.5",
"mitt": "^3.0.0",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"path": "^0.12.7",
"pinia": "^2.1.4",
"qs": "^6.11.2",
"responsive-storage": "^2.2.0",
"sortablejs": "^1.15.0",
"vue": "^3.3.4",
"vue-router": "^4.2.2",
"vue-types": "^5.0.4"
},
"devDependencies": {
"@commitlint/cli": "^17.6.5",
"@commitlint/config-conventional": "^17.6.5",
"@iconify-icons/ep": "^1.2.11",
"@iconify-icons/ri": "^1.2.8",
"@iconify/vue": "^4.1.1",
"@pureadmin/theme": "^3.1.0",
"@types/dplayer": "^1.25.2",
"@types/js-cookie": "^3.0.3",
"@types/mockjs": "^1.0.7",
"@types/node": "^20.3.1",
"@types/nprogress": "0.2.0",
"@types/qs": "^6.9.7",
"@types/sortablejs": "^1.15.1",
"@typescript-eslint/eslint-plugin": "^5.59.11",
"@typescript-eslint/parser": "^5.59.11",
"@vitejs/plugin-vue": "^4.2.3",
"@vitejs/plugin-vue-jsx": "^3.0.1",
"@vue/eslint-config-prettier": "^7.1.0",
"@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.14",
"cloc": "^2.11.0",
"cssnano": "^6.0.1",
"eslint": "^8.43.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.15.0",
"husky": "^8.0.3",
"lint-staged": "^13.2.2",
"picocolors": "^1.0.0",
"postcss": "^8.4.24",
"postcss-html": "^1.5.0",
"postcss-import": "^15.1.0",
"postcss-scss": "^4.0.6",
"prettier": "^2.8.8",
"pretty-quick": "^3.1.3",
"rimraf": "^5.0.1",
"rollup-plugin-visualizer": "^5.9.2",
"sass": "^1.63.4",
"sass-loader": "^13.3.2",
"stylelint": "^15.8.0",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recess-order": "^4.2.0",
"stylelint-config-recommended": "^12.0.0",
"stylelint-config-recommended-scss": "^12.0.0",
"stylelint-config-recommended-vue": "^1.4.0",
"stylelint-config-standard": "^33.0.0",
"stylelint-config-standard-scss": "^9.0.0",
"stylelint-order": "^6.0.3",
"stylelint-prettier": "^3.0.0",
"stylelint-scss": "^5.0.1",
"svgo": "^3.0.2",
"tailwindcss": "^3.3.2",
"terser": "^5.18.0",
"typescript": "5.0.4",
"vite": "^4.3.9",
"vite-plugin-cdn-import": "^0.3.5",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-mock": "2.9.6",
"vite-plugin-remove-console": "^2.1.1",
"vite-svg-loader": "^4.0.0",
"vue-eslint-parser": "^9.3.1",
"vue-tsc": "^1.8.0"
},
"pnpm": {
"peerDependencyRules": {
"ignoreMissing": [
"rollup",
"webpack",
"core-js"
]
},
"allowedDeprecatedVersions": {
"sourcemap-codec": "*",
"w3c-hr-time": "*",
"stable": "*"
}
},
"repository": "git@github.com:pure-admin/pure-admin-thin.git",
"author": "tp",
"license": "MIT"
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,9 @@
module.exports = {
plugins: {
"postcss-import": {},
"tailwindcss/nesting": {},
tailwindcss: {},
autoprefixer: {},
...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {})
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="300px" height="300px" viewBox="0 0 300 300" enable-background="new 0 0 300 300" xml:space="preserve"> <image id="image0" width="300" height="300" x="0" y="0"
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAYAAAB5fY51AAAABGdBTUEAALGPC/xhBQAAACBjSFJN
AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QA/wD/AP+gvaeTAAAA
CXBIWXMAABYlAAAWJQFJUiTwAAATBElEQVR42u3deXgc9X3H8e/srV2tpNVhS5Zk+ZSM8YVtLgPh
MAEC4SxXCPCYkpSrLgRCSZ6mpCm04SqEhD5AKQQoR10IIcYGJxiIAePY+MAGy7Z8ybItr63L0urY
c6Z/SAqSbSxZs5rZnX2/nocHS7s7+/3N7nw0v9/M/EYEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAADKaYnYBmebehVu/JyIjRORkEflez68PishaEQn1/BcRkaiIaD2PK5L8z2qg
5WmDWopx+tajOWxK3GlXotlue1uex9lSmO0Ijspx7xkd8Owc6Xc12G021eyCkXwEloFsdy9NtRBI
Y4ooSvcX2KYoYlNE7DZF7DZFCr2O5tPH5S07r6rgnbMnBN7Ly3I2+tz2uNkVQz8CywDhWCL35gWb
3nh1TfDbZteSabxOm8wuz/nyzPGBD2aV+5dPGuH7oiTHtSM3y8keWBoisAwQjiXcNy/YFH51TdDs
UjKWTVGk0OeUiYXerSdX5Hx4zoTAu9NKs1eMDmQ1mF0bBo/AMkA4lvDcvGDT9lfXBEeZXQu6u47Z
LrucPi7v08unFL122dSiVwqzXSGz68LACKxhpmmaJxJXPTcv2NT46pqg3ex68DVFEQlkOWVGqX/D
dTNHPnd+Vf5b5YGserPrwjezmV2A1SmKEhaRThFZYXYt6E/TRJo7Y/Lnbc3T7lu07Tc/+L/N77y2
JjivPRJ3mV0bjsxhdgEZwiYibWYXgSNTNZGmjpj8aXPTzGXbWn575fQRl9S1dN01OpBVZ3Zt6I89
LONMM7sAHJ0mIuG4Kq+v23/5VS9+ueTN9fuvbeqI+s2uC18jsIyhiUi+2UVgcBKqJivr2o6b/1bN
Sw9/sOuhbQ2dE8yuCd0ILAN4nPaIiITNrgPHZl9bxPXkJ7tvv+vtmpfW7w3NNLseEFhGajK7ABy7
SFyVRdWNc/52QfUbb3/ZcLnZ9WQ6Ass4nOeTxtbuCY372Xvbf/3y5/vmmV1LJiOwjJMwuwAMnaaJ
bNzXXnbfoq1P/e/a4LVm15OpCCzjcO1amtNEZF9b1HfX2zUvvL42eJ3Z9WQiAss4zNRgEftD0awH
39/5bwu/OnCx2bVkGk4cNcjs8pw/dsVURUS6en7V97IoVboD7dC9MFuf5w00J9axBOJgL8ka6HmD
WY42iN8NVHu/x1VVs8dVzRGOq95QJB5o6ojn7w9F/e3RuGgG/FnQRKQ62DHmJ4u2P7N+b2jP9FL/
uuF/V4hwLaFhQuG4PRJXvdIdSr0BpUj/Sfp6/6/IkYPq0M9rqJ/fcH3ueuJi0K/VRCShao5oQnV3
RNTsUCSe09QZK6htCU+o3tcx/Yv60Oy6lvDY/aFoXjg+vD3xU8bkbvqvKyddO63Uv2FY3wgiQmDB
Ypo7Y/a2cDy/pqFz+prdbWe8X9N80aq6tpntkcSwfNcdNkWum1n81sPfHX9HSa6H+YOGGYEFS2vu
iPqX72yd+8sPah/4rLZ1ynC8R16WQ+4/b+xP7z6r4iGz22t1BBYyQmtXzPvqmuAPn19Zf1v1/o6q
rlhyu4pVI7z1L1wz+erTxuUtN7utVkZgIaNUBzuOe/Hz+tteWR28PhiKBNQkDdLbbYpcPLnwgycu
q7xxbAFzag0XTmtARplc7Nv0yMUT/+G/r5l03ayynC0OW3L+ZidUTZZsaZq7qLrharPbaGUEFjLS
hZOLljx5eeVNl04pWupxJGcziMRUeW3t/h98WR86zuz2WRWBhYw1Z2zeige+M+62G08sfs3r1D97
tSYi6/eGjl+4sZG9rGHCGBYyXlNnNOfeP2x97uXVwavjSRjUGpXjbt30k1MqcrOcrWa3zWo40x0Z
r8DratvVEr6roSM2cnF105mqztPl69siuT97d8cjInKL2W2zGrqEgIhUBDz7Hrtk4t+dPDpnczLG
4V9dG7xx/d7QVLPbZTUEFtCjaoSv5umrJl09odC7W++yWsNxz1sbDjCjQ5IRWEAfM0r9X/796WW/
KfQ5dU1praqafLzz4AW1zV3cPDeJCCzgEBdPKXrt3Mr8D/QsQxORzfs7Jn21r32W2e2xEgILOMTY
/Ky9N8wqfqbY7+rQs5yWrrjnw20tF5ndHivhKKFB9h4MF7WF4/nS/cdXk/5zXQ0nMyYOTEa7Bjtn
ltbnMbuISGG2s74o261rDv1vjQ8suXRK0e9fWFV/fSwxtFUYjqnyl9rWMxrbo4HCbFdLEtZJxiOw
DBCJJ5T739vxz4urG+dJ9+2+Dp3z6lDHssErQ3xsIGYE3UCT/R1p7jCtz2OqiPjuOK3sPhF5Sk8h
fo8jvmpX61NLa5rP297UNWKoy9l9MFK+enfoZBFZYsQKtDoCywCaJrY9rZGKr4IdfhHhTsLD7EB7
bGQylnNSRe7Km16v/mhXS/iaoZ5QGmyL+j/f3Xq6EFhJwRiWATxOe0JY12npgkn5b+d4HNGhvj6m
qrIx2DHjQCiaY3ZbrICNyDjchCINzSzzrxgdcO/Rs4wtBzqnHGiPlpjdFisgsIxDYKWhiUW+XccX
Z6/Xs4ydTV0VjR3RpHRTMx2BZRwCK019uzJ/sZ7Xd8VV2dcWKTW7HVZAYBmHwEpTZ40PLFV0HG9V
NU32h2JlZrfDCggsYABjCrJ2jc7zDPm8LlUTaepIzpHLTEdgGYc9rDR2SkXup0N9raZp0tp90jB0
IrCAQZhS4hvy3Z01TaQtEs8zuw1WQGAZhz2sNDY231sz1NdqIhJLaG6z22AFBBYwCCOynXpv3cV0
5ElAYAGD4HPZ23UugsBKAgILGARV09hWUgAfAqwo6eOF7dGE3ovWGcNMAmZrMI6uLkEgy6FlOe29
82hZTe+cVqomEm3pinnDMdXsmvrZH4rqPfGTwEoCAitNzD+9/IkzJ+S9K91f/L7hd6QgHGjjMHrj
6VujdpTfaV0xNefRj3b9y7LtB2cYXONRbW/qmqSn8W6HrdPsNlgBgZUmji/2rZpbWaBrnvF0EArH
lZdXB38oIjPMrqWvDXvbZw/1tYoiku2yt5ndBiuwYvfCqlKrj5TabU36HuSKutYzhvpaRVEk3+ts
SHZNmYjASh+Zclg85b6Tm/e3Vx0IRYdcl00RKfQ5g2a3wwpS7suBjGeTFBug/mBrywV6Xm+3KTLS
79prdjusgMBKH5myh5VSbe2IxpVl21vO1bOMHLcjOtLv0numPITAAo6qtik8sTrYMV3PMsbke3YV
ZbvoEiYBgWUcvXsNKbPXYYCUaevnu9u+tbs1ouscrKoRvo0jsp37zG6LFRBYSEUpE1iLqhv/piOS
GHI9HodNppZkryvwuTgPKwkILKQam+j/XiYl8P68rfnsFbWtc4Z6T0IRkeIc18GTyv2fJHH9ZDQC
K32k1JGzYZSMsNG9jIb2qPfZz/beGdRxP0GbIjK+wLtzeqn/86SuoQzGme5pIqZqWb3/DscSvX9o
+oWYp/taw6QIx7q7QYqiiNthMzos9b6f7j/Ef9rSdMXSmpYLEjr2rjwOu8wZk/thwOvUOzUNehBY
aeKl1ftuvfWNTWeLiNz1do3I4dfkKbe+san36X23soG2OOUIPyu97zE6z7OjpTP2UMDrDBvYXL3f
S11nytc0dIyb/7ua2xs6orpmCQ14HaFzJgYWPZD01ZO5CCzj6OqmvL+l+eT3RU42uuhZZTl7bzyx
+FkRMeQol9/jCC+ubvzVaWNyeu8F2LveDgvoPi9T+j7vlIrcj3+uo4YF6/bftGJX66l62qEoItNH
ZX9RNcK7wYj1likILAykSwy+jvGiyYXvich7ZjT2verGC295Y/NtoXBc13LsiiLnTMxfMirX02xG
O6yKQXcMxCYZcuH1zqausdf8z1eL6w6GC/QOomW5bNGrZ4x80ew2WQ2BBYjIhvrQ8fNer36zTeee
lUj3tYN3n1nxH6MDHi7HSTICCwPpOz5kSRv3tU++f8mOJ1fUts5MxvKmlmTvmH9G+aNmt8uKGMPC
YFg2sNbubps1//dbnv9ke+v0mKq/5+tz2eWWU0ufLPQ5W8xumxWxh4XBsGRgLdrYcNk9C7c+//H2
g0kJK0URmTMmd/W5lYGFZrfNqtjDQsYJheO+Zz7bc8+972y7bWtDZ7GeS2/68rsdcsPskqcnFvlq
zW6jVRFYGIilLgmqa+mquOqlL1/745amOVqSW3bZ1KI3vjOp4E2z22hlBBYsrz0SV3a1hCct3dJ8
6dyn1925raGzOJlZZVMUmVXm3/KjM8r/vcjv4mYTw4jAMo4lx4FSWXNHLGt7U9e0J5bVXb6ouvHK
9Xvbx4fjyT+lbFSOu/3208oeP6E85wuz22x1BJZxCCwDROIJW21zePzq3W2n/nhhzXeXbG6+pL4t
ouuawKNx2BW5YlrR65dPLXrpJrMbnwEILOOka2ANqffU2B71NXXGSnp+TPQsxyaHn9d1pGsDj/RY
v8cTquaMq5ojElc9B7vi+fVtkbKtDV3Hff+VjcfvaOyqrG0Jl7d0xSTZ41R92RRFzq8q+OTOb5U/
mOd1RobvndCLwDJOugbWkCzc2Pj951fW3yci0Z5f9U7M1zd8BrtO+j1P0zSJq5ojmlCd7RE1uyOa
8EXjqoR7/tMzJcyxmDsxsPJXl1XeNL7QW2fIG4LAMkI4lrDfvGCTpY62DaTuYHjs8p0Hx5ldx3CZ
Nip7+yMXT7h1YpF3u9m1ZBICywAepz3xxLK6T7u6J96Lydd7Gd80VYoc5ee+tEOe13cqFk3TRKlt
Do//oj5UOZxdo29gyT1KmyJSNcK396GLxt95QhmD7EYjsAzyozNHP9weiT8u3TMfDDY+BhtWvc9V
RETNdjtUEZG2cNz221X1d979h/bHEyYkltU4bIrMKPXX/OKCcXdfOLlwsf4l4lgRWAbKdjtiRr5f
jsehPr18T1T/kobEUlPSKIrIzDJ/zS8vmnDH3Mr8pWbXk6kILGAAbodNzhofWPnTuRX3nTUxf5nZ
9WQyAsv66AvqkOtxJOadVPLc/NPLH5lQ5N1pdj2ZjsCyPksOfhvB57bLgxeOv2feSSW/9rsdBH8K
ILAwXNJyA1ekO6hOrchddd/cin86t7Jg6Xyzi8JfEVhAD4dNkbH5WQduOLH4uetnFT87rsC72+ya
0B+BhYynKCIFXmfivKr8d+49e8y/nlDmX3e/2UXhiAgsDGSoY2Ap3yV0O2xSmutunTMmb9kVUwtf
Pnti4J2A12XWaSAYBAILGcemiEwa4QteeFzB7+ZW5i8+odT/l+IcN3OwpwECC4OR9kcaFaX75qYn
lPq33jan7NFzqwKLRwey6h8zuzAcEwILA0mrsFIUEZsoIoqI12mXsjx3cEJh1ubZ5TnLz68qeGdK
iW9dtttBty9NEVhISXZFEY/TJso3xKUi3fNR2RQRh01RHXZb1Oeyt5XmuvZUFvmqJ4/0rj9upG99
WZ6ntjTXXRfwOiM/N7tR0I3AQkoaW+BpvHF2yfOFPtcB6Zl9QnpyShQRh01iHoe9M9fjaCzKdgaL
c1z1JX53fZbLHufaGesisKxP79E6U7qEJTmuA9fPKv7PcYWcC4WvcSNVDMSsMSynpMGpETAWgYVU
Zje7AKQWAgupLK2OUGL4EVgYCKGBlEFgYbgQdEg6AgupigF3HIbAQiojtNAPgYWBmNW1I6xwGAIL
QNogsDBcGHRH0hFYANIGgWV97OnAMggsAGmDwMJw0btnx1FCHIbAApA2CCwAaYPAApA2CCwMBuNJ
SAkEFlIVIYnDEFgYDM7lQkogsDAY7O0gJRBYANIGgYWBKCKiDvF1QFIRWNaXjO5czOxGACLcSBUD
iKlqorkzXtzQHu2S7u9L756T1uffvXthiZ6fnY99tMtrdu2wHgILR1XXHK74x4XbXspy2dqkO5R6
bxsv0h1UvaGlytddR9umA51TkvD2dCvRD4GFozoYjsuSLU0nml0HIMIYFoA0QmABSBsElvWl60mf
6Vo3hhGBBSBtEFgA0gaBBSBtEFgA0gaBBSBtEFhIZRwpRD8EFlIVYYXDEFjWx/V4sAwCC6mKoMVh
CCzrS9fPuO9MEICIMFuD5SmKaDZFREuzTV9RFI+kb9himBBYFjdjlH/lj88e/YSm/XXjV2TgvZeh
xFuyIlETEXVMftaO3CxHo4GrCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADS2P8DwxtL8OdpfRIAAAAl
dEVYdGRhdGU6Y3JlYXRlADIwMjMtMDctMjRUMDc6MzA6MzArMDI6MDC/wDOIAAAAJXRFWHRkYXRl
Om1vZGlmeQAyMDIzLTA3LTI0VDA3OjMwOjMwKzAyOjAwzp2LNAAAAABJRU5ErkJggg==" />
</svg>

After

Width:  |  Height:  |  Size: 7.2 KiB

@ -0,0 +1,23 @@
{
"Version": "4.4.0",
"Title": "TpAdmin",
"FixedHeader": true,
"HiddenSideBar": false,
"MultiTagsCache": false,
"KeepAlive": true,
"Layout": "vertical",
"Theme": "default",
"DarkMode": false,
"Grey": false,
"Weak": false,
"HideTabs": false,
"SidebarStatus": true,
"EpThemeColor": "#409EFF",
"ShowLogo": true,
"ShowModel": "smart",
"MenuArrowIconNoTransition": true,
"CachingAsyncRoutes": false,
"TooltipEffect": "light",
"ResponsiveStorageNameSpace": "responsive-",
"AdminHostUrl": "http://192.168.10.81:8848/"
}

@ -0,0 +1,25 @@
<template>
<el-config-provider :locale="currentLocale">
<router-view />
<ReDialog />
</el-config-provider>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { ElConfigProvider } from "element-plus";
import zhCn from "element-plus/lib/locale/lang/zh-cn";
import { ReDialog } from "@/components/ReDialog";
export default defineComponent({
name: "app",
components: {
[ElConfigProvider.name]: ElConfigProvider,
ReDialog
},
computed: {
currentLocale() {
return zhCn;
}
}
});
</script>

@ -0,0 +1,10 @@
import { http } from "@/utils/http";
type Result = {
success: boolean;
data: Array<any>;
};
export const getResultList = () => {
return http.request<Result>("get", "/getList");
};

@ -0,0 +1,22 @@
import { http } from "@/utils/http";
import { baseUrlApi } from "./utils";
type Result = {
count: number;
results?: Array<any>;
};
type Data = {
success: boolean;
msg: any;
results?: Array<any>;
};
export const getHomeList = (params?: object) => {
return http.request<Result>("get", baseUrlApi("tps"), { params });
};
export const getEvents = (params?: object) => {
return http.request<Data>("get", baseUrlApi("tps/events"), { params });
};
export const updateHomeList = (params?: object) => {
return http.request<Data>("put", baseUrlApi(""), { params });
};

@ -0,0 +1,10 @@
import { http } from "@/utils/http";
type Result = {
success: boolean;
data: Array<any>;
};
export const getAsyncRoutes = () => {
return http.request<Result>("get", "/getAsyncRoutes");
};

@ -0,0 +1,84 @@
import { http } from "@/utils/http";
import { baseUrlApi } from "./utils";
type deptResult = {
data: Array<any>;
};
/** 部门列表查询 */
export const getDeptList = (params?: object) => {
return http.request<deptResult>("get", baseUrlApi("departments/"), {
params
});
};
type deptStatus = {
success: boolean;
msg: any;
};
/** 新增部门 */
export const addDept = (data?: object) => {
return http.request<deptStatus>("post", baseUrlApi("departments/"), {
data
});
};
/** 更新部门 */
export const updateDept = (data?: object | any) => {
return http.request<deptStatus>(
"put",
baseUrlApi(`departments/${data.id}/`),
{
data
}
);
};
/** 删除部门 */
export const deleteDept = (data?: object | any) => {
return http.request<deptStatus>(
"delete",
baseUrlApi(`departments/${data}/`),
{
data
}
);
};
type userList = {
count: number;
next: any;
previous: any;
results?: Array<any>;
};
/** 用户列表查询 */
export const getUserList = (params?: object) => {
return http.request<userList>("get", baseUrlApi("user/"), {
params
});
};
type userStatus = {
success: boolean;
msg: any;
};
/** 新增用户 */
export const addUser = (data?: object) => {
return http.request<userStatus>("post", baseUrlApi("user/"), { data });
};
/** 更新用户 */
export const updateUser = (data?: object | any) => {
return http.request<userStatus>("put", baseUrlApi(`user/${data.id}/`), {
data
});
};
/** 删除用户 */
export const deleteUser = (data?: object | any) => {
return http.request<userStatus>("delete", baseUrlApi(`user/${data}/`), {
data
});
};

@ -0,0 +1,54 @@
import { http } from "@/utils/http";
import { baseUrlApi } from "./utils";
// import { getConfig } from "@/config";
export type UserResult = {
success: boolean;
/** 登录信息 */
msg: string;
data: {
/** 用户名 */
username: string;
/** 当前登陆用户的角色 */
roles: Array<string>;
/** `token` */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx' */
expires: Date;
};
};
export type RefreshTokenResult = {
success: boolean;
data: {
/** `token` */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx' */
expires: Date;
};
};
/** 登录 */
// export const getLogin = (data?: object) => {
// return http.request<UserResult>("post", "/login", { data });
// };
export const getLogin = (data?: object) => {
return http.request<UserResult>("post", baseUrlApi("user/login/"), { data });
};
// export const getLogin = (data?: object) => {
// const AdminHostUrl = getConfig().AdminHostUrl;
// const url = AdminHostUrl + "login";
// return http.request<UserResult>("post", url, { data });
// };
/** 刷新token */
export const refreshTokenApi = (data?: object) => {
return http.request<RefreshTokenResult>("post", baseUrlApi("/refreshToken"), {
data
});
};

@ -0,0 +1,2 @@
const { VITE_APP_BASE_URL } = import.meta.env;
export const baseUrlApi = (url: string) => `${VITE_APP_BASE_URL}/api/${url}`;

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

@ -0,0 +1,26 @@
@font-face {
font-family: "iconfont"; /* Project id 2208059 */
src: url("iconfont.woff2?t=1671895108120") format("woff2"),
url("iconfont.woff?t=1671895108120") format("woff"),
url("iconfont.ttf?t=1671895108120") format("truetype");
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.pure-iconfont-tabs:before {
content: "\e63e";
}
.pure-iconfont-logo:before {
content: "\e620";
}
.pure-iconfont-new:before {
content: "\e615";
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,30 @@
{
"id": "2208059",
"name": "pure-admin",
"font_family": "iconfont",
"css_prefix_text": "pure-iconfont-",
"description": "pure-admin-iconfont",
"glyphs": [
{
"icon_id": "20594647",
"name": "Tabs",
"font_class": "tabs",
"unicode": "e63e",
"unicode_decimal": 58942
},
{
"icon_id": "22129506",
"name": "PureLogo",
"font_class": "logo",
"unicode": "e620",
"unicode_decimal": 58912
},
{
"icon_id": "7795615",
"name": "New",
"font_class": "new",
"unicode": "e615",
"unicode_decimal": 58901
}
]
}

Binary file not shown.

Binary file not shown.

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path fill="#386BF3" d="M410.558.109c0 210.974-300.876 361.752-300.876 633.548 0 174.943 134.704 316.787 300.876 316.787s300.877-141.817 300.877-316.787C711.408 361.752 410.558 210.974 410.558.109z"/><path fill="#C3D2FB" d="M613.469 73.665c0 211.055-300.877 361.914-300.877 633.547C312.592 882.156 447.296 1024 613.47 1024s300.876-141.817 300.876-316.788C914.29 435.58 613.469 284.72 613.469 73.665z"/><path fill="#303F5B" d="M312.592 707.212c0-183.713 137.636-312.171 226.723-441.39 81.702 106.112 172.12 218.74 172.12 367.726A309.755 309.755 0 0 1 420.36 950.064a323.114 323.114 0 0 1-107.769-242.852z"/></svg>

After

Width:  |  Height:  |  Size: 712 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="133px" height="143px" viewBox="0 0 133 143" enable-background="new 0 0 133 143" xml:space="preserve"> <image id="image0" width="133" height="143" x="0" y="0"
href="data:image/png;base64,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" />
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M2.88 18.054a35.897 35.897 0 0 1 8.531-16.32.8.8 0 0 1 1.178 0c.166.18.304.332.413.455a35.897 35.897 0 0 1 8.118 15.865c-2.141.451-4.34.747-6.584.874l-2.089 4.178a.5.5 0 0 1-.894 0l-2.089-4.178a44.019 44.019 0 0 1-6.584-.874zm6.698-1.123 1.157.066L12 19.527l1.265-2.53 1.157-.066a42.137 42.137 0 0 0 4.227-.454A33.913 33.913 0 0 0 12 4.09a33.913 33.913 0 0 0-6.649 12.387c1.395.222 2.805.374 4.227.454zM12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></svg>

After

Width:  |  Height:  |  Size: 588 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11.38 2.019a7.5 7.5 0 1 0 10.6 10.6C21.662 17.854 17.316 22 12.001 22 6.477 22 2 17.523 2 12c0-5.315 4.146-9.661 9.38-9.981z"/></svg>

After

Width:  |  Height:  |  Size: 263 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85 1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"/></svg>

After

Width:  |  Height:  |  Size: 480 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" aria-hidden="true" class="iconify iconify--ant-design" viewBox="0 0 1024 1024"><path fill="currentColor" d="M864 170h-60c-4.4 0-8 3.6-8 8v518H310v-73c0-6.7-7.8-10.5-13-6.3l-141.9 112a8 8 0 0 0 0 12.6l141.9 112c5.3 4.2 13 .4 13-6.3v-75h498c35.3 0 64-28.7 64-64V178c0-4.4-3.6-8-8-8z"/></svg>

After

Width:  |  Height:  |  Size: 352 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="true" class="re-screen" color="#00000073" viewBox="0 0 16 16"><path fill="currentColor" d="M3.5 4H1V3h2V1h1v2.5l-.5.5zM13 3V1h-1v2.5l.5.5H15V3h-2zm-1 9.5V15h1v-2h2v-1h-2.5l-.5.5zM1 12v1h2v2h1v-2.5l-.5-.5H1zm11-1.5-.5.5h-7l-.5-.5v-5l.5-.5h7l.5.5v5zM10 7H6v2h4V7z"/></svg>

After

Width:  |  Height:  |  Size: 348 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="true" class="re-screen" color="#00000073" viewBox="0 0 16 16"><path fill="currentColor" d="M3 12h10V4H3v8zm2-6h6v4H5V6zM2 6H1V2.5l.5-.5H5v1H2v3zm13-3.5V6h-1V3h-3V2h3.5l.5.5zM14 10h1v3.5l-.5.5H11v-1h3v-3zM2 13h3v1H1.5l-.5-.5V10h1v3z"/></svg>

After

Width:  |  Height:  |  Size: 318 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" aria-hidden="true" class="iconify iconify--mdi" viewBox="0 0 24 24"><path fill="currentColor" d="M1 7h6v2H3v2h4v2H3v2h4v2H1V7m10 0h4v2h-4v2h2a2 2 0 0 1 2 2v2c0 1.11-.89 2-2 2H9v-2h4v-2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2m8 0h2a2 2 0 0 1 2 2v1h-2V9h-2v6h2v-1h2v1c0 1.11-.89 2-2 2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2Z"/></svg>

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

@ -0,0 +1,5 @@
import auth from "./src/auth";
const Auth = auth;
export { Auth };

@ -0,0 +1,20 @@
import { defineComponent, Fragment } from "vue";
import { hasAuth } from "@/router/utils";
export default defineComponent({
name: "Auth",
props: {
value: {
type: undefined,
default: []
}
},
setup(props, { slots }) {
return () => {
if (!slots) return null;
return hasAuth(props.value) ? (
<Fragment>{slots.default?.()}</Fragment>
) : null;
};
}
});

@ -0,0 +1,29 @@
import { ElCol } from "element-plus";
import { h, defineComponent } from "vue";
// 封装element-plus的el-col组件
export default defineComponent({
name: "ReCol",
props: {
value: {
type: Number,
default: 24
}
},
render() {
const attrs = this.$attrs;
const val = this.value;
return h(
ElCol,
{
xs: val,
sm: val,
md: val,
lg: val,
xl: val,
...attrs
},
{ default: () => this.$slots.default() }
);
}
});

@ -0,0 +1,64 @@
import { ref } from "vue";
import reDialog from "./index.vue";
import { useTimeoutFn } from "@vueuse/core";
import { withInstall } from "@pureadmin/utils";
import type {
EventType,
ArgsType,
DialogProps,
ButtonProps,
DialogOptions
} from "./type";
const dialogStore = ref<Array<DialogOptions>>([]);
/** 打开弹框 */
const addDialog = (options: DialogOptions) => {
const open = () =>
dialogStore.value.push(Object.assign(options, { visible: true }));
if (options?.openDelay) {
useTimeoutFn(() => {
open();
}, options.openDelay);
} else {
open();
}
};
/** 关闭弹框 */
const closeDialog = (options: DialogOptions, index: number, args?: any) => {
dialogStore.value.splice(index, 1);
options.closeCallBack && options.closeCallBack({ options, index, args });
};
/**
* @description
* @param value
* @param key `title`
* @param index `0``index`
*/
const updateDialog = (value: any, key = "title", index = 0) => {
dialogStore.value[index][key] = value;
};
/** 关闭所有弹框 */
const closeAllDialog = () => {
dialogStore.value = [];
};
/** 使`addDialog`
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L4
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L13
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L18
*/
const ReDialog = withInstall(reDialog);
export type { EventType, ArgsType, DialogProps, ButtonProps, DialogOptions };
export {
ReDialog,
dialogStore,
addDialog,
closeDialog,
updateDialog,
closeAllDialog
};

@ -0,0 +1,157 @@
<script setup lang="ts">
import {
closeDialog,
dialogStore,
type EventType,
type ButtonProps,
type DialogOptions
} from "./index";
import { ref, computed } from "vue";
import { isFunction } from "@pureadmin/utils";
import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
const fullscreen = ref(false);
const footerButtons = computed(() => {
return (options: DialogOptions) => {
return options?.footerButtons?.length > 0
? options.footerButtons
: ([
{
label: "取消",
text: true,
bg: true,
btnClick: ({ dialog: { options, index } }) => {
const done = () =>
closeDialog(options, index, { command: "cancel" });
if (options?.beforeCancel && isFunction(options?.beforeCancel)) {
options.beforeCancel(done, { options, index });
} else {
done();
}
}
},
{
label: "确定",
type: "primary",
text: true,
bg: true,
btnClick: ({ dialog: { options, index } }) => {
const done = () =>
closeDialog(options, index, { command: "sure" });
if (options?.beforeSure && isFunction(options?.beforeSure)) {
options.beforeSure(done, { options, index });
} else {
done();
}
}
}
] as Array<ButtonProps>);
};
});
const fullscreenClass = computed(() => {
return [
"el-icon",
"el-dialog__close",
"-translate-x-2",
"cursor-pointer",
"hover:!text-[red]"
];
});
function eventsCallBack(
event: EventType,
options: DialogOptions,
index: number
) {
fullscreen.value = options?.fullscreen ?? false;
if (options?.[event] && isFunction(options?.[event])) {
return options?.[event]({ options, index });
}
}
function handleClose(
options: DialogOptions,
index: number,
args = { command: "close" }
) {
closeDialog(options, index, args);
eventsCallBack("close", options, index);
}
</script>
<template>
<el-dialog
class="pure-dialog"
v-for="(options, index) in dialogStore"
:key="index"
v-bind="options"
v-model="options.visible"
:fullscreen="fullscreen ? true : options?.fullscreen ? true : false"
@close="handleClose(options, index)"
@opened="eventsCallBack('open', options, index)"
@openAutoFocus="eventsCallBack('openAutoFocus', options, index)"
@closeAutoFocus="eventsCallBack('closeAutoFocus', options, index)"
>
<!-- header -->
<template
v-if="options?.fullscreenIcon || options?.headerRenderer"
#header="{ close, titleId, titleClass }"
>
<div
v-if="options?.fullscreenIcon"
class="flex items-center justify-between"
>
<span :id="titleId" :class="titleClass">{{ options?.title }}</span>
<i
v-if="!options?.fullscreen"
:class="fullscreenClass"
@click="fullscreen = !fullscreen"
>
<IconifyIconOffline
class="pure-dialog-svg"
:icon="
options?.fullscreen
? ExitFullscreen
: fullscreen
? ExitFullscreen
: Fullscreen
"
/>
</i>
</div>
<component
v-else
:is="options?.headerRenderer({ close, titleId, titleClass })"
/>
</template>
<component
v-bind="options?.props"
:is="options.contentRenderer({ options, index })"
@close="args => handleClose(options, index, args)"
/>
<!-- footer -->
<template v-if="!options?.hideFooter" #footer>
<template v-if="options?.footerRenderer">
<component :is="options?.footerRenderer({ options, index })" />
</template>
<span v-else>
<el-button
v-for="(btn, key) in footerButtons(options)"
:key="key"
v-bind="btn"
@click="
btn.btnClick({
dialog: { options, index },
button: { btn, index: key }
})
"
>
{{ btn?.label }}
</el-button>
</span>
</template>
</el-dialog>
</template>

@ -0,0 +1,218 @@
import type { CSSProperties, VNode, Component } from "vue";
type DoneFn = (cancel?: boolean) => void;
type EventType = "open" | "close" | "openAutoFocus" | "closeAutoFocus";
type ArgsType = {
/** `cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或者空白页 */
command: "cancel" | "sure" | "close";
};
/** https://element-plus.org/zh-CN/component/dialog.html#attributes */
type DialogProps = {
/** `Dialog` 的显示与隐藏 */
visible?: boolean;
/** `Dialog` 的标题 */
title?: string;
/** `Dialog` 的宽度,默认 `50%` */
width?: string | number;
/** 是否为全屏 `Dialog`(会一直处于全屏状态,除非弹框关闭),默认 `false``fullscreen` 和 `fullscreenIcon` 都传时只有 `fullscreen` 会生效 */
fullscreen?: boolean;
/** 是否显示全屏操作图标,默认 `false``fullscreen` 和 `fullscreenIcon` 都传时只有 `fullscreen` 会生效 */
fullscreenIcon?: boolean;
/** `Dialog CSS` 中的 `margin-top` 值,默认 `15vh` */
top?: string;
/** 是否需要遮罩层,默认 `true` */
modal?: boolean;
/** `Dialog` 自身是否插入至 `body` 元素上。嵌套的 `Dialog` 必须指定该属性并赋值为 `true`,默认 `false` */
appendToBody?: boolean;
/** 是否在 `Dialog` 出现时将 `body` 滚动锁定,默认 `true` */
lockScroll?: boolean;
/** `Dialog` 的自定义类名 */
class?: string;
/** `Dialog` 的自定义样式 */
style?: CSSProperties;
/** `Dialog` 打开的延时时间,单位毫秒,默认 `0` */
openDelay?: number;
/** `Dialog` 关闭的延时时间,单位毫秒,默认 `0` */
closeDelay?: number;
/** 是否可以通过点击 `modal` 关闭 `Dialog`,默认 `true` */
closeOnClickModal?: boolean;
/** 是否可以通过按下 `ESC` 关闭 `Dialog`,默认 `true` */
closeOnPressEscape?: boolean;
/** 是否显示关闭按钮,默认 `true` */
showClose?: boolean;
/** 关闭前的回调,会暂停 `Dialog` 的关闭. 回调函数内执行 `done` 参数方法的时候才是真正关闭对话框的时候 */
beforeClose?: (done: DoneFn) => void;
/** 为 `Dialog` 启用可拖拽功能,默认 `false` */
draggable?: boolean;
/** 是否让 `Dialog` 的 `header` 和 `footer` 部分居中排列,默认 `false` */
center?: boolean;
/** 是否水平垂直对齐对话框,默认 `false` */
alignCenter?: boolean;
/** 当关闭 `Dialog` 时,销毁其中的元素,默认 `false` */
destroyOnClose?: boolean;
};
type BtnClickDialog = {
options?: DialogOptions;
index?: number;
};
type BtnClickButton = {
btn?: ButtonProps;
index?: number;
};
/** https://element-plus.org/zh-CN/component/button.html#button-attributes */
type ButtonProps = {
/** 按钮文字 */
label: string;
/** 按钮尺寸 */
size?: "large" | "default" | "small";
/** 按钮类型 */
type?: "primary" | "success" | "warning" | "danger" | "info";
/** 是否为朴素按钮,默认 `false` */
plain?: boolean;
/** 是否为文字按钮,默认 `false` */
text?: boolean;
/** 是否显示文字按钮背景颜色,默认 `false` */
bg?: boolean;
/** 是否为链接按钮,默认 `false` */
link?: boolean;
/** 是否为圆角按钮,默认 `false` */
round?: boolean;
/** 是否为圆形按钮,默认 `false` */
circle?: boolean;
/** 是否为加载中状态,默认 `false` */
loading?: boolean;
/** 自定义加载中状态图标组件 */
loadingIcon?: string | Component;
/** 按钮是否为禁用状态,默认 `false` */
disabled?: boolean;
/** 图标组件 */
icon?: string | Component;
/** 是否开启原生 `autofocus` 属性,默认 `false` */
autofocus?: boolean;
/** 原生 `type` 属性,默认 `button` */
nativeType?: "button" | "submit" | "reset";
/** 自动在两个中文字符之间插入空格 */
autoInsertSpace?: boolean;
/** 自定义按钮颜色, 并自动计算 `hover` 和 `active` 触发后的颜色 */
color?: string;
/** `dark` 模式, 意味着自动设置 `color` 为 `dark` 模式的颜色,默认 `false` */
dark?: boolean;
/** 自定义元素标签 */
tag?: string | Component;
/** 点击按钮后触发的回调 */
btnClick?: ({
dialog,
button
}: {
/** 当前 `Dialog` 信息 */
dialog: BtnClickDialog;
/** 当前 `button` 信息 */
button: BtnClickButton;
}) => void;
};
interface DialogOptions extends DialogProps {
/** 内容区组件的 `props`,可通过 `defineProps` 接收 */
props?: any;
/** 是否隐藏 `Dialog` 按钮操作区的内容 */
hideFooter?: boolean;
/**
* @description
* @see {@link https://element-plus.org/zh-CN/component/dialog.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E5%A4%B4%E9%83%A8}
*/
headerRenderer?: ({
close,
titleId,
titleClass
}: {
close: Function;
titleId: string;
titleClass: string;
}) => VNode | Component;
/** 自定义内容渲染器 */
contentRenderer?: ({
options,
index
}: {
options: DialogOptions;
index: number;
}) => VNode | Component;
/** 自定义按钮操作区的内容渲染器,会覆盖`footerButtons`以及默认的 `取消` 和 `确定` 按钮 */
footerRenderer?: ({
options,
index
}: {
options: DialogOptions;
index: number;
}) => VNode | Component;
/** 自定义底部按钮操作 */
footerButtons?: Array<ButtonProps>;
/** `Dialog` 打开后的回调 */
open?: ({
options,
index
}: {
options: DialogOptions;
index: number;
}) => void;
/** `Dialog` 关闭后的回调(只有点击右上角关闭按钮或者空白页关闭页面时才会触发) */
close?: ({
options,
index
}: {
options: DialogOptions;
index: number;
}) => void;
/** `Dialog` 关闭后的回调。 `args` 返回的 `command` 值解析:`cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或者空白页 */
closeCallBack?: ({
options,
index,
args
}: {
options: DialogOptions;
index: number;
args: any;
}) => void;
/** 输入焦点聚焦在 `Dialog` 内容时的回调 */
openAutoFocus?: ({
options,
index
}: {
options: DialogOptions;
index: number;
}) => void;
/** 输入焦点从 `Dialog` 内容失焦时的回调 */
closeAutoFocus?: ({
options,
index
}: {
options: DialogOptions;
index: number;
}) => void;
/** 点击底部取消按钮的回调,会暂停 `Dialog` 的关闭. 回调函数内执行 `done` 参数方法的时候才是真正关闭对话框的时候 */
beforeCancel?: (
done: Function,
{
options,
index
}: {
options: DialogOptions;
index: number;
}
) => void;
/** 点击底部确定按钮的回调,会暂停 `Dialog` 的关闭. 回调函数内执行 `done` 参数方法的时候才是真正关闭对话框的时候 */
beforeSure?: (
done: Function,
{
options,
index
}: {
options: DialogOptions;
index: number;
}
) => void;
}
export type { EventType, ArgsType, DialogProps, ButtonProps, DialogOptions };

@ -0,0 +1,12 @@
import iconifyIconOffline from "./src/iconifyIconOffline";
import iconifyIconOnline from "./src/iconifyIconOnline";
import fontIcon from "./src/iconfont";
/** 本地图标组件 */
const IconifyIconOffline = iconifyIconOffline;
/** 在线图标组件 */
const IconifyIconOnline = iconifyIconOnline;
/** iconfont组件 */
const FontIcon = fontIcon;
export { IconifyIconOffline, IconifyIconOnline, FontIcon };

@ -0,0 +1,61 @@
import { iconType } from "./types";
import { h, defineComponent, Component } from "vue";
import { IconifyIconOnline, IconifyIconOffline, FontIcon } from "../index";
/**
* `iconfont` `svg` `iconify`
* @see {@link https://yiming_chang.gitee.io/pure-admin-doc/pages/icon/}
* @param icon
* @param attrs iconType
* @returns Component
*/
export function useRenderIcon(icon: any, attrs?: iconType): Component {
// iconfont
const ifReg = /^IF-/;
// typeof icon === "function" 属于SVG
if (ifReg.test(icon)) {
// iconfont
const name = icon.split(ifReg)[1];
const iconName = name.slice(
0,
name.indexOf(" ") == -1 ? name.length : name.indexOf(" ")
);
const iconType = name.slice(name.indexOf(" ") + 1, name.length);
return defineComponent({
name: "FontIcon",
render() {
return h(FontIcon, {
icon: iconName,
iconType,
...attrs
});
}
});
} else if (typeof icon === "function" || typeof icon?.render === "function") {
// svg
return icon;
} else if (typeof icon === "object") {
return defineComponent({
name: "OfflineIcon",
render() {
return h(IconifyIconOffline, {
icon: icon,
...attrs
});
}
});
} else {
// 通过是否存在 : 符号来判断是在线还是本地图标,存在即是在线图标,反之
return defineComponent({
name: "Icon",
render() {
const IconifyIcon =
icon && icon.includes(":") ? IconifyIconOnline : IconifyIconOffline;
return h(IconifyIcon, {
icon: icon,
...attrs
});
}
});
}
}

@ -0,0 +1,48 @@
import { h, defineComponent } from "vue";
// 封装iconfont组件默认`font-class`引用模式,支持`unicode`引用、`font-class`引用、`symbol`引用 https://www.iconfont.cn/help/detail?spm=a313x.7781069.1998910419.20&helptype=code
export default defineComponent({
name: "FontIcon",
props: {
icon: {
type: String,
default: ""
}
},
render() {
const attrs = this.$attrs;
if (Object.keys(attrs).includes("uni") || attrs?.iconType === "uni") {
return h(
"i",
{
class: "iconfont",
...attrs
},
this.icon
);
} else if (
Object.keys(attrs).includes("svg") ||
attrs?.iconType === "svg"
) {
return h(
"svg",
{
class: "icon-svg",
"aria-hidden": true
},
{
default: () => [
h("use", {
"xlink:href": `#${this.icon}`
})
]
}
);
} else {
return h("i", {
class: `iconfont ${this.icon}`,
...attrs
});
}
}
});

@ -0,0 +1,30 @@
import { h, defineComponent } from "vue";
import { Icon as IconifyIcon, addIcon } from "@iconify/vue/dist/offline";
// Iconify Icon在Vue里本地使用用于内网环境https://docs.iconify.design/icon-components/vue/offline.html
export default defineComponent({
name: "IconifyIconOffline",
components: { IconifyIcon },
props: {
icon: {
default: null
}
},
render() {
if (typeof this.icon === "object") addIcon(this.icon, this.icon);
const attrs = this.$attrs;
return h(
IconifyIcon,
{
icon: this.icon,
style: attrs?.style
? Object.assign(attrs.style, { outline: "none" })
: { outline: "none" },
...attrs
},
{
default: () => []
}
);
}
});

@ -0,0 +1,30 @@
import { h, defineComponent } from "vue";
import { Icon as IconifyIcon } from "@iconify/vue";
// Iconify Icon在Vue里在线使用用于外网环境
export default defineComponent({
name: "IconifyIconOnline",
components: { IconifyIcon },
props: {
icon: {
type: String,
default: ""
}
},
render() {
const attrs = this.$attrs;
return h(
IconifyIcon,
{
icon: `${this.icon}`,
style: attrs?.style
? Object.assign(attrs.style, { outline: "none" })
: { outline: "none" },
...attrs
},
{
default: () => []
}
);
}
});

@ -0,0 +1,14 @@
import { addIcon } from "@iconify/vue/dist/offline";
/**
* src/layout/index.vue
*/
// 本地菜单图标后端在路由的icon中返回对应的图标字符串并且前端在此处使用addIcon添加即可渲染菜单图标
import HomeFilled from "@iconify-icons/ep/home-filled";
import InformationLine from "@iconify-icons/ri/information-line";
import Lollipop from "@iconify-icons/ep/lollipop";
addIcon("homeFilled", HomeFilled);
addIcon("informationLine", InformationLine);
addIcon("lollipop", Lollipop);

@ -0,0 +1,19 @@
export interface iconType {
// iconify (https://docs.iconify.design/icon-components/vue/#properties)
inline?: boolean;
width?: string | number;
height?: string | number;
horizontalFlip?: boolean;
verticalFlip?: boolean;
flip?: string;
rotate?: number | string;
color?: string;
horizontalAlign?: boolean;
verticalAlign?: boolean;
align?: string;
onLoad?: Function;
includes?: Function;
// all icon
style?: object;
}

@ -0,0 +1,5 @@
import pureTableBar from "./src/bar";
import { withInstall } from "@pureadmin/utils";
/** 配合 `@pureadmin/table` 实现快速便捷的表格操作 https://github.com/pure-admin/pure-admin-table */
export const PureTableBar = withInstall(pureTableBar);

@ -0,0 +1,344 @@
import { useEpThemeStoreHook } from "@/store/modules/epTheme";
import { delay, getKeyList, cloneDeep } from "@pureadmin/utils";
import { defineComponent, ref, computed, type PropType, nextTick } from "vue";
import Sortable from "sortablejs";
import DragIcon from "./svg/drag.svg?component";
import ExpandIcon from "./svg/expand.svg?component";
import RefreshIcon from "./svg/refresh.svg?component";
import SettingIcon from "./svg/settings.svg?component";
import CollapseIcon from "./svg/collapse.svg?component";
const props = {
/** 头部最左边的标题 */
title: {
type: String,
default: "列表"
},
/** 对于树形表格如果想启用展开和折叠功能传入当前表格的ref即可 */
tableRef: {
type: Object as PropType<any>
},
/** 需要展示的列 */
columns: {
type: Array as PropType<TableColumnList>,
default: () => []
}
};
export default defineComponent({
name: "PureTableBar",
props,
emits: ["refresh"],
setup(props, { emit, slots, attrs }) {
const buttonRef = ref();
const size = ref("default");
const isExpandAll = ref(true);
const loading = ref(false);
const checkAll = ref(true);
const isIndeterminate = ref(false);
let checkColumnList = getKeyList(cloneDeep(props?.columns), "label");
const checkedColumns = ref(checkColumnList);
const dynamicColumns = ref(cloneDeep(props?.columns));
const getDropdownItemStyle = computed(() => {
return s => {
return {
background:
s === size.value ? useEpThemeStoreHook().epThemeColor : "",
color: s === size.value ? "#fff" : "var(--el-text-color-primary)"
};
};
});
const iconClass = computed(() => {
return [
"text-black",
"dark:text-white",
"duration-100",
"hover:!text-primary",
"cursor-pointer",
"outline-none"
];
});
const topClass = computed(() => {
return [
"flex",
"justify-between",
"pt-[3px]",
"px-[11px]",
"border-b-[1px]",
"border-solid",
"border-[#dcdfe6]",
"dark:border-[#303030]"
];
});
function onReFresh() {
loading.value = true;
emit("refresh");
delay(500).then(() => (loading.value = false));
}
function onExpand() {
isExpandAll.value = !isExpandAll.value;
toggleRowExpansionAll(props.tableRef.data, isExpandAll.value);
}
function toggleRowExpansionAll(data, isExpansion) {
data.forEach(item => {
props.tableRef.toggleRowExpansion(item, isExpansion);
if (item.children !== undefined && item.children !== null) {
toggleRowExpansionAll(item.children, isExpansion);
}
});
}
function handleCheckAllChange(val: boolean) {
checkedColumns.value = val ? checkColumnList : [];
isIndeterminate.value = false;
dynamicColumns.value.map(column =>
val ? (column.hide = false) : (column.hide = true)
);
}
function handleCheckedColumnsChange(value: string[]) {
const checkedCount = value.length;
checkAll.value = checkedCount === checkColumnList.length;
isIndeterminate.value =
checkedCount > 0 && checkedCount < checkColumnList.length;
}
function handleCheckColumnListChange(val: boolean, label: string) {
dynamicColumns.value.filter(item => item.label === label)[0].hide = !val;
}
async function onReset() {
checkAll.value = true;
isIndeterminate.value = false;
dynamicColumns.value = cloneDeep(props?.columns);
checkColumnList = [];
checkColumnList = await getKeyList(cloneDeep(props?.columns), "label");
checkedColumns.value = checkColumnList;
}
const dropdown = {
dropdown: () => (
<el-dropdown-menu class="translation">
<el-dropdown-item
style={getDropdownItemStyle.value("large")}
onClick={() => (size.value = "large")}
>
</el-dropdown-item>
<el-dropdown-item
style={getDropdownItemStyle.value("default")}
onClick={() => (size.value = "default")}
>
</el-dropdown-item>
<el-dropdown-item
style={getDropdownItemStyle.value("small")}
onClick={() => (size.value = "small")}
>
</el-dropdown-item>
</el-dropdown-menu>
)
};
/** 列展示拖拽排序 */
const rowDrop = (event: { preventDefault: () => void }) => {
event.preventDefault();
nextTick(() => {
const wrapper: HTMLElement = document.querySelector(
".el-checkbox-group>div"
);
Sortable.create(wrapper, {
animation: 300,
handle: ".drag-btn",
onEnd: ({ newIndex, oldIndex, item }) => {
const targetThElem = item;
const wrapperElem = targetThElem.parentNode as HTMLElement;
const oldColumn = dynamicColumns.value[oldIndex];
const newColumn = dynamicColumns.value[newIndex];
if (oldColumn?.fixed || newColumn?.fixed) {
// 当前列存在fixed属性 则不可拖拽
const oldThElem = wrapperElem.children[oldIndex] as HTMLElement;
if (newIndex > oldIndex) {
wrapperElem.insertBefore(targetThElem, oldThElem);
} else {
wrapperElem.insertBefore(
targetThElem,
oldThElem ? oldThElem.nextElementSibling : oldThElem
);
}
return;
}
const currentRow = dynamicColumns.value.splice(oldIndex, 1)[0];
dynamicColumns.value.splice(newIndex, 0, currentRow);
}
});
});
};
const isFixedColumn = (label: string) => {
return dynamicColumns.value.filter(item => item.label === label)[0].fixed
? true
: false;
};
const reference = {
reference: () => (
<SettingIcon
class={["w-[16px]", iconClass.value]}
onMouseover={e => (buttonRef.value = e.currentTarget)}
/>
)
};
return () => (
<>
<div {...attrs} class="w-[99/100] mt-2 px-2 pb-2 bg-bg_color">
<div class="flex justify-between w-full h-[60px] p-4">
{slots?.title ? (
slots.title()
) : (
<p class="font-bold truncate">{props.title}</p>
)}
<div class="flex items-center justify-around">
{slots?.buttons ? (
<div class="flex mr-4">{slots.buttons()}</div>
) : null}
{props.tableRef?.size ? (
<>
<el-tooltip
effect="dark"
content={isExpandAll.value ? "折叠" : "展开"}
placement="top"
>
<ExpandIcon
class={["w-[16px]", iconClass.value]}
style={{
transform: isExpandAll.value ? "none" : "rotate(-90deg)"
}}
onClick={() => onExpand()}
/>
</el-tooltip>
<el-divider direction="vertical" />
</>
) : null}
<el-tooltip effect="dark" content="刷新" placement="top">
<RefreshIcon
class={[
"w-[16px]",
iconClass.value,
loading.value ? "animate-spin" : ""
]}
onClick={() => onReFresh()}
/>
</el-tooltip>
<el-divider direction="vertical" />
<el-tooltip effect="dark" content="密度" placement="top">
<el-dropdown v-slots={dropdown} trigger="click">
<CollapseIcon class={["w-[16px]", iconClass.value]} />
</el-dropdown>
</el-tooltip>
<el-divider direction="vertical" />
<el-popover
v-slots={reference}
placement="bottom-start"
popper-style={{ padding: 0 }}
width="160"
trigger="click"
>
<div class={[topClass.value]}>
<el-checkbox
class="!-mr-1"
label="列展示"
v-model={checkAll.value}
indeterminate={isIndeterminate.value}
onChange={value => handleCheckAllChange(value)}
/>
<el-button type="primary" link onClick={() => onReset()}>
</el-button>
</div>
<div class="pt-[6px] pl-[11px]">
<el-checkbox-group
v-model={checkedColumns.value}
onChange={value => handleCheckedColumnsChange(value)}
>
<el-space
direction="vertical"
alignment="flex-start"
size={0}
>
{checkColumnList.map(item => {
return (
<div class="flex items-center">
<DragIcon
class={[
"drag-btn w-[16px] mr-2",
isFixedColumn(item)
? "!cursor-no-drop"
: "!cursor-grab"
]}
onMouseenter={(event: {
preventDefault: () => void;
}) => rowDrop(event)}
/>
<el-checkbox
key={item}
label={item}
onChange={value =>
handleCheckColumnListChange(value, item)
}
>
<span
title={item}
class="inline-block w-[120px] truncate hover:text-text_color_primary"
>
{item}
</span>
</el-checkbox>
</div>
);
})}
</el-space>
</el-checkbox-group>
</div>
</el-popover>
</div>
<el-tooltip
popper-options={{
modifiers: [
{
name: "computeStyles",
options: {
adaptive: false,
enabled: false
}
}
]
}}
placement="top"
virtual-ref={buttonRef.value}
virtual-triggering
trigger="hover"
content="列设置"
/>
</div>
{slots.default({
size: size.value,
dynamicColumns: dynamicColumns.value
})}
</div>
</>
);
}
});

@ -0,0 +1 @@
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M13.79 10.21a1 1 0 0 0 1.42 0 1 1 0 0 0 0-1.42l-2.5-2.5a1 1 0 0 0-.33-.21 1 1 0 0 0-.76 0 1 1 0 0 0-.33.21l-2.5 2.5a1 1 0 0 0 1.42 1.42l.79-.8v5.18l-.79-.8a1 1 0 0 0-1.42 1.42l2.5 2.5a1 1 0 0 0 .33.21.94.94 0 0 0 .76 0 1 1 0 0 0 .33-.21l2.5-2.5a1 1 0 0 0-1.42-1.42l-.79.8V9.41ZM7 4h10a1 1 0 0 0 0-2H7a1 1 0 0 0 0 2Zm10 16H7a1 1 0 0 0 0 2h10a1 1 0 0 0 0-2Z"/></svg>

After

Width:  |  Height:  |  Size: 441 B

@ -0,0 +1 @@
<svg width="32" height="32" fill="currentColor" aria-hidden="true" data-icon="holder" viewBox="64 64 896 896"><path d="M300 276.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97zm0 284a56 56 0 1 0 56-97 56 56 0 0 0-56 97zM640 228a56 56 0 1 0 112 0 56 56 0 0 0-112 0zm0 284a56 56 0 1 0 112 0 56 56 0 0 0-112 0zM300 844.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97zM640 796a56 56 0 1 0 112 0 56 56 0 0 0-112 0z"/></svg>

After

Width:  |  Height:  |  Size: 398 B

@ -0,0 +1 @@
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M22 4V2H2v2h9v14.17l-5.5-5.5-1.42 1.41L12 22l7.92-7.92-1.42-1.41-5.5 5.5V4h9Z"/></svg>

After

Width:  |  Height:  |  Size: 163 B

@ -0,0 +1 @@
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 11A8.1 8.1 0 0 0 4.5 9M4 5v4h4m-4 4a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"/></svg>

After

Width:  |  Height:  |  Size: 235 B

@ -0,0 +1 @@
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M3.34 17a10.018 10.018 0 0 1-.978-2.326 3 3 0 0 0 .002-5.347A9.99 9.99 0 0 1 4.865 4.99a3 3 0 0 0 4.631-2.674 9.99 9.99 0 0 1 5.007.002 3 3 0 0 0 4.632 2.672A9.99 9.99 0 0 1 20.66 7c.433.749.757 1.53.978 2.326a3 3 0 0 0-.002 5.347 9.99 9.99 0 0 1-2.501 4.337 3 3 0 0 0-4.631 2.674 9.99 9.99 0 0 1-5.007-.002 3 3 0 0 0-4.632-2.672A10.018 10.018 0 0 1 3.34 17zm5.66.196a4.993 4.993 0 0 1 2.25 2.77c.499.047 1 .048 1.499.001A4.993 4.993 0 0 1 15 17.197a4.993 4.993 0 0 1 3.525-.565c.29-.408.54-.843.748-1.298A4.993 4.993 0 0 1 18 12c0-1.26.47-2.437 1.273-3.334a8.126 8.126 0 0 0-.75-1.298A4.993 4.993 0 0 1 15 6.804a4.993 4.993 0 0 1-2.25-2.77c-.499-.047-1-.048-1.499-.001A4.993 4.993 0 0 1 9 6.803a4.993 4.993 0 0 1-3.525.565 7.99 7.99 0 0 0-.748 1.298A4.993 4.993 0 0 1 6 12a4.99 4.99 0 0 1-1.273 3.334 8.126 8.126 0 0 0 .75 1.298A4.993 4.993 0 0 1 9 17.196zM12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></svg>

After

Width:  |  Height:  |  Size: 1011 B

@ -0,0 +1,48 @@
<script setup lang="ts">
import DPlayer from "dplayer";
import { onBeforeUnmount, onMounted, reactive, ref } from "vue";
const videoRef = ref();
const videoUrl = ref("");
const highlight = ref([]);
const state = reactive({
instance: null
});
// const props = defineProps({
// video: {
// type: Object,
// // eslint-disable-next-line vue/require-valid-default-prop
// default: {
// url: videoUrl.value
// }
// }
// });
const video = ref({
url: videoUrl.value
})
onMounted(() => {
videoUrl.value = sessionStorage.getItem("video_url");
highlight.value = JSON.parse(sessionStorage.getItem("highlights"));
const player = {
container: videoRef.value,
video: video.value,
theme: "#b7daff", //
loop: false, //
lang: "zh-cn", // 'en', 'zh-cn', 'zh-tw'
// screenshot: true, //
hotkey: true, //
preload: "auto", //
volume: 1, //
autoplay: true,
highlight: highlight.value
};
state.instance = new DPlayer(player);
});
onBeforeUnmount(() => {
state.instance.destroy();
});
</script>
<template>
<div ref="videoRef" />
</template>

@ -0,0 +1,79 @@
<template>
<div class='playerBox'>
<div id="dplayer" ref="videoRef"></div>
</div>
</template>
<script setup>
import Dplayer from 'dplayer';
import {onMounted, onUnmounted, ref} from "vue";
const Isvisible = ref(false)
const highlight = ref("")
const videoRef = ref(null);
onMounted (() =>{
Isvisible.value = true
let seek_time = 0
highlight.value = JSON.parse(sessionStorage.getItem("highlights"))
if (highlight.value instanceof Array) {
seek_time = highlight.value[0].time
}
videoRef.value = new Dplayer({
container: videoRef.value,
video: {
url: sessionStorage.getItem("video_url"),
},
theme: '#b7daff', //
loop: false, //
lang: 'zh-cn', // 'en', 'zh-cn', 'zh-tw'
// screenshot: true, //
hotkey: true, //
preload: 'auto', //
volume: 1, //
autoplay: true,
highlight: highlight.value
})
videoRef.value.seek(seek_time)
},)
onUnmounted(() => {
Isvisible.value = false;
sessionStorage.removeItem("video_url");
sessionStorage.removeItem("highlights");
})
</script>
<style scoped>
#dplayer {
width: 100%;
height: 100%;
margin: 10px auto;
}
:deep(.dplayer-controller .dplayer-icons .dplayer-label){
padding-right: 50px
}
.playerBox {
width: 100%;
height: 80%;
margin: 10px auto;
text-align: right;
overflow: hidden;
}
.nav a {
margin: 100px auto;
font: normal 20px/100px '微软雅黑';
}
</style>

@ -0,0 +1,55 @@
import { App } from "vue";
import axios from "axios";
let config: object = {};
const { VITE_PUBLIC_PATH } = import.meta.env;
const setConfig = (cfg?: unknown) => {
config = Object.assign(config, cfg);
};
const getConfig = (key?: string): ServerConfigs => {
if (typeof key === "string") {
const arr = key.split(".");
if (arr && arr.length) {
let data = config;
arr.forEach(v => {
if (data && typeof data[v] !== "undefined") {
data = data[v];
} else {
data = null;
}
});
return data;
}
}
return config;
};
/** 获取项目动态全局配置 */
export const getServerConfig = async (app: App): Promise<undefined> => {
app.config.globalProperties.$config = getConfig();
return axios({
method: "get",
url: `${VITE_PUBLIC_PATH}serverConfig.json`
})
.then(({ data: config }) => {
let $config = app.config.globalProperties.$config;
// 自动注入项目配置
if (app && $config && typeof config === "object") {
$config = Object.assign($config, config);
app.config.globalProperties.$config = $config;
// 设置全局配置
setConfig($config);
}
return $config;
})
.catch(() => {
throw "请在public文件夹下添加serverConfig.json配置文件";
});
};
/** 本地响应式存储的命名空间 */
const responsiveStorageNameSpace = () => getConfig().ResponsiveStorageNameSpace;
export { getConfig, setConfig, responsiveStorageNameSpace };

@ -0,0 +1,15 @@
import { hasAuth } from "@/router/utils";
import { Directive, type DirectiveBinding } from "vue";
export const auth: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding;
if (value) {
!hasAuth(value) && el.parentNode?.removeChild(el);
} else {
throw new Error(
"[Directive: auth]: need auths! Like v-auth=\"['btn.add','btn.edit']\""
);
}
}
};

@ -0,0 +1,33 @@
import { message } from "@/utils/message";
import { useEventListener } from "@vueuse/core";
import { copyTextToClipboard } from "@pureadmin/utils";
import { Directive, type DirectiveBinding } from "vue";
interface CopyEl extends HTMLElement {
copyValue: string;
}
/** 文本复制指令(默认双击复制) */
export const copy: Directive = {
mounted(el: CopyEl, binding: DirectiveBinding) {
const { value } = binding;
if (value) {
el.copyValue = value;
const arg = binding.arg ?? "dblclick";
// Register using addEventListener on mounted, and removeEventListener automatically on unmounted
useEventListener(el, arg, () => {
const success = copyTextToClipboard(el.copyValue);
success
? message("复制成功", { type: "success" })
: message("复制失败", { type: "error" });
});
} else {
throw new Error(
'[Directive: copy]: need value! Like v-copy="modelValue"'
);
}
},
updated(el: CopyEl, binding: DirectiveBinding) {
el.copyValue = binding.value;
}
};

@ -0,0 +1,3 @@
export * from "./auth";
export * from "./copy";
export * from "./optimize";

@ -0,0 +1,55 @@
import {
isFunction,
isObject,
isArray,
debounce,
throttle
} from "@pureadmin/utils";
import { useEventListener } from "@vueuse/core";
import { Directive, type DirectiveBinding } from "vue";
/** 防抖v-optimize或v-optimize:debounce、节流v-optimize:throttle指令 */
export const optimize: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding;
const optimizeType = binding.arg ?? "debounce";
const type = ["debounce", "throttle"].find(t => t === optimizeType);
if (type) {
if (value && value.event && isFunction(value.fn)) {
let params = value?.params;
if (params) {
if (isArray(params) || isObject(params)) {
params = isObject(params) ? Array.of(params) : params;
} else {
throw new Error(
"[Directive: optimize]: `params` must be an array or object"
);
}
}
// Register using addEventListener on mounted, and removeEventListener automatically on unmounted
useEventListener(
el,
value.event,
type === "debounce"
? debounce(
params ? () => value.fn(...params) : value.fn,
value?.timeout ?? 200,
value?.immediate ?? false
)
: throttle(
params ? () => value.fn(...params) : value.fn,
value?.timeout ?? 1000
)
);
} else {
throw new Error(
"[Directive: optimize]: `event` and `fn` are required, and `fn` must be a function"
);
}
} else {
throw new Error(
"[Directive: optimize]: only `debounce` and `throttle` are supported"
);
}
}
};

@ -0,0 +1,148 @@
<script setup lang="ts">
import { useGlobal } from "@pureadmin/utils";
import backTop from "@/assets/svg/back_top.svg?component";
import { h, computed, Transition, defineComponent } from "vue";
import { usePermissionStoreHook } from "@/store/modules/permission";
const props = defineProps({
fixedHeader: Boolean
});
const { $storage, $config } = useGlobal<GlobalPropertiesApi>();
const keepAlive = computed(() => {
return $config?.KeepAlive;
});
const transitions = computed(() => {
return route => {
return route.meta.transition;
};
});
const hideTabs = computed(() => {
return $storage?.configure.hideTabs;
});
const layout = computed(() => {
return $storage?.layout.layout === "vertical";
});
const getSectionStyle = computed(() => {
return [
hideTabs.value && layout ? "padding-top: 48px;" : "",
!hideTabs.value && layout ? "padding-top: 85px;" : "",
hideTabs.value && !layout.value ? "padding-top: 48px" : "",
!hideTabs.value && !layout.value ? "padding-top: 85px;" : "",
props.fixedHeader ? "" : "padding-top: 0;"
];
});
const transitionMain = defineComponent({
render() {
return h(
Transition,
{
name:
transitions.value(this.route) &&
this.route.meta.transition.enterTransition
? "pure-classes-transition"
: (transitions.value(this.route) &&
this.route.meta.transition.name) ||
"fade-transform",
enterActiveClass:
transitions.value(this.route) &&
`animate__animated ${this.route.meta.transition.enterTransition}`,
leaveActiveClass:
transitions.value(this.route) &&
`animate__animated ${this.route.meta.transition.leaveTransition}`,
mode: "out-in",
appear: true
},
{
default: () => [this.$slots.default()]
}
);
},
props: {
route: {
type: undefined,
required: true
}
}
});
</script>
<template>
<section
:class="[props.fixedHeader ? 'app-main' : 'app-main-nofixed-header']"
:style="getSectionStyle"
>
<router-view>
<template #default="{ Component, route }">
<el-scrollbar v-if="props.fixedHeader">
<el-backtop title="回到顶部" target=".app-main .el-scrollbar__wrap">
<backTop />
</el-backtop>
<transitionMain :route="route">
<keep-alive
v-if="keepAlive"
:include="usePermissionStoreHook().cachePageList"
>
<component
:is="Component"
:key="route.fullPath"
class="main-content"
/>
</keep-alive>
<component
v-else
:is="Component"
:key="route.fullPath"
class="main-content"
/>
</transitionMain>
</el-scrollbar>
<div v-else>
<transitionMain :route="route">
<keep-alive
v-if="keepAlive"
:include="usePermissionStoreHook().cachePageList"
>
<component
:is="Component"
:key="route.fullPath"
class="main-content"
/>
</keep-alive>
<component
v-else
:is="Component"
:key="route.fullPath"
class="main-content"
/>
</transitionMain>
</div>
</template>
</router-view>
</section>
</template>
<style scoped>
.app-main {
position: relative;
width: 100%;
height: 100vh;
overflow-x: hidden;
}
.app-main-nofixed-header {
position: relative;
width: 100%;
min-height: 100vh;
}
.main-content {
margin: 24px;
}
</style>

@ -0,0 +1,133 @@
<script setup lang="ts">
import Search from "./search/index.vue";
import Notice from "./notice/index.vue";
import mixNav from "./sidebar/mixNav.vue";
import { useNav } from "@/layout/hooks/useNav";
import Breadcrumb from "./sidebar/breadCrumb.vue";
import topCollapse from "./sidebar/topCollapse.vue";
import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
import Setting from "@iconify-icons/ri/settings-3-line";
const {
layout,
device,
logout,
onPanel,
pureApp,
username,
userAvatar,
avatarsStyle,
toggleSideBar
} = useNav();
</script>
<template>
<div
class="navbar bg-[#fff] shadow-sm shadow-[rgba(0, 21, 41, 0.08)] dark:shadow-[#0d0d0d]"
>
<topCollapse
v-if="device === 'mobile'"
class="hamburger-container"
:is-active="pureApp.sidebar.opened"
@toggleClick="toggleSideBar"
/>
<Breadcrumb
v-if="layout !== 'mix' && device !== 'mobile'"
class="breadcrumb-container"
/>
<mixNav v-if="layout === 'mix'" />
<div v-if="layout === 'vertical'" class="vertical-header-right">
<!-- 菜单搜索 -->
<Search />
<!-- 通知 -->
<!-- <Notice id="header-notice" />-->
<!-- 退出登录 -->
<el-dropdown trigger="click">
<span class="el-dropdown-link navbar-bg-hover select-none">
<img :src="userAvatar" :style="avatarsStyle" />
<p v-if="username" class="dark:text-white">{{ username }}</p>
</span>
<template #dropdown>
<el-dropdown-menu class="logout">
<el-dropdown-item @click="logout">
<IconifyIconOffline
:icon="LogoutCircleRLine"
style="margin: 5px"
/>
退出系统
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<span
class="set-icon navbar-bg-hover"
title="打开项目配置"
@click="onPanel"
>
<IconifyIconOffline :icon="Setting" />
</span>
</div>
</div>
</template>
<style lang="scss" scoped>
.navbar {
width: 100%;
height: 48px;
overflow: hidden;
.hamburger-container {
float: left;
height: 100%;
line-height: 48px;
cursor: pointer;
}
.vertical-header-right {
display: flex;
align-items: center;
justify-content: flex-end;
min-width: 280px;
height: 48px;
color: #000000d9;
.el-dropdown-link {
display: flex;
align-items: center;
justify-content: space-around;
height: 48px;
padding: 10px;
color: #000000d9;
cursor: pointer;
p {
font-size: 14px;
}
img {
width: 22px;
height: 22px;
border-radius: 50%;
}
}
}
.breadcrumb-container {
float: left;
margin-left: 16px;
}
}
.logout {
max-width: 120px;
::v-deep(.el-dropdown-menu__item) {
display: inline-flex;
flex-wrap: wrap;
min-width: 100%;
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save