feat:新增pure-admin前端

This commit is contained in:
chenchun
2024-08-23 14:31:00 +08:00
parent 556d32729a
commit 4bc2cebd60
579 changed files with 85268 additions and 0 deletions

55
Yi.Pure.Vue3/src/App.vue Normal file
View File

@@ -0,0 +1,55 @@
<template>
<el-config-provider :locale="currentLocale">
<router-view />
<ReDialog />
</el-config-provider>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { checkVersion } from "version-rocket";
import { ElConfigProvider } from "element-plus";
import { ReDialog } from "@/components/ReDialog";
import en from "element-plus/es/locale/lang/en";
import zhCn from "element-plus/es/locale/lang/zh-cn";
import plusEn from "plus-pro-components/es/locale/lang/en";
import plusZhCn from "plus-pro-components/es/locale/lang/zh-cn";
export default defineComponent({
name: "app",
components: {
[ElConfigProvider.name]: ElConfigProvider,
ReDialog
},
computed: {
currentLocale() {
return this.$storage.locale?.locale === "zh"
? { ...zhCn, ...plusZhCn }
: { ...en, ...plusEn };
}
},
beforeCreate() {
const { version, name: title } = __APP_INFO__.pkg;
const { VITE_PUBLIC_PATH, MODE } = import.meta.env;
// https://github.com/guMcrey/version-rocket/blob/main/README.zh-CN.md#api
if (MODE === "production") {
// 版本实时更新检测,只作用于线上环境
checkVersion(
// config
{
// 5分钟检测一次版本
pollingTime: 300000,
localPackageVersion: version,
originVersionFileUrl: `${location.origin}${VITE_PUBLIC_PATH}version.json`
},
// options
{
title,
description: "检测到新版本",
buttonText: "立即更新"
}
);
}
}
});
</script>

View File

@@ -0,0 +1,14 @@
import { http } from "@/utils/http";
type Result = {
success: boolean;
data?: {
/** 列表数据 */
list: Array<any>;
};
};
/** 卡片列表 */
export const getCardList = (data?: object) => {
return http.request<Result>("post", "/get-card-list", { data });
};

View File

@@ -0,0 +1,25 @@
import { http } from "@/utils/http";
type Result = {
success: boolean;
data: Array<any>;
};
/** 地图数据 */
export const mapJson = (params?: object) => {
return http.request<Result>("get", "/get-map-info", { params });
};
/** 文件上传 */
export const formUpload = data => {
return http.request<Result>(
"post",
"https://run.mocky.io/v3/3aa761d7-b0b3-4a03-96b3-6168d4f7467b",
{ data },
{
headers: {
"Content-Type": "multipart/form-data"
}
}
);
};

View File

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

View File

@@ -0,0 +1,85 @@
import { http } from "@/utils/http";
type Result = {
success: boolean;
data?: Array<any>;
};
type ResultTable = {
success: boolean;
data?: {
/** 列表数据 */
list: Array<any>;
/** 总条目数 */
total?: number;
/** 每页显示条目个数 */
pageSize?: number;
/** 当前页数 */
currentPage?: number;
};
};
/** 获取系统管理-用户管理列表 */
export const getUserList = (data?: object) => {
return http.request<ResultTable>("post", "/user", { data });
};
/** 系统管理-用户管理-获取所有角色列表 */
export const getAllRoleList = () => {
return http.request<Result>("get", "/list-all-role");
};
/** 系统管理-用户管理-根据userId获取对应角色id列表userId用户id */
export const getRoleIds = (data?: object) => {
return http.request<Result>("post", "/list-role-ids", { data });
};
/** 获取系统管理-角色管理列表 */
export const getRoleList = (data?: object) => {
return http.request<ResultTable>("post", "/role", { data });
};
/** 获取系统管理-菜单管理列表 */
export const getMenuList = (data?: object) => {
return http.request<Result>("post", "/menu", { data });
};
/** 获取系统管理-部门管理列表 */
export const getDeptList = (data?: object) => {
return http.request<Result>("post", "/dept", { data });
};
/** 获取系统监控-在线用户列表 */
export const getOnlineLogsList = (data?: object) => {
return http.request<ResultTable>("post", "/online-logs", { data });
};
/** 获取系统监控-登录日志列表 */
export const getLoginLogsList = (data?: object) => {
return http.request<ResultTable>("post", "/login-logs", { data });
};
/** 获取系统监控-操作日志列表 */
export const getOperationLogsList = (data?: object) => {
return http.request<ResultTable>("post", "/operation-logs", { data });
};
/** 获取系统监控-系统日志列表 */
export const getSystemLogsList = (data?: object) => {
return http.request<ResultTable>("post", "/system-logs", { data });
};
/** 获取系统监控-系统日志-根据 id 查日志详情 */
export const getSystemLogsDetail = (data?: object) => {
return http.request<Result>("post", "/system-logs-detail", { data });
};
/** 获取角色管理-权限-菜单权限 */
export const getRoleMenu = (data?: object) => {
return http.request<Result>("post", "/role-menu", { data });
};
/** 获取角色管理-权限-菜单权限-根据角色 id 查对应菜单 */
export const getRoleMenuIds = (data?: object) => {
return http.request<Result>("post", "/role-menu-ids", { data });
};

View File

@@ -0,0 +1,89 @@
import { http } from "@/utils/http";
export type UserResult = {
success: boolean;
data: {
/** 头像 */
avatar: string;
/** 用户名 */
username: string;
/** 昵称 */
nickname: string;
/** 当前登录用户的角色 */
roles: Array<string>;
/** 按钮级别权限 */
permissions: 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 type UserInfo = {
/** 头像 */
avatar: string;
/** 用户名 */
username: string;
/** 昵称 */
nickname: string;
/** 邮箱 */
email: string;
/** 联系电话 */
phone: string;
/** 简介 */
description: string;
};
export type UserInfoResult = {
success: boolean;
data: UserInfo;
};
type ResultTable = {
success: boolean;
data?: {
/** 列表数据 */
list: Array<any>;
/** 总条目数 */
total?: number;
/** 每页显示条目个数 */
pageSize?: number;
/** 当前页数 */
currentPage?: number;
};
};
/** 登录 */
export const getLogin = (data?: object) => {
return http.request<UserResult>("post", "/login", { data });
};
/** 刷新`token` */
export const refreshTokenApi = (data?: object) => {
return http.request<RefreshTokenResult>("post", "/refresh-token", { data });
};
/** 账户设置-个人信息 */
export const getMine = (data?: object) => {
return http.request<UserInfoResult>("get", "/mine", { data });
};
/** 账户设置-个人安全日志 */
export const getMineLogs = (data?: object) => {
return http.request<ResultTable>("get", "/mine-logs", { data });
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,27 @@
@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

View File

@@ -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.

Binary file not shown.

View File

@@ -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.109"/><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.665"/><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.1 323.1 0 0 1-107.769-242.852z"/></svg>

After

Width:  |  Height:  |  Size: 706 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: 6.7 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

View File

@@ -0,0 +1 @@
<svg width="32" height="32" viewBox="0 0 48 48"><path fill="#2F88FF" fill-rule="evenodd" stroke="#000" stroke-linejoin="round" stroke-width="4" d="M44 40.836q-7.34-8.96-13.036-10.168t-10.846-.365V41L4 23.545 20.118 7v10.167q9.523.075 16.192 6.833 6.668 6.758 7.69 16.836Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 300 B

View File

@@ -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.9 35.9 0 0 1 8.531-16.32.8.8 0 0 1 1.178 0q.25.27.413.455a35.9 35.9 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 44 0 0 1-6.584-.874m6.698-1.123 1.157.066L12 19.527l1.265-2.53 1.157-.066a42 42 0 0 0 4.227-.454A33.9 33.9 0 0 0 12 4.09a33.9 33.9 0 0 0-6.649 12.387q2.093.334 4.227.454M12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6m0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/></svg>

After

Width:  |  Height:  |  Size: 533 B

View File

@@ -0,0 +1 @@
<svg width="1em" height="1em" fill="none" class="t-icon t-icon-calendar" viewBox="0 0 16 16"><path fill="currentColor" d="M10 3H6V1.5H5V3H3a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-2V1.5h-1zM5 5h1V4h4v1h1V4h2v2H3V4h2zM3 7h10v6H3z"/></svg>

After

Width:  |  Height:  |  Size: 261 B

View File

@@ -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.981"/></svg>

After

Width:  |  Height:  |  Size: 262 B

View File

@@ -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 12M11 1h2v3h-2zm0 19h2v3h-2zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414zm2.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.414zM23 11v2h-3v-2zM4 11v2H1v-2z"/></svg>

After

Width:  |  Height:  |  Size: 435 B

View File

@@ -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-8"/></svg>

After

Width:  |  Height:  |  Size: 351 B

View File

@@ -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.5zM13 3V1h-1v2.5l.5.5H15V3zm-1 9.5V15h1v-2h2v-1h-2.5zM1 12v1h2v2h1v-2.5l-.5-.5zm11-1.5-.5.5h-7l-.5-.5v-5l.5-.5h7l.5.5zM10 7H6v2h4z"/></svg>

After

Width:  |  Height:  |  Size: 327 B

View File

@@ -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 12h10V4H3zm2-6h6v4H5zM2 6H1V2.5l.5-.5H5v1H2zm13-3.5V6h-1V3h-3V2h3.5zM14 10h1v3.5l-.5.5H11v-1h3zM2 13h3v1H1.5l-.5-.5V10h1z"/></svg>

After

Width:  |  Height:  |  Size: 302 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="true" class="globalization" viewBox="0 0 512 512"><path fill="currentColor" d="m478.33 433.6-90-218a22 22 0 0 0-40.67 0l-90 218a22 22 0 1 0 40.67 16.79L316.66 406h102.67l18.33 44.39A22 22 0 0 0 458 464a22 22 0 0 0 20.32-30.4zM334.83 362 368 281.65 401.17 362zm-66.99-19.08a22 22 0 0 0-4.89-30.7c-.2-.15-15-11.13-36.49-34.73 39.65-53.68 62.11-114.75 71.27-143.49H330a22 22 0 0 0 0-44H214V70a22 22 0 0 0-44 0v20H54a22 22 0 0 0 0 44h197.25c-9.52 26.95-27.05 69.5-53.79 108.36-31.41-41.68-43.08-68.65-43.17-68.87a22 22 0 0 0-40.58 17c.58 1.38 14.55 34.23 52.86 83.93.92 1.19 1.83 2.35 2.74 3.51-39.24 44.35-77.74 71.86-93.85 80.74a22 22 0 1 0 21.07 38.63c2.16-1.18 48.6-26.89 101.63-85.59 22.52 24.08 38 35.44 38.93 36.1a22 22 0 0 0 30.75-4.9z"/></svg>

After

Width:  |  Height:  |  Size: 826 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 1024 1024"><path fill="#FF5D50" d="M428.698 107.315c-6.503 72.192-36.352 207.258-160.256 337.408 3.686-48.025-7.117-83.763-19.047-107.673-6.605-13.159-26.06-10.599-28.877 3.84-5.734 29.44-20.582 75.059-57.6 137.779-71.628 121.395-62.566 459.878 340.736 459.878S934.093 585.728 876.8 442.522c-37.376-93.44-93.952-152.525-128.82-182.324-11.417-9.779-29.132-1.945-29.593 13.056-.921 30.464-7.321 73.37-33.075 102.144-.666-52.787-38.144-208.384-202.445-296.857-23.296-12.544-51.763 2.457-54.17 28.774z"/><path fill="#FFDF99" d="M702.26 678.4c-4.2-45.056-60.673-166.554-212.634-246.426-10.599-5.58-23.092 3.124-21.504 15.002 6.246 46.848 12.953 140.493-24.064 184.73 4.044-40.397-18.125-73.83-36.66-94.31-8.396-9.217-23.552-4.66-25.497 7.68-3.533 22.322-12.851 56.268-36.557 97.945-42.086 74.035-86.989 188.672 124.57 294.656 10.956.563 22.17.87 33.74.87a618 618 0 0 0 32.717-.87C694.631 878.182 709.837 759.706 702.26 678.4"/></svg>

After

Width:  |  Height:  |  Size: 1004 B

View File

@@ -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 7h6v2H3v2h4v2H3v2h4v2H1zm10 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-2"/></svg>

After

Width:  |  Height:  |  Size: 379 B

View File

@@ -0,0 +1 @@
<svg width="1em" height="1em" fill="none" class="t-icon t-icon-laptop" viewBox="0 0 16 16"><path fill="currentColor" d="M2.5 12a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h11a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1zm0-1h11V4h-11zM15 13H1v1h14z"/></svg>

After

Width:  |  Height:  |  Size: 228 B

View File

@@ -0,0 +1 @@
<svg width="1em" height="1em" fill="none" class="t-icon t-icon-service" viewBox="0 0 16 16"><path fill="currentColor" d="M2.52 6.37a5.5 5.5 0 0 1 10.98.13v4c0 .05 0 .1-.02.15A4.5 4.5 0 0 1 9 14.7H8v-1h1a3.5 3.5 0 0 0 3.4-2.7h-1.9a.5.5 0 0 1-.5-.5v-4c0-.28.22-.5.5-.5h1.93a4.5 4.5 0 0 0-8.86 0H5.5c.28 0 .5.22.5.5v4a.5.5 0 0 1-.5.5H3a.5.5 0 0 1-.5-.5v-4c0-.04 0-.09.02-.13M12.5 7H11v3h1.5zm-9 0v3H5V7z"/></svg>

After

Width:  |  Height:  |  Size: 409 B

View File

@@ -0,0 +1 @@
<svg width="1em" height="1em" fill="none" class="t-icon t-icon-shop" viewBox="0 0 16 16"><path fill="currentColor" d="M8 1a2.5 2.5 0 0 0-2.5 2.5V5h-2a.5.5 0 0 0-.5.5v9c0 .28.22.5.5.5h9a.5.5 0 0 0 .5-.5v-9a.5.5 0 0 0-.5-.5h-2V3.5A2.5 2.5 0 0 0 8 1m1.5 5v2h1V6H12v8H4V6h1.5v2h1V6zm0-1h-3V3.5a1.5 1.5 0 1 1 3 0z"/></svg>

After

Width:  |  Height:  |  Size: 317 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="icon" viewBox="0 0 1024 1024"><path d="M554 849.574c0 23.365-18.635 42.307-42 42.307s-42-18.941-42-42.307V662.719c0-23.365 18.635-42.307 42-42.307v-7.051c23.365 0 42 25.993 42 49.358z"/><path d="M893 888.5c0 17.397-14.103 31.5-31.5 31.5h-700c-17.397 0-31.5-14.103-31.5-31.5s14.103-31.5 31.5-31.5h700c17.397 0 31.5 14.103 31.5 31.5m33-714.074C926 135.484 894.686 105 855.744 105H168.256C129.314 105 98 135.484 98 174.426V533h828zM98 630.988C98 669.931 129.314 702 168.256 702h687.488C894.686 702 926 669.931 926 630.988V596H98z"/></svg>

After

Width:  |  Height:  |  Size: 605 B

View File

@@ -0,0 +1 @@
<svg width="1em" height="1em" fill="none" class="t-icon t-icon-user-avatar" viewBox="0 0 16 16"><path fill="currentColor" d="M8 10.5c1.24 0 2.42.31 3.5.88v1.12h1v-1.14a.94.94 0 0 0-.49-.84 8.48 8.48 0 0 0-8.02 0 .94.94 0 0 0-.49.84v1.14h1v-1.12A7.5 7.5 0 0 1 8 10.5M10.5 6a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0m-1 0a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0"/><path fill="currentColor" d="M2.5 1.5a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1v-11a1 1 0 0 0-1-1zm11 1v11h-11v-11z"/></svg>

After

Width:  |  Height:  |  Size: 482 B

View File

@@ -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 2m10 16H7a1 1 0 0 0 0 2h10a1 1 0 0 0 0-2"/></svg>

After

Width:  |  Height:  |  Size: 439 B

View File

@@ -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 97m0 284a56 56 0 1 0 56-97 56 56 0 0 0-56 97M640 228a56 56 0 1 0 112 0 56 56 0 0 0-112 0m0 284a56 56 0 1 0 112 0 56 56 0 0 0-112 0M300 844.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97M640 796a56 56 0 1 0 112 0 56 56 0 0 0-112 0"/></svg>

After

Width:  |  Height:  |  Size: 392 B

View File

@@ -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.5V4z"/></svg>

After

Width:  |  Height:  |  Size: 161 B

View File

@@ -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

View File

@@ -0,0 +1 @@
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M3.34 17a10 10 0 0 1-.978-2.326 3 3 0 0 0 .002-5.347A10 10 0 0 1 4.865 4.99a3 3 0 0 0 4.631-2.674 10 10 0 0 1 5.007.002 3 3 0 0 0 4.632 2.672A10 10 0 0 1 20.66 7c.433.749.757 1.53.978 2.326a3 3 0 0 0-.002 5.347 10 10 0 0 1-2.501 4.337 3 3 0 0 0-4.631 2.674 10 10 0 0 1-5.007-.002 3 3 0 0 0-4.632-2.672A10 10 0 0 1 3.34 17m5.66.196a5 5 0 0 1 2.25 2.77q.75.071 1.499.001A5 5 0 0 1 15 17.197a5 5 0 0 1 3.525-.565q.435-.614.748-1.298A5 5 0 0 1 18 12c0-1.26.47-2.437 1.273-3.334a8 8 0 0 0-.75-1.298A5 5 0 0 1 15 6.804a5 5 0 0 1-2.25-2.77q-.75-.071-1.499-.001A5 5 0 0 1 9 6.803a5 5 0 0 1-3.525.565 8 8 0 0 0-.748 1.298A5 5 0 0 1 6 12a5 5 0 0 1-1.273 3.334 8 8 0 0 0 .75 1.298A5 5 0 0 1 9 17.196M12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6m0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/></svg>

After

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1,7 @@
import { withInstall } from "@pureadmin/utils";
import reAnimateSelector from "./src/index.vue";
/** [animate.css](https://animate.style/) 选择器组件 */
export const ReAnimateSelector = withInstall(reAnimateSelector);
export default ReAnimateSelector;

View File

@@ -0,0 +1,114 @@
export const animates = [
/* Attention seekers */
"bounce",
"flash",
"pulse",
"rubberBand",
"shakeX",
"headShake",
"swing",
"tada",
"wobble",
"jello",
"heartBeat",
/* Back entrances */
"backInDown",
"backInLeft",
"backInRight",
"backInUp",
/* Back exits */
"backOutDown",
"backOutLeft",
"backOutRight",
"backOutUp",
/* Bouncing entrances */
"bounceIn",
"bounceInDown",
"bounceInLeft",
"bounceInRight",
"bounceInUp",
/* Bouncing exits */
"bounceOut",
"bounceOutDown",
"bounceOutLeft",
"bounceOutRight",
"bounceOutUp",
/* Fading entrances */
"fadeIn",
"fadeInDown",
"fadeInDownBig",
"fadeInLeft",
"fadeInLeftBig",
"fadeInRight",
"fadeInRightBig",
"fadeInUp",
"fadeInUpBig",
"fadeInTopLeft",
"fadeInTopRight",
"fadeInBottomLeft",
"fadeInBottomRight",
/* Fading exits */
"fadeOut",
"fadeOutDown",
"fadeOutDownBig",
"fadeOutLeft",
"fadeOutLeftBig",
"fadeOutRight",
"fadeOutRightBig",
"fadeOutUp",
"fadeOutUpBig",
"fadeOutTopLeft",
"fadeOutTopRight",
"fadeOutBottomRight",
"fadeOutBottomLeft",
/* Flippers */
"flip",
"flipInX",
"flipInY",
"flipOutX",
"flipOutY",
/* Lightspeed */
"lightSpeedInRight",
"lightSpeedInLeft",
"lightSpeedOutRight",
"lightSpeedOutLeft",
/* Rotating entrances */
"rotateIn",
"rotateInDownLeft",
"rotateInDownRight",
"rotateInUpLeft",
"rotateInUpRight",
/* Rotating exits */
"rotateOut",
"rotateOutDownLeft",
"rotateOutDownRight",
"rotateOutUpLeft",
"rotateOutUpRight",
/* Specials */
"hinge",
"jackInTheBox",
"rollIn",
"rollOut",
/* Zooming entrances */
"zoomIn",
"zoomInDown",
"zoomInLeft",
"zoomInRight",
"zoomInUp",
/* Zooming exits */
"zoomOut",
"zoomOutDown",
"zoomOutLeft",
"zoomOutRight",
"zoomOutUp",
/* Sliding entrances */
"slideInDown",
"slideInLeft",
"slideInRight",
"slideInUp",
/* Sliding exits */
"slideOutDown",
"slideOutLeft",
"slideOutRight",
"slideOutUp"
];

View File

@@ -0,0 +1,136 @@
<script setup lang="ts">
import { ref, computed } from "vue";
import { animates } from "./animate";
import { cloneDeep } from "@pureadmin/utils";
defineOptions({
name: "ReAnimateSelector"
});
defineProps({
placeholder: {
type: String,
default: "请选择动画"
}
});
const inputValue = defineModel({ type: String });
const searchVal = ref();
const animatesList = ref(animates);
const copyAnimatesList = cloneDeep(animatesList);
const animateClass = computed(() => {
return [
"mt-1",
"flex",
"border",
"w-[130px]",
"h-[100px]",
"items-center",
"cursor-pointer",
"transition-all",
"justify-center",
"border-[#e5e7eb]",
"hover:text-primary",
"hover:duration-[700ms]"
];
});
const animateStyle = computed(
() => (i: string) =>
inputValue.value === i
? {
borderColor: "var(--el-color-primary)",
color: "var(--el-color-primary)"
}
: ""
);
function onChangeIcon(animate: string) {
inputValue.value = animate;
}
function onClear() {
inputValue.value = "";
}
function filterMethod(value: any) {
searchVal.value = value;
animatesList.value = copyAnimatesList.value.filter((i: string | any[]) =>
i.includes(value)
);
}
const animateMap = ref({});
function onMouseEnter(index: string | number) {
animateMap.value[index] = animateMap.value[index]?.loading
? Object.assign({}, animateMap.value[index], {
loading: false
})
: Object.assign({}, animateMap.value[index], {
loading: true
});
}
function onMouseleave() {
animateMap.value = {};
}
</script>
<template>
<el-select
clearable
filterable
:placeholder="placeholder"
popper-class="pure-animate-popper"
:model-value="inputValue"
:filter-method="filterMethod"
@clear="onClear"
>
<template #empty>
<div class="w-[280px]">
<el-scrollbar
noresize
height="212px"
:view-style="{ overflow: 'hidden' }"
class="border-t border-[#e5e7eb]"
>
<ul class="flex flex-wrap justify-around mb-1">
<li
v-for="(animate, index) in animatesList"
:key="index"
:class="animateClass"
:style="animateStyle(animate)"
@mouseenter.prevent="onMouseEnter(index)"
@mouseleave.prevent="onMouseleave"
@click="onChangeIcon(animate)"
>
<h4
:class="[
`animate__animated animate__${
animateMap[index]?.loading
? animate + ' animate__infinite'
: ''
} `
]"
>
{{ animate }}
</h4>
</li>
</ul>
<el-empty
v-show="animatesList.length === 0"
:description="`${searchVal} 动画不存在`"
:image-size="60"
/>
</el-scrollbar>
</div>
</template>
</el-select>
</template>
<style>
.pure-animate-popper {
min-width: 0 !important;
}
</style>

View File

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

View File

@@ -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;
};
}
});

View File

@@ -0,0 +1,7 @@
import { withInstall } from "@pureadmin/utils";
import reBarcode from "./src/index.vue";
/** 条形码组件 */
export const ReBarcode = withInstall(reBarcode);
export default ReBarcode;

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
import JsBarcode from "jsbarcode";
import { ref, onMounted } from "vue";
defineOptions({
name: "ReBarcode"
});
const props = defineProps({
tag: {
type: String,
default: "canvas"
},
text: {
type: String,
default: null
},
// 完整配置 https://github.com/lindell/JsBarcode/wiki/Options
options: {
type: Object,
default() {
return {};
}
},
// type 相当于 options.format如果 type 和 options.format 同时存在type 值优先;
type: {
type: String,
default: "CODE128"
}
});
const wrapEl = ref(null);
onMounted(() => {
const opt = { ...props.options, format: props.type };
JsBarcode(wrapEl.value, props.text, opt);
});
</script>
<template>
<component :is="tag" ref="wrapEl" />
</template>

View File

@@ -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() }
);
}
});

View File

@@ -0,0 +1,2 @@
normal 普通数字动画组件
rebound 回弹式数字动画组件

View File

@@ -0,0 +1,11 @@
import reNormalCountTo from "./src/normal";
import reboundCountTo from "./src/rebound";
import { withInstall } from "@pureadmin/utils";
/** 普通数字动画组件 */
const ReNormalCountTo = withInstall(reNormalCountTo);
/** 回弹式数字动画组件 */
const ReboundCountTo = withInstall(reboundCountTo);
export { ReNormalCountTo, ReboundCountTo };

View File

@@ -0,0 +1,179 @@
import {
watch,
unref,
computed,
reactive,
onMounted,
defineComponent
} from "vue";
import { countToProps } from "./props";
import { isNumber } from "@pureadmin/utils";
export default defineComponent({
name: "ReNormalCountTo",
props: countToProps,
emits: ["mounted", "callback"],
setup(props, { emit }) {
const state = reactive<{
localStartVal: number;
printVal: number | null;
displayValue: string;
paused: boolean;
localDuration: number | null;
startTime: number | null;
timestamp: number | null;
rAF: any;
remaining: number | null;
color: string;
fontSize: string;
}>({
localStartVal: props.startVal,
displayValue: formatNumber(props.startVal),
printVal: null,
paused: false,
localDuration: props.duration,
startTime: null,
timestamp: null,
remaining: null,
rAF: null,
color: null,
fontSize: "16px"
});
const getCountDown = computed(() => {
return props.startVal > props.endVal;
});
watch([() => props.startVal, () => props.endVal], () => {
if (props.autoplay) {
start();
}
});
function start() {
const { startVal, duration, color, fontSize } = props;
state.localStartVal = startVal;
state.startTime = null;
state.localDuration = duration;
state.paused = false;
state.color = color;
state.fontSize = fontSize;
state.rAF = requestAnimationFrame(count);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function pauseResume() {
if (state.paused) {
resume();
state.paused = false;
} else {
pause();
state.paused = true;
}
}
function pause() {
cancelAnimationFrame(state.rAF);
}
function resume() {
state.startTime = null;
state.localDuration = +(state.remaining as number);
state.localStartVal = +(state.printVal as number);
requestAnimationFrame(count);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function reset() {
state.startTime = null;
cancelAnimationFrame(state.rAF);
state.displayValue = formatNumber(props.startVal);
}
function count(timestamp: number) {
const { useEasing, easingFn, endVal } = props;
if (!state.startTime) state.startTime = timestamp;
state.timestamp = timestamp;
const progress = timestamp - state.startTime;
state.remaining = (state.localDuration as number) - progress;
if (useEasing) {
if (unref(getCountDown)) {
state.printVal =
state.localStartVal -
easingFn(
progress,
0,
state.localStartVal - endVal,
state.localDuration as number
);
} else {
state.printVal = easingFn(
progress,
state.localStartVal,
endVal - state.localStartVal,
state.localDuration as number
);
}
} else {
if (unref(getCountDown)) {
state.printVal =
state.localStartVal -
(state.localStartVal - endVal) *
(progress / (state.localDuration as number));
} else {
state.printVal =
state.localStartVal +
(endVal - state.localStartVal) *
(progress / (state.localDuration as number));
}
}
if (unref(getCountDown)) {
state.printVal = state.printVal < endVal ? endVal : state.printVal;
} else {
state.printVal = state.printVal > endVal ? endVal : state.printVal;
}
state.displayValue = formatNumber(state.printVal);
if (progress < (state.localDuration as number)) {
state.rAF = requestAnimationFrame(count);
} else {
emit("callback");
}
}
function formatNumber(num: number | string) {
const { decimals, decimal, separator, suffix, prefix } = props;
num = Number(num).toFixed(decimals);
num += "";
const x = num.split(".");
let x1 = x[0];
const x2 = x.length > 1 ? decimal + x[1] : "";
const rgx = /(\d+)(\d{3})/;
if (separator && !isNumber(separator)) {
while (rgx.test(x1)) {
x1 = x1.replace(rgx, "$1" + separator + "$2");
}
}
return prefix + x1 + x2 + suffix;
}
onMounted(() => {
if (props.autoplay) {
start();
}
emit("mounted");
});
return () => (
<>
<span
style={{
color: props.color,
fontSize: props.fontSize
}}
>
{state.displayValue}
</span>
</>
);
}
});

View File

@@ -0,0 +1,32 @@
import type { PropType } from "vue";
import propTypes from "@/utils/propTypes";
export const countToProps = {
startVal: propTypes.number.def(0),
endVal: propTypes.number.def(2020),
duration: propTypes.number.def(1300),
autoplay: propTypes.bool.def(true),
decimals: {
type: Number as PropType<number>,
required: false,
default: 0,
validator(value: number) {
return value >= 0;
}
},
color: propTypes.string.def(),
fontSize: propTypes.string.def(),
decimal: propTypes.string.def("."),
separator: propTypes.string.def(","),
prefix: propTypes.string.def(""),
suffix: propTypes.string.def(""),
useEasing: propTypes.bool.def(true),
easingFn: {
type: Function as PropType<
(t: number, b: number, c: number, d: number) => number
>,
default(t: number, b: number, c: number, d: number) {
return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b;
}
}
};

View File

@@ -0,0 +1,72 @@
import "./rebound.css";
import {
ref,
unref,
onBeforeMount,
defineComponent,
onBeforeUnmount
} from "vue";
import { reboundProps } from "./props";
export default defineComponent({
name: "ReboundCountTo",
props: reboundProps,
setup(props) {
const ulRef = ref();
const timer = ref(null);
onBeforeMount(() => {
const ua = navigator.userAgent.toLowerCase();
const testUA = regexp => regexp.test(ua);
const isSafari = testUA(/safari/g) && !testUA(/chrome/g);
// Safari浏览器的兼容代码
isSafari &&
(timer.value = setTimeout(() => {
ulRef.value.setAttribute(
"style",
`
animation: none;
transform: translateY(calc(var(--i) * -9.09%))
`
);
}, props.delay * 1000));
});
onBeforeUnmount(() => {
clearTimeout(unref(timer));
});
return () => (
<>
<div
class="scroll-num"
style={{ "--i": props.i, "--delay": props.delay }}
>
<ul ref="ulRef" style={{ fontSize: "32px" }}>
<li>0</li>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
<li>0</li>
</ul>
<svg width="0" height="0">
<filter id="blur">
<feGaussianBlur
in="SourceGraphic"
stdDeviation={`0 ${props.blur}`}
/>
</filter>
</svg>
</div>
</>
);
}
});

View File

@@ -0,0 +1,15 @@
import type { PropType } from "vue";
import propTypes from "@/utils/propTypes";
export const reboundProps = {
delay: propTypes.number.def(1),
blur: propTypes.number.def(2),
i: {
type: Number as PropType<number>,
required: false,
default: 0,
validator(value: number) {
return value < 10 && value >= 0 && Number.isInteger(value);
}
}
};

View File

@@ -0,0 +1,77 @@
.scroll-num {
width: var(--width, 20px);
height: var(--height, calc(var(--width, 20px) * 1.8));
color: var(--color, #333);
font-size: var(--height, calc(var(--width, 20px) * 1.1));
line-height: var(--height, calc(var(--width, 20px) * 1.8));
text-align: center;
overflow: hidden;
animation: enhance-bounce-in-down 1s calc(var(--delay) * 1s) forwards;
}
ul {
animation:
move 0.3s linear infinite,
bounce-in-down 1s calc(var(--delay) * 1s) forwards;
}
@keyframes move {
from {
transform: translateY(-90%);
filter: url(#blur);
}
to {
transform: translateY(1%);
filter: url(#blur);
}
}
@keyframes bounce-in-down {
from {
transform: translateY(calc(var(--i) * -9.09% - 7%));
filter: none;
}
25% {
transform: translateY(calc(var(--i) * -9.09% + 3%));
}
50% {
transform: translateY(calc(var(--i) * -9.09% - 1%));
}
70% {
transform: translateY(calc(var(--i) * -9.09% + 0.6%));
}
85% {
transform: translateY(calc(var(--i) * -9.09% - 0.3%));
}
to {
transform: translateY(calc(var(--i) * -9.09%));
}
}
@keyframes enhance-bounce-in-down {
25% {
transform: translateY(8%);
}
50% {
transform: translateY(-4%);
}
70% {
transform: translateY(2%);
}
85% {
transform: translateY(-1%);
}
to {
transform: translateY(0);
}
}

View File

@@ -0,0 +1,7 @@
import reCropper from "./src";
import { withInstall } from "@pureadmin/utils";
/** 图片裁剪组件 */
export const ReCropper = withInstall(reCropper);
export default ReCropper;

View File

@@ -0,0 +1,8 @@
@import "cropperjs/dist/cropper.css";
.re-circled {
.cropper-view-box,
.cropper-face {
border-radius: 50%;
}
}

View File

@@ -0,0 +1,457 @@
import "./circled.css";
import Cropper from "cropperjs";
import { ElUpload } from "element-plus";
import type { CSSProperties } from "vue";
import { useEventListener } from "@vueuse/core";
import { longpress } from "@/directives/longpress";
import { useTippy, directive as tippy } from "vue-tippy";
import {
type PropType,
ref,
unref,
computed,
onMounted,
onUnmounted,
defineComponent
} from "vue";
import {
delay,
debounce,
isArray,
downloadByBase64,
useResizeObserver
} from "@pureadmin/utils";
import {
Reload,
Upload,
ArrowH,
ArrowV,
ArrowUp,
ArrowDown,
ArrowLeft,
ChangeIcon,
ArrowRight,
RotateLeft,
SearchPlus,
RotateRight,
SearchMinus,
DownloadIcon
} from "./svg";
type Options = Cropper.Options;
const defaultOptions: Options = {
aspectRatio: 1,
zoomable: true,
zoomOnTouch: true,
zoomOnWheel: true,
cropBoxMovable: true,
cropBoxResizable: true,
toggleDragModeOnDblclick: true,
autoCrop: true,
background: true,
highlight: true,
center: true,
responsive: true,
restore: true,
checkCrossOrigin: true,
checkOrientation: true,
scalable: true,
modal: true,
guides: true,
movable: true,
rotatable: true
};
const props = {
src: { type: String, required: true },
alt: { type: String },
circled: { type: Boolean, default: false },
/** 是否可以通过点击裁剪区域关闭右键弹出的功能菜单,默认 `true` */
isClose: { type: Boolean, default: true },
realTimePreview: { type: Boolean, default: true },
height: { type: [String, Number], default: "360px" },
crossorigin: {
type: String as PropType<"" | "anonymous" | "use-credentials" | undefined>,
default: undefined
},
imageStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) },
options: { type: Object as PropType<Options>, default: () => ({}) }
};
export default defineComponent({
name: "ReCropper",
props,
setup(props, { attrs, emit }) {
const tippyElRef = ref<ElRef<HTMLImageElement>>();
const imgElRef = ref<ElRef<HTMLImageElement>>();
const cropper = ref<Nullable<Cropper>>();
const inCircled = ref(props.circled);
const isInClose = ref(props.isClose);
const inSrc = ref(props.src);
const isReady = ref(false);
const imgBase64 = ref();
let scaleX = 1;
let scaleY = 1;
const debounceRealTimeCroppered = debounce(realTimeCroppered, 80);
const getImageStyle = computed((): CSSProperties => {
return {
height: props.height,
maxWidth: "100%",
...props.imageStyle
};
});
const getClass = computed(() => {
return [
attrs.class,
{
["re-circled"]: inCircled.value
}
];
});
const iconClass = computed(() => {
return [
"p-[6px]",
"h-[30px]",
"w-[30px]",
"outline-none",
"rounded-[4px]",
"cursor-pointer",
"hover:bg-[rgba(0,0,0,0.06)]"
];
});
const getWrapperStyle = computed((): CSSProperties => {
return { height: `${props.height}`.replace(/px/, "") + "px" };
});
onMounted(init);
onUnmounted(() => {
cropper.value?.destroy();
isReady.value = false;
cropper.value = null;
imgBase64.value = "";
scaleX = 1;
scaleY = 1;
});
useResizeObserver(tippyElRef, () => handCropper("reset"));
async function init() {
const imgEl = unref(imgElRef);
if (!imgEl) return;
cropper.value = new Cropper(imgEl, {
...defaultOptions,
ready: () => {
isReady.value = true;
realTimeCroppered();
delay(400).then(() => emit("readied", cropper.value));
},
crop() {
debounceRealTimeCroppered();
},
zoom() {
debounceRealTimeCroppered();
},
cropmove() {
debounceRealTimeCroppered();
},
...props.options
});
}
function realTimeCroppered() {
props.realTimePreview && croppered();
}
function croppered() {
if (!cropper.value) return;
const canvas = inCircled.value
? getRoundedCanvas()
: cropper.value.getCroppedCanvas();
// https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLCanvasElement/toBlob
canvas.toBlob(blob => {
if (!blob) return;
const fileReader: FileReader = new FileReader();
fileReader.readAsDataURL(blob);
fileReader.onloadend = e => {
if (!e.target?.result || !blob) return;
imgBase64.value = e.target.result;
emit("cropper", {
base64: e.target.result,
blob,
info: { size: blob.size, ...cropper.value.getData() }
});
};
fileReader.onerror = () => {
emit("error");
};
});
}
function getRoundedCanvas() {
const sourceCanvas = cropper.value!.getCroppedCanvas();
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d")!;
const width = sourceCanvas.width;
const height = sourceCanvas.height;
canvas.width = width;
canvas.height = height;
context.imageSmoothingEnabled = true;
context.drawImage(sourceCanvas, 0, 0, width, height);
context.globalCompositeOperation = "destination-in";
context.beginPath();
context.arc(
width / 2,
height / 2,
Math.min(width, height) / 2,
0,
2 * Math.PI,
true
);
context.fill();
return canvas;
}
function handCropper(event: string, arg?: number | Array<number>) {
if (event === "scaleX") {
scaleX = arg = scaleX === -1 ? 1 : -1;
}
if (event === "scaleY") {
scaleY = arg = scaleY === -1 ? 1 : -1;
}
arg && isArray(arg)
? cropper.value?.[event]?.(...arg)
: cropper.value?.[event]?.(arg);
}
function beforeUpload(file) {
const reader = new FileReader();
reader.readAsDataURL(file);
inSrc.value = "";
reader.onload = e => {
inSrc.value = e.target?.result as string;
};
reader.onloadend = () => {
init();
};
return false;
}
const menuContent = defineComponent({
directives: {
tippy,
longpress
},
setup() {
return () => (
<div class="flex flex-wrap w-[60px] justify-between">
<ElUpload
accept="image/*"
show-file-list={false}
before-upload={beforeUpload}
>
<Upload
class={iconClass.value}
v-tippy={{
content: "上传",
placement: "left-start"
}}
/>
</ElUpload>
<DownloadIcon
class={iconClass.value}
v-tippy={{
content: "下载",
placement: "right-start"
}}
onClick={() => downloadByBase64(imgBase64.value, "cropping.png")}
/>
<ChangeIcon
class={iconClass.value}
v-tippy={{
content: "圆形、矩形裁剪",
placement: "left-start"
}}
onClick={() => {
inCircled.value = !inCircled.value;
realTimeCroppered();
}}
/>
<Reload
class={iconClass.value}
v-tippy={{
content: "重置",
placement: "right-start"
}}
onClick={() => handCropper("reset")}
/>
<ArrowUp
class={iconClass.value}
v-tippy={{
content: "上移(可长按)",
placement: "left-start"
}}
v-longpress={[() => handCropper("move", [0, -10]), "0:100"]}
/>
<ArrowDown
class={iconClass.value}
v-tippy={{
content: "下移(可长按)",
placement: "right-start"
}}
v-longpress={[() => handCropper("move", [0, 10]), "0:100"]}
/>
<ArrowLeft
class={iconClass.value}
v-tippy={{
content: "左移(可长按)",
placement: "left-start"
}}
v-longpress={[() => handCropper("move", [-10, 0]), "0:100"]}
/>
<ArrowRight
class={iconClass.value}
v-tippy={{
content: "右移(可长按)",
placement: "right-start"
}}
v-longpress={[() => handCropper("move", [10, 0]), "0:100"]}
/>
<ArrowH
class={iconClass.value}
v-tippy={{
content: "水平翻转",
placement: "left-start"
}}
onClick={() => handCropper("scaleX", -1)}
/>
<ArrowV
class={iconClass.value}
v-tippy={{
content: "垂直翻转",
placement: "right-start"
}}
onClick={() => handCropper("scaleY", -1)}
/>
<RotateLeft
class={iconClass.value}
v-tippy={{
content: "逆时针旋转",
placement: "left-start"
}}
onClick={() => handCropper("rotate", -45)}
/>
<RotateRight
class={iconClass.value}
v-tippy={{
content: "顺时针旋转",
placement: "right-start"
}}
onClick={() => handCropper("rotate", 45)}
/>
<SearchPlus
class={iconClass.value}
v-tippy={{
content: "放大(可长按)",
placement: "left-start"
}}
v-longpress={[() => handCropper("zoom", 0.1), "0:100"]}
/>
<SearchMinus
class={iconClass.value}
v-tippy={{
content: "缩小(可长按)",
placement: "right-start"
}}
v-longpress={[() => handCropper("zoom", -0.1), "0:100"]}
/>
</div>
);
}
});
function onContextmenu(event) {
event.preventDefault();
const { show, setProps, destroy, state } = useTippy(tippyElRef, {
content: menuContent,
arrow: false,
theme: "light",
trigger: "manual",
interactive: true,
appendTo: "parent",
// hideOnClick: false,
placement: "bottom-end"
});
setProps({
getReferenceClientRect: () => ({
width: 0,
height: 0,
top: event.clientY,
bottom: event.clientY,
left: event.clientX,
right: event.clientX
})
});
show();
if (isInClose.value) {
if (!state.value.isShown && !state.value.isVisible) return;
useEventListener(tippyElRef, "click", destroy);
}
}
return {
inSrc,
props,
imgElRef,
tippyElRef,
getClass,
getWrapperStyle,
getImageStyle,
isReady,
croppered,
onContextmenu
};
},
render() {
const {
inSrc,
isReady,
getClass,
getImageStyle,
onContextmenu,
getWrapperStyle
} = this;
const { alt, crossorigin } = this.props;
return inSrc ? (
<div
ref="tippyElRef"
class={getClass}
style={getWrapperStyle}
onContextmenu={event => onContextmenu(event)}
>
<img
v-show={isReady}
ref="imgElRef"
style={getImageStyle}
src={inSrc}
alt={alt}
crossorigin={crossorigin}
/>
</div>
) : null;
}
});

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M862 465.3h-81c-4.6 0-9 2-12.1 5.5L550 723.1V160c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v563.1L255.1 470.8c-3-3.5-7.4-5.5-12.1-5.5h-81c-6.8 0-10.5 8.1-6 13.2L487.9 861a31.96 31.96 0 0 0 48.3 0L868 478.5c4.5-5.2.8-13.2-6-13.2"/></svg>

After

Width:  |  Height:  |  Size: 346 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="m296.992 216.992-272 272L3.008 512l21.984 23.008 272 272 46.016-46.016L126.016 544h772L680.992 760.992l46.016 46.016 272-272L1020.992 512l-21.984-23.008-272-272-46.048 46.048L898.016 480h-772l216.96-216.992z"/></svg>

After

Width:  |  Height:  |  Size: 325 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M872 474H286.9l350.2-304c5.6-4.9 2.2-14-5.2-14h-88.5c-3.9 0-7.6 1.4-10.5 3.9L155 487.8a31.96 31.96 0 0 0 0 48.3L535.1 866c1.5 1.3 3.3 2 5.2 2h91.5c7.4 0 10.8-9.2 5.2-14L286.9 550H872c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8"/></svg>

After

Width:  |  Height:  |  Size: 343 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M869 487.8 491.2 159.9c-2.9-2.5-6.6-3.9-10.5-3.9h-88.5c-7.4 0-10.8 9.2-5.2 14l350.2 304H152c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h585.1L386.9 854c-5.6 4.9-2.2 14 5.2 14h91.5c1.9 0 3.8-.7 5.2-2L869 536.2a32.07 32.07 0 0 0 0-48.4"/></svg>

After

Width:  |  Height:  |  Size: 350 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M868 545.5 536.1 163a31.96 31.96 0 0 0-48.3 0L156 545.5a7.97 7.97 0 0 0 6 13.2h81c4.6 0 9-2 12.1-5.5L474 300.9V864c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V300.9l218.9 252.3c3 3.5 7.4 5.5 12.1 5.5h81c6.8 0 10.5-8 6-13.2"/></svg>

After

Width:  |  Height:  |  Size: 338 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="m512 67.008-23.008 21.984-256 256 46.048 46.048L480 190.016v644L279.008 632.96l-46.048 46.08 256 256 23.008 21.984 23.008-21.984 256-256-46.016-46.016L544 834.016v-644l200.992 200.96 46.016-45.984-256-256z"/></svg>

After

Width:  |  Height:  |  Size: 323 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="M956.8 988.8H585.6c-16 0-25.6-9.6-25.6-28.8V576c0-16 9.6-28.8 25.6-28.8h371.2c16 0 25.6 9.6 25.6 28.8v384c0 16-9.6 28.8-25.6 28.8M608 937.6h326.4V598.4H608zm-121.6 44.8C262.4 982.4 144 848 144 595.2c0-19.2 9.6-28.8 25.6-28.8s25.6 12.8 25.6 28.8c0 220.8 96 326.4 288 326.4 16 0 25.6 12.8 25.6 28.8s-6.4 32-22.4 32"/><path d="M262.4 694.4c-6.4 0-9.6-3.2-16-6.4L160 601.6c-9.6-9.6-9.6-22.4 0-28.8s22.4-9.6 28.8 0l86.4 86.4c9.6 9.6 9.6 22.4 0 28.8-3.2 3.2-6.4 6.4-12.8 6.4"/><path d="M86.4 694.4c-6.4 0-9.6-3.2-16-6.4-9.6-9.6-9.6-22.4 0-28.8l86.4-86.4c9.6-9.6 22.4-9.6 28.8 0 9.6 9.6 9.6 22.4 0 28.8L99.2 688c-3.2 3.2-6.4 6.4-12.8 6.4m790.4-249.6c-16 0-28.8-12.8-28.8-32 0-224-99.2-336-300.8-336-16 0-28.8-12.8-28.8-32s9.6-32 28.8-32c233.6 0 355.2 137.6 355.2 396.8 0 22.4-9.6 35.2-25.6 35.2"/><path d="M876.8 448c-6.4 0-9.6-3.2-16-6.4l-86.4-86.4c-9.6-9.6-9.6-22.4 0-28.8s22.4-9.6 28.8 0l86.4 86.4c9.6 9.6 9.6 22.4 0 28.8 0 3.2-6.4 6.4-12.8 6.4"/><path d="M876.8 448c-6.4 0-9.6-3.2-16-6.4-9.6-9.6-9.6-22.4 0-28.8l86.4-86.4c9.6-9.6 22.4-9.6 28.8 0s9.6 22.4 0 28.8l-86.4 86.4c-3.2 3.2-6.4 6.4-12.8 6.4M288 524.8C156.8 524.8 48 416 48 278.4S156.8 35.2 288 35.2 528 144 528 281.6 419.2 524.8 288 524.8m-3.2-432c-99.2 0-179.2 83.2-179.2 185.6S185.6 464 284.8 464 464 380.8 464 278.4 384 92.8 284.8 92.8"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M505.7 661a8 8 0 0 0 12.6 0l112-141.7c4.1-5.2.4-12.9-6.3-12.9h-74.1V168c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v338.3H400c-6.7 0-10.4 7.7-6.3 12.9zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8"/></svg>

After

Width:  |  Height:  |  Size: 417 B

View File

@@ -0,0 +1,31 @@
import Reload from "./reload.svg?component";
import Upload from "./upload.svg?component";
import ArrowH from "./arrow-h.svg?component";
import ArrowV from "./arrow-v.svg?component";
import ArrowUp from "./arrow-up.svg?component";
import ChangeIcon from "./change.svg?component";
import ArrowDown from "./arrow-down.svg?component";
import ArrowLeft from "./arrow-left.svg?component";
import DownloadIcon from "./download.svg?component";
import ArrowRight from "./arrow-right.svg?component";
import RotateLeft from "./rotate-left.svg?component";
import SearchPlus from "./search-plus.svg?component";
import RotateRight from "./rotate-right.svg?component";
import SearchMinus from "./search-minus.svg?component";
export {
Reload,
Upload,
ArrowH,
ArrowV,
ArrowUp,
ArrowDown,
ArrowLeft,
ChangeIcon,
ArrowRight,
RotateLeft,
SearchPlus,
RotateRight,
SearchMinus,
DownloadIcon
};

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M168 504.2c1-43.7 10-86.1 26.9-126 17.3-41 42.1-77.7 73.7-109.4S337 212.3 378 195c42.4-17.9 87.4-27 133.9-27s91.5 9.1 133.8 27A341.5 341.5 0 0 1 755 268.8c9.9 9.9 19.2 20.4 27.8 31.4l-60.2 47a8 8 0 0 0 3 14.1l175.7 43c5 1.2 9.9-2.6 9.9-7.7l.8-180.9c0-6.7-7.7-10.5-12.9-6.3l-56.4 44.1C765.8 155.1 646.2 92 511.8 92 282.7 92 96.3 275.6 92 503.8a8 8 0 0 0 8 8.2h60c4.4 0 7.9-3.5 8-7.8m756 7.8h-60c-4.4 0-7.9 3.5-8 7.8-1 43.7-10 86.1-26.9 126-17.3 41-42.1 77.8-73.7 109.4A342.45 342.45 0 0 1 512.1 856a342.24 342.24 0 0 1-243.2-100.8c-9.9-9.9-19.2-20.4-27.8-31.4l60.2-47a8 8 0 0 0-3-14.1l-175.7-43c-5-1.2-9.9 2.6-9.9 7.7l-.7 181c0 6.7 7.7 10.5 12.9 6.3l56.4-44.1C258.2 868.9 377.8 932 512.2 932c229.2 0 415.5-183.7 419.8-411.8a8 8 0 0 0-8-8.2"/></svg>

After

Width:  |  Height:  |  Size: 863 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M672 418H144c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32m-44 402H188V494h440z"/><path fill="currentColor" d="M819.3 328.5c-78.8-100.7-196-153.6-314.6-154.2l-.2-64c0-6.5-7.6-10.1-12.6-6.1l-128 101c-4 3.1-3.9 9.1 0 12.3L492 318.6c5.1 4 12.7.4 12.6-6.1v-63.9c12.9.1 25.9.9 38.8 2.5 42.1 5.2 82.1 18.2 119 38.7 38.1 21.2 71.2 49.7 98.4 84.3 27.1 34.7 46.7 73.7 58.1 115.8 11 40.7 14 82.7 8.9 124.8-.7 5.4-1.4 10.8-2.4 16.1h74.9c14.8-103.6-11.3-213-81-302.3"/></svg>

After

Width:  |  Height:  |  Size: 630 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M480.5 251.2c13-1.6 25.9-2.4 38.8-2.5v63.9c0 6.5 7.5 10.1 12.6 6.1L660 217.6c4-3.2 4-9.2 0-12.3l-128-101c-5.1-4-12.6-.4-12.6 6.1l-.2 64c-118.6.5-235.8 53.4-314.6 154.2-69.6 89.2-95.7 198.6-81.1 302.4h74.9c-.9-5.3-1.7-10.7-2.4-16.1-5.1-42.1-2.1-84.1 8.9-124.8 11.4-42.2 31-81.1 58.1-115.8 27.2-34.7 60.3-63.2 98.4-84.3 37-20.6 76.9-33.6 119.1-38.8"/><path fill="currentColor" d="M880 418H352c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32m-44 402H396V494h440z"/></svg>

After

Width:  |  Height:  |  Size: 633 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M637 443H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h312c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8m284 424L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11M696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430"/></svg>

After

Width:  |  Height:  |  Size: 532 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M637 443H519V309c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v134H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h118v134c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V519h118c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8m284 424L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11M696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430"/></svg>

After

Width:  |  Height:  |  Size: 628 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M400 317.7h73.9V656c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V317.7H624c6.7 0 10.4-7.7 6.3-12.9L518.3 163a8 8 0 0 0-12.6 0l-112 141.7c-4.1 5.3-.4 13 6.3 13M878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8"/></svg>

After

Width:  |  Height:  |  Size: 421 B

View File

@@ -0,0 +1,7 @@
import reCropperPreview from "./src/index.vue";
import { withInstall } from "@pureadmin/utils";
/** 图片裁剪预览组件 */
export const ReCropperPreview = withInstall(reCropperPreview);
export default ReCropperPreview;

View File

@@ -0,0 +1,76 @@
<script setup lang="tsx">
import { ref } from "vue";
import ReCropper from "@/components/ReCropper";
import { formatBytes } from "@pureadmin/utils";
defineOptions({
name: "ReCropperPreview"
});
defineProps({
imgSrc: String
});
const emit = defineEmits(["cropper"]);
const infos = ref();
const popoverRef = ref();
const refCropper = ref();
const showPopover = ref(false);
const cropperImg = ref<string>("");
function onCropper({ base64, blob, info }) {
infos.value = info;
cropperImg.value = base64;
emit("cropper", { base64, blob, info });
}
function hidePopover() {
popoverRef.value.hide();
}
defineExpose({ hidePopover });
</script>
<template>
<div v-loading="!showPopover" element-loading-background="transparent">
<el-popover
ref="popoverRef"
:visible="showPopover"
placement="right"
width="18vw"
>
<template #reference>
<div class="w-[18vw]">
<ReCropper
ref="refCropper"
:src="imgSrc"
circled
@cropper="onCropper"
@readied="showPopover = true"
/>
<p v-show="showPopover" class="mt-1 text-center">
温馨提示右键上方裁剪区可开启功能菜单
</p>
</div>
</template>
<div class="flex flex-wrap justify-center items-center text-center">
<el-image
v-if="cropperImg"
:src="cropperImg"
:preview-src-list="Array.of(cropperImg)"
fit="cover"
/>
<div v-if="infos" class="mt-1">
<p>
图像大小{{ parseInt(infos.width) }} ×
{{ parseInt(infos.height) }}像素
</p>
<p>
文件大小{{ formatBytes(infos.size) }}{{ infos.size }} 字节
</p>
</div>
</div>
</el-popover>
</div>
</template>

View File

@@ -0,0 +1,69 @@
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[index].visible = false;
options.closeCallBack && options.closeCallBack({ options, index, args });
const closeDelay = options?.closeDelay ?? 200;
useTimeoutFn(() => {
dialogStore.value.splice(index, 1);
}, closeDelay);
};
/**
* @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#L12
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L22
*/
const ReDialog = withInstall(reDialog);
export type { EventType, ArgsType, DialogProps, ButtonProps, DialogOptions };
export {
ReDialog,
dialogStore,
addDialog,
closeDialog,
updateDialog,
closeAllDialog
};

View File

@@ -0,0 +1,206 @@
<script setup lang="ts">
import {
type EventType,
type ButtonProps,
type DialogOptions,
closeDialog,
dialogStore
} 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";
defineOptions({
name: "ReDialog"
});
const sureBtnMap = ref({});
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,
popconfirm: options?.popconfirm,
btnClick: ({ dialog: { options, index } }) => {
if (options?.sureBtnLoading) {
sureBtnMap.value[index] = Object.assign(
{},
sureBtnMap.value[index],
{
loading: true
}
);
}
const closeLoading = () => {
if (options?.sureBtnLoading) {
sureBtnMap.value[index].loading = false;
}
};
const done = () => {
closeLoading();
closeDialog(options, index, { command: "sure" });
};
if (options?.beforeSure && isFunction(options?.beforeSure)) {
options.beforeSure(done, { options, index, closeLoading });
} 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,
isClickFullScreen = false
) {
if (!isClickFullScreen) 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
v-for="(options, index) in dialogStore"
:key="index"
v-bind="options"
v-model="options.visible"
class="pure-dialog"
:fullscreen="fullscreen ? true : options?.fullscreen ? true : false"
@closed="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;
eventsCallBack(
'fullscreenCallBack',
{ ...options, fullscreen },
index,
true
);
}
"
>
<IconifyIconOffline
class="pure-dialog-svg"
:icon="
options?.fullscreen
? ExitFullscreen
: fullscreen
? ExitFullscreen
: Fullscreen
"
/>
</i>
</div>
<component
:is="options?.headerRenderer({ close, titleId, titleClass })"
v-else
/>
</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>
<template v-for="(btn, key) in footerButtons(options)" :key="key">
<el-popconfirm
v-if="btn.popconfirm"
v-bind="btn.popconfirm"
@confirm="
btn.btnClick({
dialog: { options, index },
button: { btn, index: key }
})
"
>
<template #reference>
<el-button v-bind="btn">{{ btn?.label }}</el-button>
</template>
</el-popconfirm>
<el-button
v-else
v-bind="btn"
:loading="key === 1 && sureBtnMap[index]?.loading"
@click="
btn.btnClick({
dialog: { options, index },
button: { btn, index: key }
})
"
>
{{ btn?.label }}
</el-button>
</template>
</span>
</template>
</el-dialog>
</template>

View File

@@ -0,0 +1,275 @@
import type { CSSProperties, VNode, Component } from "vue";
type DoneFn = (cancel?: boolean) => void;
type EventType =
| "open"
| "close"
| "openAutoFocus"
| "closeAutoFocus"
| "fullscreenCallBack";
type ArgsType = {
/** `cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页或按下了esc键 */
command: "cancel" | "sure" | "close";
};
type ButtonType =
| "primary"
| "success"
| "warning"
| "danger"
| "info"
| "text";
/** 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;
};
//element-plus.org/zh-CN/component/popconfirm.html#attributes
type Popconfirm = {
/** 标题 */
title?: string;
/** 确定按钮文字 */
confirmButtonText?: string;
/** 取消按钮文字 */
cancelButtonText?: string;
/** 确定按钮类型,默认 `primary` */
confirmButtonType?: ButtonType;
/** 取消按钮类型,默认 `text` */
cancelButtonType?: ButtonType;
/** 自定义图标,默认 `QuestionFilled` */
icon?: string | Component;
/** `Icon` 颜色,默认 `#f90` */
iconColor?: string;
/** 是否隐藏 `Icon`,默认 `false` */
hideIcon?: boolean;
/** 关闭时的延迟,默认 `200` */
hideAfter?: number;
/** 是否将 `popover` 的下拉列表插入至 `body` 元素,默认 `true` */
teleported?: boolean;
/** 当 `popover` 组件长时间不触发且 `persistent` 属性设置为 `false` 时, `popover` 将会被删除,默认 `false` */
persistent?: boolean;
/** 弹层宽度,最小宽度 `150px`,默认 `150` */
width?: string | number;
};
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;
/** 确定按钮的 `Popconfirm` 气泡确认框相关配置 */
popconfirm?: Popconfirm;
/** 是否为加载中状态,默认 `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;
/** 确定按钮的 `Popconfirm` 气泡确认框相关配置 */
popconfirm?: Popconfirm;
/** 点击确定按钮后是否开启 `loading` 加载动画 */
sureBtnLoading?: 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` 关闭后的回调只有点击右上角关闭按钮或空白页或按下了esc键关闭页面时才会触发 */
close?: ({
options,
index
}: {
options: DialogOptions;
index: number;
}) => void;
/** `Dialog` 关闭后的回调。 `args` 返回的 `command` 值解析:`cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页或按下了esc键 */
closeCallBack?: ({
options,
index,
args
}: {
options: DialogOptions;
index: number;
args: any;
}) => void;
/** 点击全屏按钮时的回调 */
fullscreenCallBack?: ({
options,
index
}: {
options: DialogOptions;
index: number;
}) => 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,
closeLoading
}: {
options: DialogOptions;
index: number;
/** 关闭确定按钮的 `loading` 加载动画 */
closeLoading: Function;
}
) => void;
}
export type { EventType, ArgsType, DialogProps, ButtonProps, DialogOptions };

View File

@@ -0,0 +1,39 @@
.point {
width: var(--point-width);
height: var(--point-height);
background: var(--point-background);
position: relative;
border-radius: var(--point-border-radius);
}
.point-flicker:after {
background: var(--point-background);
}
.point-flicker:before,
.point-flicker:after {
content: "";
width: 100%;
height: 100%;
top: 0;
left: 0;
position: absolute;
border-radius: var(--point-border-radius);
animation: flicker 1.2s ease-out infinite;
}
@keyframes flicker {
0% {
transform: scale(0.5);
opacity: 1;
}
30% {
opacity: 1;
}
100% {
transform: scale(var(--point-scale));
opacity: 0;
}
}

View File

@@ -0,0 +1,44 @@
import "./index.css";
import { type Component, h, defineComponent } from "vue";
export interface attrsType {
width?: string;
height?: string;
borderRadius?: number | string;
background?: string;
scale?: number | string;
}
/**
* 圆点、方形闪烁动画组件
* @param width 可选 string 宽
* @param height 可选 string 高
* @param borderRadius 可选 number | string 传0为方形、传50%或者不传为圆形
* @param background 可选 string 闪烁颜色
* @param scale 可选 number | string 闪烁范围默认2值越大闪烁范围越大
* @returns Component
*/
export function useRenderFlicker(attrs?: attrsType): Component {
return defineComponent({
name: "ReFlicker",
render() {
return h(
"div",
{
class: "point point-flicker",
style: {
"--point-width": attrs?.width ?? "12px",
"--point-height": attrs?.height ?? "12px",
"--point-background":
attrs?.background ?? "var(--el-color-primary)",
"--point-border-radius": attrs?.borderRadius ?? "50%",
"--point-scale": attrs?.scale ?? "2"
}
},
{
default: () => []
}
);
}
});
}

View File

@@ -0,0 +1,7 @@
import reFlop from "./src/index.vue";
import { withInstall } from "@pureadmin/utils";
/** 时间翻牌组件 */
export const ReFlop = withInstall(reFlop);
export default ReFlop;

View File

@@ -0,0 +1,184 @@
.m-flipper {
display: inline-block;
position: relative;
width: 60px;
height: 100px;
line-height: 100px;
border: solid 1px #000;
border-radius: 10px;
background: #fff;
font-size: 66px;
color: #fff;
box-shadow: 0 0 6px rgb(0 0 0 / 50%);
text-align: center;
font-family: "Helvetica Neue";
}
.m-flipper .digital::before,
.m-flipper .digital::after {
content: "";
position: absolute;
left: 0;
right: 0;
background: #000;
overflow: hidden;
box-sizing: border-box;
}
.m-flipper .digital::before {
top: 0;
bottom: 50%;
border-radius: 10px 10px 0 0;
border-bottom: solid 1px #666;
}
.m-flipper .digital::after {
top: 50%;
bottom: 0;
border-radius: 0 0 10px 10px;
line-height: 0;
}
/* 向下翻 */
.m-flipper.down .front::before {
z-index: 3;
}
.m-flipper.down .back::after {
z-index: 2;
transform-origin: 50% 0%;
transform: perspective(160px) rotateX(180deg);
}
.m-flipper.down .front::after,
.m-flipper.down .back::before {
z-index: 1;
}
.m-flipper.down.go .front::before {
transform-origin: 50% 100%;
animation: frontFlipDown 0.6s ease-in-out both;
box-shadow: 0 -2px 6px rgb(255 255 255 / 30%);
backface-visibility: hidden;
}
.m-flipper.down.go .back::after {
animation: backFlipDown 0.6s ease-in-out both;
}
/* 向上翻 */
.m-flipper.up .front::after {
z-index: 3;
}
.m-flipper.up .back::before {
z-index: 2;
transform-origin: 50% 100%;
transform: perspective(160px) rotateX(-180deg);
}
.m-flipper.up .front::before,
.m-flipper.up .back::after {
z-index: 1;
}
.m-flipper.up.go .front::after {
transform-origin: 50% 0;
animation: frontFlipUp 0.6s ease-in-out both;
box-shadow: 0 2px 6px rgb(255 255 255 / 30%);
backface-visibility: hidden;
}
.m-flipper.up.go .back::before {
animation: backFlipUp 0.6s ease-in-out both;
}
@keyframes frontFlipDown {
0% {
transform: perspective(160px) rotateX(0deg);
}
100% {
transform: perspective(160px) rotateX(-180deg);
}
}
@keyframes backFlipDown {
0% {
transform: perspective(160px) rotateX(180deg);
}
100% {
transform: perspective(160px) rotateX(0deg);
}
}
@keyframes frontFlipUp {
0% {
transform: perspective(160px) rotateX(0deg);
}
100% {
transform: perspective(160px) rotateX(180deg);
}
}
@keyframes backFlipUp {
0% {
transform: perspective(160px) rotateX(-180deg);
}
100% {
transform: perspective(160px) rotateX(0deg);
}
}
.m-flipper .number0::before,
.m-flipper .number0::after {
content: "0";
}
.m-flipper .number1::before,
.m-flipper .number1::after {
content: "1";
}
.m-flipper .number2::before,
.m-flipper .number2::after {
content: "2";
}
.m-flipper .number3::before,
.m-flipper .number3::after {
content: "3";
}
.m-flipper .number4::before,
.m-flipper .number4::after {
content: "4";
}
.m-flipper .number5::before,
.m-flipper .number5::after {
content: "5";
}
.m-flipper .number6::before,
.m-flipper .number6::after {
content: "6";
}
.m-flipper .number7::before,
.m-flipper .number7::after {
content: "7";
}
.m-flipper .number8::before,
.m-flipper .number8::after {
content: "8";
}
.m-flipper .number9::before,
.m-flipper .number9::after {
content: "9";
}

View File

@@ -0,0 +1,92 @@
import "./filpper.css";
import propTypes from "@/utils/propTypes";
import { defineComponent, ref } from "vue";
const props = {
// front paper text
// 前牌文字
frontText: propTypes.number.def(0),
// back paper text
// 后牌文字
backText: propTypes.number.def(1),
// flipping duration, please be consistent with the CSS animation-duration value.
// 翻牌动画时间与CSS中设置的animation-duration保持一致
duration: propTypes.number.def(600)
};
export default defineComponent({
name: "ReFlop",
props,
setup(props) {
const { frontText, backText, duration } = props;
const isFlipping = ref(false);
const flipType = ref("down");
const frontTextFromData = ref(frontText);
const backTextFromData = ref(backText);
const textClass = (number: number) => {
return "number" + number;
};
const flip = (type: string, front: number, back: number) => {
// 如果处于翻转中,则不执行
if (isFlipping.value) return false;
frontTextFromData.value = front;
backTextFromData.value = back;
// 根据传递过来的type设置翻转方向
flipType.value = type;
// 设置翻转状态为true
isFlipping.value = true;
setTimeout(() => {
// 设置翻转状态为false
isFlipping.value = false;
frontTextFromData.value = back;
}, duration);
};
// 下翻牌
const flipDown = (front: any, back: any): void => {
flip("down", front, back);
};
// 上翻牌
const flipUp = (front: any, back: any): void => {
flip("up", front, back);
};
// 设置前牌文字
function setFront(text: number): void {
frontTextFromData.value = text;
}
// 设置后牌文字
const setBack = (text: number): void => {
backTextFromData.value = text;
};
return {
flipType,
isFlipping,
frontTextFromData,
backTextFromData,
textClass,
flipDown,
flipUp,
setFront,
setBack
};
},
render() {
const main = `m-flipper ${this.flipType} ${this.isFlipping ? "go" : ""}`;
const front = `digital front ${this.textClass(this.frontTextFromData)}`;
const back = `digital back ${this.textClass(this.backTextFromData)}`;
return (
<div class={main}>
<div class={front} />
<div class={back} />
</div>
);
}
});

View File

@@ -0,0 +1,135 @@
<script setup lang="ts">
import flippers from "./filpper";
import { ref, unref, nextTick, onUnmounted } from "vue";
defineOptions({
name: "ReFlop"
});
const timer = ref(null);
const flipObjs = ref([]);
const flipperHour1 = ref();
const flipperHour2 = ref();
const flipperMinute1 = ref();
const flipperMinute2 = ref();
const flipperSecond1 = ref();
const flipperSecond2 = ref();
// 初始化数字
const init = () => {
const now = new Date();
const nowTimeStr = formatDate(new Date(now.getTime()), "hhiiss");
for (let i = 0; i < flipObjs.value.length; i++) {
flipObjs?.value[i]?.setFront(nowTimeStr[i]);
}
};
// 开始计时
const run = () => {
timer.value = setInterval(() => {
// 获取当前时间
const now = new Date();
const nowTimeStr = formatDate(new Date(now.getTime() - 1000), "hhiiss");
const nextTimeStr = formatDate(now, "hhiiss");
for (let i = 0; i < flipObjs.value.length; i++) {
if (nowTimeStr[i] === nextTimeStr[i]) {
continue;
}
flipObjs?.value[i]?.flipDown(nowTimeStr[i], nextTimeStr[i]);
}
}, 1000);
};
// 正则格式化日期
const formatDate = (date: Date, dateFormat: string) => {
/* 单独格式化年份根据y的字符数量输出年份
* 例如yyyy => 2019
yy => 19
y => 9
*/
if (/(y+)/.test(dateFormat)) {
dateFormat = dateFormat.replace(
RegExp.$1,
(date.getFullYear() + "").substr(4 - RegExp.$1.length)
);
}
// 格式化月、日、时、分、秒
const o = {
"m+": date.getMonth() + 1,
"d+": date.getDate(),
"h+": date.getHours(),
"i+": date.getMinutes(),
"s+": date.getSeconds()
};
for (const k in o) {
if (new RegExp(`(${k})`).test(dateFormat)) {
// 取出对应的值
const str = o[k] + "";
/* 根据设置的格式,输出对应的字符
* 例如: 早上8时hh => 08h => 8
* 但是,当数字>=10时无论格式为一位还是多位不做截取这是与年份格式化不一致的地方
* 例如: 下午15时hh => 15, h => 15
*/
dateFormat = dateFormat.replace(
RegExp.$1,
RegExp.$1.length === 1 ? str : padLeftZero(str)
);
}
}
return dateFormat;
};
// 日期时间补零
const padLeftZero = (str: string | any[]) => {
return ("00" + str).substr(str.length);
};
nextTick(() => {
flipObjs.value = [
unref(flipperHour1),
unref(flipperHour2),
unref(flipperMinute1),
unref(flipperMinute2),
unref(flipperSecond1),
unref(flipperSecond2)
];
init();
run();
});
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value);
timer.value = null;
}
});
</script>
<template>
<div class="flip-clock">
<flippers ref="flipperHour1" />
<flippers ref="flipperHour2" />
<em>:</em>
<flippers ref="flipperMinute1" />
<flippers ref="flipperMinute2" />
<em>:</em>
<flippers ref="flipperSecond1" />
<flippers ref="flipperSecond2" />
</div>
</template>
<style>
.flip-clock .m-flipper {
margin: 0 3px;
}
.flip-clock em {
display: inline-block;
font-size: 66px;
font-style: normal;
line-height: 102px;
vertical-align: top;
}
</style>

View File

@@ -0,0 +1,17 @@
import control from "./src/Control.vue";
import nodePanel from "./src/NodePanel.vue";
import dataDialog from "./src/DataDialog.vue";
import { withInstall } from "@pureadmin/utils";
/** LogicFlow流程图-控制面板 */
const Control = withInstall(control);
/** LogicFlow流程图-拖拽面板 */
const NodePanel = withInstall(nodePanel);
/** LogicFlow流程图-查看数据 */
const DataDialog = withInstall(dataDialog);
export { Control, NodePanel, DataDialog };
// LogicFlow流程图文档http://logic-flow.org/

View File

@@ -0,0 +1,147 @@
<script setup lang="ts">
import { ref, unref, onMounted } from "vue";
import { LogicFlow } from "@logicflow/core";
interface Props {
lf: LogicFlow;
catTurboData?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
lf: null
});
const emit = defineEmits<{
(e: "catData"): void;
}>();
const controlButton3 = ref();
const controlButton4 = ref();
const focusIndex = ref<Number>(-1);
const titleLists = ref([
{
icon: "icon-zoom-out-hs",
text: "缩小",
size: "18",
disabled: false
},
{
icon: "icon-enlarge-hs",
text: "放大",
size: "18",
disabled: false
},
{
icon: "icon-full-screen-hs",
text: "适应",
size: "15",
disabled: false
},
{
icon: "icon-previous-hs",
text: "上一步",
size: "15",
disabled: true
},
{
icon: "icon-next-step-hs",
text: "下一步",
size: "17",
disabled: true
},
{
icon: "icon-download-hs",
text: "下载图片",
size: "17",
disabled: false
},
{
icon: "icon-watch-hs",
text: "查看数据",
size: "17",
disabled: false
}
]);
const onControl = (item, key) => {
["zoom", "zoom", "resetZoom", "undo", "redo", "getSnapshot"].forEach(
(v, i) => {
const domControl = props.lf;
if (key === 1) {
domControl.zoom(true);
}
if (key === 6) {
emit("catData");
}
if (key === i) {
domControl[v]();
}
}
);
};
const onEnter = key => {
focusIndex.value = key;
};
onMounted(() => {
props.lf.on("history:change", ({ data: { undoAble, redoAble } }) => {
unref(titleLists)[3].disabled = unref(controlButton3).disabled = !undoAble;
unref(titleLists)[4].disabled = unref(controlButton4).disabled = !redoAble;
});
});
</script>
<template>
<div class="control-container">
<!-- 功能按钮 -->
<ul>
<li
v-for="(item, key) in titleLists"
:key="key"
:title="item.text"
class="dark:text-bg_color"
@mouseenter.prevent="onEnter(key)"
@mouseleave.prevent="focusIndex = -1"
>
<button
:ref="'controlButton' + key"
v-tippy="{
content: item.text
}"
:disabled="item.disabled"
:style="{
cursor: item.disabled === false ? 'pointer' : 'not-allowed',
color: item.disabled === false ? '' : '#00000040',
background: 'transparent'
}"
@click="onControl(item, key)"
>
<span
:class="'iconfont ' + item.icon"
:style="{ fontSize: `${item.size}px` }"
/>
</button>
</li>
</ul>
</div>
</template>
<style scoped>
@import url("./assets/iconfont/iconfont.css");
.control-container {
background: hsl(0deg 0% 100% / 80%);
box-shadow: 0 1px 4px rgb(0 0 0 / 20%);
}
.control-container ul li {
margin: 10px;
text-align: center;
}
.control-container ul li span:hover {
color: var(--el-color-primary);
}
</style>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import VueJsonPretty from "vue-json-pretty";
import "vue-json-pretty/lib/styles.css";
defineProps({
graphData: Object
});
</script>
<template>
<vue-json-pretty
:path="'res'"
:deep="3"
:showLength="true"
:data="graphData"
/>
</template>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,166 @@
const TurboType = {
SEQUENCE_FLOW: 1,
START_EVENT: 2,
END_EVENT: 3,
USER_TASK: 4,
SERVICE_TASK: 5,
EXCLUSIVE_GATEWAY: 6
};
function getTurboType(type) {
switch (type) {
case "bpmn:sequenceFlow":
return TurboType.SEQUENCE_FLOW;
case "bpmn:startEvent":
return TurboType.START_EVENT;
case "bpmn:endEvent":
return TurboType.END_EVENT;
case "bpmn:userTask":
return TurboType.USER_TASK;
case "bpmn:serviceTask":
return TurboType.SERVICE_TASK;
case "bpmn:exclusiveGateway":
return TurboType.EXCLUSIVE_GATEWAY;
default:
return type;
}
}
function convertNodeToTurboElement(node) {
const { id, type, x, y, text = "", properties } = node;
return {
incoming: [],
outgoing: [],
dockers: [],
type: getTurboType(node.type),
properties: {
...properties,
name: (text && text.value) || "",
x: x,
y: y,
text,
logicFlowType: type
},
key: id
};
}
function convertEdgeToTurboElement(edge) {
const {
id,
type,
sourceNodeId,
targetNodeId,
startPoint,
endPoint,
pointsList,
text = "",
properties
} = edge;
return {
incoming: [sourceNodeId],
outgoing: [targetNodeId],
type: getTurboType(type),
dockers: [],
properties: {
...properties,
name: (text && text.value) || "",
text,
startPoint,
endPoint,
pointsList,
logicFlowType: type
},
key: id
};
}
export function toTurboData(data) {
const nodeMap = new Map();
const turboData = {
flowElementList: []
};
data.nodes.forEach(node => {
const flowElement = convertNodeToTurboElement(node);
turboData.flowElementList.push(flowElement);
nodeMap.set(node.id, flowElement);
});
data.edges.forEach(edge => {
const flowElement = convertEdgeToTurboElement(edge);
const sourceElement = nodeMap.get(edge.sourceNodeId);
sourceElement.outgoing.push(flowElement.key);
const targetElement = nodeMap.get(edge.targetNodeId);
targetElement.incoming.push(flowElement.key);
turboData.flowElementList.push(flowElement);
});
return turboData;
}
function convertFlowElementToEdge(element) {
const { incoming, outgoing, properties, key } = element;
const { text, startPoint, endPoint, pointsList, logicFlowType } = properties;
const edge = {
id: key,
type: logicFlowType,
sourceNodeId: incoming[0],
targetNodeId: outgoing[0],
text,
startPoint,
endPoint,
pointsList,
properties: {}
};
const excludeProperties = [
"startPoint",
"endPoint",
"pointsList",
"text",
"logicFlowType"
];
Object.keys(element.properties).forEach(property => {
if (excludeProperties.indexOf(property) === -1) {
edge.properties[property] = element.properties[property];
}
});
return edge;
}
function convertFlowElementToNode(element) {
const { properties, key } = element;
const { x, y, text, logicFlowType } = properties;
const node = {
id: key,
type: logicFlowType,
x,
y,
text,
properties: {}
};
const excludeProperties = ["x", "y", "text", "logicFlowType"];
Object.keys(element.properties).forEach(property => {
if (excludeProperties.indexOf(property) === -1) {
node.properties[property] = element.properties[property];
}
});
return node;
}
export function toLogicflowData(data) {
const lfData = {
nodes: [],
edges: []
};
const list = data.flowElementList;
list &&
list.length > 0 &&
list.forEach(element => {
if (element.type === TurboType.SEQUENCE_FLOW) {
const edge = convertFlowElementToEdge(element);
lfData.edges.push(edge);
} else {
const node = convertFlowElementToNode(element);
lfData.nodes.push(node);
}
});
return lfData;
}

View File

@@ -0,0 +1,49 @@
@font-face {
font-family: "iconfont";
src: url("iconfont.eot?t=1618544337340"); /* IE9 */
src:
url("iconfont.eot?t=1618544337340#iefix") format("embedded-opentype"),
/* IE6-IE8 */
url("data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAZ0AAsAAAAADKgAAAYmAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDZAqLQIldATYCJAMgCxIABCAFhG0HgQkb6ApRlA9Sk+xngd1wXQyjTXRCW7pkEvLB0N9/pZhyo7nvIIK1Nisnipg3omjUREiURDXNNEL/jDRCI5H/riTu/9q0D5OakT05VaM3E4kMJI2QhanZillesmYnVT0pD5+399suTrCEkjDhqLtAxyURhIU6Ser/1tp8aDPgI2g7ex2ah+Q7i0rI+Gy9rSNYOtEEdPFQVkrlj/1c3oZFk6Sv/bYQqWUunsgkk8QRkrgkCJEKpUcO8zx0cFLQr+x6CEiNi0BN2YWV4MwJhmDEqhdU4BwR8oIOEXPCjGMzcoKDuLmnLwLw6vy9vMCFM6ggIW50umRpIbVW14U29L/QmIZgqDs5cD0JDKwCHFIylReQ51yFpO+XKBwDcjHltbq9801mxdeFzX8inbguoAq1yCWzpH95JuRUJIC0EDPH5nNGtIkkA4GgvROBocpEEKLCCBwVj0BRF/CJHFYhEo9WCbF1TCdgEEgF0A0Ee8NxioIeN97QzQqFMd2tdfIJC3KeK0T3eJYu0J07g6BVbCB0IiDVDNsQ1mFcbNxDCTk6IWEb2ShHfHxUlvAjkfj0mHDhC56GAL4CWMUgQXgEywDxuH0TBAD7gDZuRqtx7KWpnyTbushlJUpytdfnUvoS/pXG880npIYe3wueUdIJoa9HlRgdsYiF5QJv8C2zjIbzXERGQmwH0QylmjJfC4evBB8UUKQZMsAMG2aWMU6nc6s9m7X4Thn0gTfomgnm5d0qwX4v0rQH3GZn4Ajp8F2VeUcTTARpA+FfyLcpc+T05bOemT2fny8EH8Vn4LPFh3htyOtB3jDSJj34IpEQ3HNboUdasWNDQifcA8BfPPkTe6YaWp0nF/IrhQHGW2D5HTO7O2zfTH3+gxip/NioTs9VwUXL7T3AbzTxHa3qSu1e4EZTfZl/QiC2c7UI5jZ/ET938pSH8Z8IPBwU0NopeLgB7h6Kvp0GVCOw72KAjKFA71sPKX7/9g+Js/AmNfj8/o28sqNVdSTVI93p08F3v/75zqw8W79vb0RVaCTrw6aNntrQwCtbzzDKosTRFMjp/WFqtpZUEGxsi6P8L09byvlyrrvUJ6/ZFJR/X32mbUmndlduWjbdnwnY2ZBHo8OIKIVDUJah62hi4aKdSoqZsWypN7d0w6nsAzb12tWrqZOl12+W/W7YyLFxDy/7U369cgFF85PUVevYahz8y/HS9ZGrbv7saR0sn5MfEzhinC2Dizcv5xHycyChG33pcskigbRkvXnDaurRjRuIeDdu4rnSgPQ/L196FHQg6FGs7266c82aTtDT1jU0CqzWoG2Ndf91wRo1g/0wo9b4VPtV+2iwl/fjvxq4f83CBZeYgx6njp8mb7jzou9FfPdwBBpffvyUx6XARoc/1umGwtrl034lryLH/YCEwly/XrrckYHsd+/YWY/u3EGI085rV6RD5+Bw7dqnoAvBjzifw3S3zdaNZL/dRnfz7XZup232DX4VtD6Cn+AzkqFgBq6unr/gwtCDuydN51fk76ocHS/nN25Y/WqMe1fzBRgEQHPEjqE0gIbkR1CKM/zYUukn9ItRVMHwLfuO1kaP2mlUivpAUpbb8f5wZS1eib+cs3/qlD9r8DU2NEccqhPVFos3SRGSKtb4hyJEcX6VZhArj8Y+edgVpHICKD9tt8ddsvuYpNLZfQGoyBiY2CzKm1chkFmHUGwbUityTs70kCCSE2DZZADRaSeo0heYTpdQ3vwIAv0+QagzEKTOQnnOzHzoXTMkrCJYy6q7Wb1GNPO6hLi6keVYaDeqpDDFGarGkqy3sLFRMXFPDjZjqYsD5A6BI4RneUk0sdlwM2w0iqxFEtuwhkTpCLHER0fzWQ+I0ogmcLVPgqkQmBZLrdvC1tMQmfGTE66J3y+HCdoZqUgFBd/Y1TCJTL92VqwoMRVQOUxzpYJTiZd1EHAIyXmskS4RmbCySY4ZpVPEsmRv1QbTIKLoGtgt4kVTI74qM2p4tulMzwFS4qPiUDFxCSSUSGJJKJd2ozFS1kgYmyN1snOnimh0brybVuw0G0WV9iF3xeYjFAg4LcEi4Q692C7TUI8omiJRZAN3M+4ikTLBlosAAAA=")
format("woff2"),
url("iconfont.woff?t=1618544337340") format("woff"),
url("iconfont.ttf?t=1618544337340") format("truetype"),
/* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
url("iconfont.svg?t=1618544337340#iconfont") format("svg"); /* iOS 4.1- */
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-full-screen-hs::before {
content: "\e656";
}
.icon-watch-hs::before {
content: "\e766";
}
.icon-download-hs::before {
content: "\e6af";
}
.icon-enlarge-hs::before {
content: "\e765";
}
.icon-previous-hs::before {
content: "\e84c";
}
.icon-zoom-out-hs::before {
content: "\e744";
}
.icon-next-step-hs::before {
content: "\e84b";
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,58 @@
{
"id": "2491438",
"name": "liu'c'tu",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "755619",
"name": "自适应图标",
"font_class": "full-screen-hs",
"unicode": "e656",
"unicode_decimal": 58966
},
{
"icon_id": "14445801",
"name": "查看",
"font_class": "watch-hs",
"unicode": "e766",
"unicode_decimal": 59238
},
{
"icon_id": "9712640",
"name": "下载",
"font_class": "download-hs",
"unicode": "e6af",
"unicode_decimal": 59055
},
{
"icon_id": "1029099",
"name": "放大",
"font_class": "enlarge-hs",
"unicode": "e765",
"unicode_decimal": 59237
},
{
"icon_id": "20017362",
"name": "上一步",
"font_class": "previous-hs",
"unicode": "e84c",
"unicode_decimal": 59468
},
{
"icon_id": "1010015",
"name": "缩小",
"font_class": "zoom-out-hs",
"unicode": "e744",
"unicode_decimal": 59204
},
{
"icon_id": "20017363",
"name": "下一步",
"font_class": "next-step-hs",
"unicode": "e84b",
"unicode_decimal": 59467
}
]
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -0,0 +1,55 @@
export const nodeList = [
{
text: "开始",
type: "start",
class: "node-start"
},
{
text: "矩形",
type: "rect",
class: "node-rect"
},
{
type: "user",
text: "用户",
class: "node-user"
},
{
type: "push",
text: "推送",
class: "node-push"
},
{
type: "download",
text: "位置",
class: "node-download"
},
{
type: "end",
text: "结束",
class: "node-end"
}
];
export const BpmnNode = [
{
type: "bpmn:startEvent",
text: "开始",
class: "bpmn-start"
},
{
type: "bpmn:endEvent",
text: "结束",
class: "bpmn-end"
},
{
type: "bpmn:exclusiveGateway",
text: "网关",
class: "bpmn-exclusiveGateway"
},
{
type: "bpmn:userTask",
text: "用户",
class: "bpmn-user"
}
];

File diff suppressed because it is too large Load Diff

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