chore: init frontend with knowledge module

This commit is contained in:
2026-07-02 18:20:38 +08:00
parent fe67114be2
commit ad2e91ef30
649 changed files with 86676 additions and 1 deletions
+8
View File
@@ -0,0 +1,8 @@
<template>
<router-view />
<cool />
</template>
<script setup lang="ts">
import Cool from '/@/cool/index.vue';
</script>
+9
View File
@@ -0,0 +1,9 @@
import { host, value } from './proxy';
export default {
// 根地址
host,
// 请求地址
baseUrl: `/${value}`
};
+62
View File
@@ -0,0 +1,62 @@
import { storage } from '../cool';
import dev from './dev';
import prod from './prod';
// 是否开发模式
export const isDev = import.meta.env.DEV;
// 配置
export const config = {
// 项目信息
app: {
name: import.meta.env.VITE_NAME,
// 菜单
menu: {
// 是否分组显示
isGroup: false,
// 自定义菜单列表
list: []
},
// 路由
router: {
// 模式
mode: import.meta.env.MODE == 'static' ? 'hash' : 'history',
// 转场动画
transition: 'slide'
}
},
// 国际化配置
i18n: {
locale: storage.get('locale') || 'zh-cn',
languages: [
{
label: '中文',
value: 'zh-cn'
},
{
label: '繁体中文',
value: 'zh-tw'
},
{
label: 'English',
value: 'en'
}
]
},
// 忽略规则
ignore: {
// 不显示请求进度条
NProgress: ['__cool_*'],
// 页面不需要登录验证
token: []
},
// 当前环境
...(isDev ? dev : prod)
};
export * from './proxy';
+17
View File
@@ -0,0 +1,17 @@
import { proxy } from './proxy';
export default {
// 根地址
host: proxy['/prod/'].target,
// 请求地址
get baseUrl() {
const mode = import.meta.env.MODE;
if (mode == 'static') {
return location.origin;
} else {
return '/api';
}
}
};
+18
View File
@@ -0,0 +1,18 @@
const proxy = {
'/dev/': {
target: 'http://127.0.0.1:8001',
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/dev/, '')
},
'/prod/': {
target: 'https://show.cool-admin.com',
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/prod/, '/api')
}
};
const value = 'dev';
const host = proxy[`/${value}/`]?.target;
export { proxy, host, value };
+62
View File
@@ -0,0 +1,62 @@
import { merge } from 'lodash-es';
import { BaseService, service } from '../service';
import { isDev } from '/@/config';
import { eps } from 'virtual:eps';
import { hmr } from '../hooks';
export function createEps() {
// 设置 request 方法
function set(d: any) {
if (d.namespace) {
const a = new BaseService(d.namespace);
for (const i in d) {
const { path, method = 'get' } = d[i];
if (path) {
a.request = a.request;
a[i] = function (data?: any) {
return this.request({
url: path,
method,
[method.toLocaleLowerCase() == 'post' ? 'data' : 'params']: data
});
};
}
}
for (const i in a) {
d[i] = a[i];
}
} else {
for (const i in d) {
set(d[i]);
}
}
}
// 遍历每一个方法
set(eps.service);
// 合并 eps
merge(service, eps.service);
// 热更新处理
hmr.setData('service', service);
// 提示
if (isDev) {
console.log('[cool-eps] updated');
}
}
// 监听 vite 触发事件
if (import.meta.hot) {
import.meta.hot.on('eps-update', ({ service }) => {
if (service) {
eps.service = service;
}
createEps();
});
}
+24
View File
@@ -0,0 +1,24 @@
import { createPinia } from 'pinia';
import { type App } from 'vue';
import { createModule } from './module';
import { router } from '../router';
import { Loading } from '../utils';
import { createEps } from './eps';
import 'virtual:svg-register';
export async function bootstrap(app: App) {
// pinia
app.use(createPinia());
// 路由
app.use(router);
// 模块
const { eventLoop } = createModule(app);
// eps
createEps();
// 加载
Loading.set([eventLoop()]);
}
+126
View File
@@ -0,0 +1,126 @@
import { type App, type Directive } from 'vue';
import { assign, isFunction, orderBy, mergeWith } from 'lodash-es';
import { filename } from '../utils';
import { module } from '../module';
import { hmr } from '../hooks';
import { config } from '/@/config';
// 扫描文件
const files = import.meta.glob('/src/{modules,plugins}/*/{config.ts,service/**,directives/**}', {
eager: true,
import: 'default'
});
// 模块列表
module.list = hmr.getData('modules', []);
// 解析
for (const i in files) {
// 分割
const [, , type, name, action] = i.split('/');
// 文件名
const n = filename(i);
// 文件内容
const v = files[i];
// 模块是否存在
const m = module.get(name);
// 数据
const d = m || {
name,
type,
value: null,
services: [],
directives: []
};
// 配置
if (action == 'config.ts') {
d.value = v;
}
// 服务
else if (action == 'service') {
const s = new (v as any)();
if (s) {
d.services?.push({
path: s.namespace,
value: s
});
}
}
// 指令
else if (action == 'directives') {
d.directives?.push({ name: n, value: v as Directive });
}
if (!m) {
module.add(d);
}
}
// 创建
export function createModule(app: App) {
// 排序
module.list.forEach(e => {
const d = isFunction(e.value) ? e.value(app) : e.value;
if (d) {
assign(e, d);
}
if (!d.order) {
e.order = 0;
}
});
const list = orderBy(module.list, 'order', 'desc').map(e => {
if (e.enable !== false) {
// 初始化
e.install?.(app, e.options);
// 注册组件
e.components?.forEach(async (c: any) => {
const v = await (isFunction(c) ? c() : c);
const n = v.default || v;
if (n.name) {
app.component(n.name, n);
}
});
// 注册指令
e.directives?.forEach(v => {
app.directive(v.name, v.value);
});
// 合并忽略配置
config.ignore = mergeWith({}, config.ignore, e.ignore, (a, b) => a?.concat(b));
}
// 附加值
e.pages?.forEach(v => {
v.isPage = true;
});
return e;
});
return {
// 模块列表
list,
// 事件加载
async eventLoop() {
const events: any = {};
for (let i = 0; i < list.length; i++) {
if (list[i].onLoad) {
assign(events, await list[i]?.onLoad?.(events));
}
}
}
};
}
+41
View File
@@ -0,0 +1,41 @@
import { useEventListener } from '@vueuse/core';
import { reactive, watch } from 'vue';
import { getBrowser } from '../utils';
// 使用 reactive 创建一个响应式的浏览器对象
const browser = reactive(getBrowser());
// 存储屏幕变化事件的回调函数数组
const events: (() => void)[] = [];
// 监听浏览器屏幕属性的变化
watch(
() => browser.screen, // 监听的属性
() => {
// 当屏幕属性变化时,执行所有注册的回调函数
events.forEach(ev => ev());
}
);
// 监听窗口的 resize 事件,并更新浏览器对象
useEventListener(window, 'resize', () => {
// 使用 Object.assign 更新响应式对象的属性
Object.assign(browser, getBrowser());
});
// 导出一个自定义的 hook
export function useBrowser() {
return {
browser, // 返回响应式的浏览器对象
// 注册屏幕变化的回调函数
onScreenChange(ev: () => void, immediate = true) {
// 将回调函数添加到事件数组中
events.push(ev);
// 如果 immediate 为 true,立即执行回调函数
if (immediate) {
ev();
}
}
};
}
+32
View File
@@ -0,0 +1,32 @@
// 解决热更新后数据失效问题
// 初始化数据对象,如果热更新数据存在则使用它
const data = import.meta.hot?.data.getData?.() || {};
// 检查是否支持热更新
if (import.meta.hot) {
// 将当前数据存储函数赋值给热更新数据对象
import.meta.hot.data.getData = () => {
return data;
};
}
// 导出一个热更新模块对象
export const hmr = {
data, // 当前数据对象
// 设置数据的方法
setData(key: string, value: any) {
// 将指定键值对存入数据对象
data[key] = value;
},
// 获取数据的方法
getData(key: string, defaultValue?: any) {
// 如果指定键不存在且提供了默认值,则设置默认值
if (defaultValue !== undefined && !data[key]) {
this.setData(key, defaultValue);
}
// 返回指定键的值
return data[key];
}
};
+58
View File
@@ -0,0 +1,58 @@
import { getCurrentInstance, type Ref, reactive } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { service } from '../service';
import { useBrowser } from './browser';
import { useMitt } from './mitt';
// 创建一个响应式的 refs 对象,并提供设置 refs 的方法
export function useRefs() {
const refs = reactive<{ [key: string]: any }>({});
// 设置 refs 的方法,返回一个函数用于更新特定 ref
function setRefs(name: string) {
return (el: any) => {
refs[name] = el;
return () => refs[name]; // 返回一个函数用于获取当前 ref
};
}
return { refs, setRefs };
}
// 获取指定名称的父组件实例,并将其暴露的属性赋值给传入的 Ref
export function useParent(name: string, r: Ref) {
const instance = getCurrentInstance();
if (instance) {
let parent = instance.proxy?.$.parent;
// 遍历父组件链,直到找到匹配的组件名称
while (parent && parent.type?.name !== name) {
parent = parent?.parent;
}
// 如果找到匹配的父组件,将其暴露的属性赋值给 Ref
if (parent && parent.type.name === name) {
r.value = parent.exposed;
}
}
return r;
}
// 组合多个功能模块,返回一个包含服务、路由、事件总线等的对象
export function useCool() {
return {
service,
route: useRoute(),
router: useRouter(),
mitt: useMitt(),
...useBrowser(),
...useRefs()
};
}
// 导出其他模块的功能
export * from './browser';
export * from './hmr';
export * from './mitt';
+9
View File
@@ -0,0 +1,9 @@
import Mitt, { type Emitter } from 'mitt';
import { hmr } from './hmr';
export const mitt: Emitter<any> = hmr.getData('mitt', Mitt());
// 返回 mitt 实例,用于在应用中进行事件的发布和订阅
export function useMitt() {
return mitt;
}
+7
View File
@@ -0,0 +1,7 @@
export * from './service';
export * from './bootstrap';
export * from './hooks';
export * from './module';
export * from './router';
export * from './types';
export { storage } from './utils';
+39
View File
@@ -0,0 +1,39 @@
<template>
<div class="cool">
<component v-for="item in list" :key="item.name" :is="item.component" />
</div>
</template>
<script setup lang="ts">
import { isFunction, orderBy } from 'lodash-es';
import { markRaw, onMounted, shallowRef } from 'vue';
import { module } from '/@/cool';
const list = shallowRef<any[]>([]);
async function refresh() {
const arr = orderBy(
module.list.filter(e => e.enable !== false && !!e.index).map(e => e.index),
'order'
);
list.value = await Promise.all(
arr
.filter(e => e?.component)
.map(async e => {
if (e) {
const c = await (isFunction(e.component) ? e.component() : e.component);
return {
...e,
component: markRaw(c.default || c)
};
}
})
);
}
onMounted(() => {
refresh();
});
</script>
+43
View File
@@ -0,0 +1,43 @@
import type { Module } from '../types';
import { hmr } from '../hooks';
import { ctx } from 'virtual:ctx';
// 获取模块列表,若不存在则初始化为空数组
const list: Module[] = hmr.getData('modules', []);
// 定义模块对象
const module = {
// 模块列表
list,
// 模块目录
dirs: ctx.modules,
// 请求对象,初始化为已解决的 Promise
req: Promise.resolve(),
// 根据名称获取模块
get(name: string): Module {
// 使用 find 方法查找模块,假设模块名称是唯一的
return this.list.find(e => e.name == name)!;
},
// 获取模块的配置选项
config(name: string) {
// 如果模块存在,返回其配置选项,否则返回空对象
return this.get(name).options || {};
},
// 添加新模块到列表中
add(data: Module) {
this.list.push(data);
},
// 返回请求对象
wait() {
return this.req;
}
};
// 导出模块对象
export { module };
+254
View File
@@ -0,0 +1,254 @@
import { ElMessage } from 'element-plus';
import {
createRouter,
createRouterMatcher,
createWebHashHistory,
createWebHistory,
type RouteRecordRaw
} from 'vue-router';
import { type Router, storage, module } from '/@/cool';
import { isArray } from 'lodash-es';
import { useBase } from '/$/base';
import { Loading } from '../utils';
import { config, isDev } from '/@/config';
// 基本路径
const baseUrl = import.meta.env.BASE_URL;
// 扫描文件
const files = import.meta.glob(['/src/modules/*/{views,pages}/**/*', '!**/components']);
// 默认路由
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'index',
component: () => import('/$/base/pages/main/index.vue'),
children: []
},
{
path: '/:catchAll(.*)',
name: '404',
component: () => import('/$/base/pages/error/404.vue')
}
];
// 创建路由器
const router = createRouter({
history:
config.app.router.mode == 'history'
? createWebHistory(baseUrl)
: createWebHashHistory(baseUrl),
routes
}) as Router;
// 组件加载后
router.beforeResolve(() => {
Loading.close();
});
let lock = false;
// 错误监听
router.onError((error: Error) => {
if (!lock) {
lock = true;
// 显示错误信息
ElMessage.error(`页面存在错误:${error.message}`);
console.error(error);
// 如果是动态加载模块失败的错误,且非开发环境,则刷新页面
if (error.message?.includes('Failed to fetch dynamically imported module')) {
if (!isDev) {
window.location.reload();
}
}
// 短暂延迟后解锁,允许后续错误处理
setTimeout(() => {
lock = false;
}, 0);
}
});
// 添加视图,页面路由
router.append = function (routeData) {
if (!routeData) {
return false; // 如果没有路由数据,直接返回
}
// 确保 routeData 是数组
const routeList = isArray(routeData) ? routeData : [routeData];
routeList.forEach(route => {
if (!route.meta) {
route.meta = {}; // 初始化 meta 对象
}
// 如果没有指定组件路径
if (!route.component) {
const viewPath = route.viewPath;
if (viewPath) {
if (viewPath.startsWith('http')) {
// 如果是外部链接,使用 iframe 组件
route.meta.iframeUrl = viewPath;
route.component = () => import('/$/base/views/frame.vue');
} else {
// 从文件系统中动态导入组件
route.component = files['/src/' + viewPath.replace('cool/', '')];
}
} else if (!route.redirect) {
// 如果没有组件路径且没有重定向,默认重定向到 404
route.redirect = '/404';
}
}
// 支持 props 接收参数
route.props = true;
// 标记为动态添加的路由
route.meta.dynamic = true;
// 判断是页面还是视图,并添加到相应的路由
if (route.isPage || route.viewPath?.includes('/pages/')) {
router.addRoute(route);
} else {
router.addRoute('index', route);
}
});
};
// 删除路由
router.del = function (routeName) {
const allRoutes = router.getRoutes();
allRoutes.forEach(route => {
if (route.name === routeName) {
router.removeRoute(routeName); // 移除指定名称的路由
}
});
};
// 清空路由
router.clear = function () {
const allRoutes = router.getRoutes();
allRoutes.forEach(route => {
if (route.name && route.meta?.dynamic) {
router.removeRoute(route.name); // 移除所有动态添加的路由
}
});
};
// 找路由
router.find = function (path: string) {
const { menu } = useBase();
// 获取已注册的路由
const registeredRoutes = router.getRoutes();
// 构建路由列表,包括已注册的路由、菜单配置和模块自定义路由
const routeList: any[] = [
...registeredRoutes.map(route => ({
...route,
isReg: true
})),
...menu.routes,
...module.list.flatMap(module => (module.views || []).concat(module.pages || []))
];
let isRegistered = false;
let matchedRoute: (typeof routeList)[number] | undefined;
// 创建路由匹配器
const matcher = createRouterMatcher(routeList, {});
// 查找匹配的路由
matcher.getRoutes().find(route => {
const routeRegex = new RegExp(route.re);
if (routeRegex.test(path)) {
if (path === '/') {
// 如果路径是根路径,查找标记为首页的路由
matchedRoute = routeList.find(route => route.meta?.isHome);
} else {
// 否则查找路径匹配且名称不是 'index' 的路由
matchedRoute = routeList.find(
r => r.path === route.record.path && r.name !== 'index'
);
}
if (matchedRoute) {
isRegistered = !!matchedRoute.isReg; // 检查路由是否已注册
}
return true;
}
return false;
});
return {
route: matchedRoute,
isReg: isRegistered
};
};
// 路由守卫
router.beforeEach(async (to, from, next) => {
// 等待应用配置加载完
await Loading.wait();
// 获取用户和进程数据
const { user, process } = useBase();
// 查找路由信息
const { isReg, route } = router.find(to.path);
// 如果路由不存在
if (!route) {
next(user.token ? '/404' : '/login'); // 根据用户登录状态重定向
return;
}
// 如果路由未注册
if (!isReg) {
router.append(route); // 注册路由
next(to.fullPath); // 重定向到原路径
return;
}
// demo 模式:访问 /login 时清除旧 token,确保能进入登录页
if (to.path.includes('/login')) {
user.clear();
next();
return;
}
// 如果用户已登录
if (user.token) {
if (to.path.includes('/login')) {
// 如果在登录页且 Token 未过期,重定向到首页
if (!storage.isExpired('token')) {
next('/');
return;
}
} else {
process.add(to); // 添加路由进程
}
} else {
// 清除用户信息
user.clear();
// 如果路径不在忽略 Token 验证的列表中,重定向到登录页
if (!config.ignore.token.some(ignorePath => to.path === ignorePath)) {
next('/login');
return;
}
}
next(); // 继续导航
});
export { router };
+86
View File
@@ -0,0 +1,86 @@
import { config } from '/@/config';
import { request } from './request';
import { AxiosRequestConfig } from 'axios';
export class BaseService {
namespace?: string;
constructor(namespace?: string) {
if (namespace) {
this.namespace = namespace;
}
}
// 发送请求
async request(options: AxiosRequestConfig = {}) {
let url = options.url;
if (url && url.indexOf('http') < 0) {
if (this.namespace) {
url = this.namespace + url;
}
if (options.proxy !== false) {
url = config.baseUrl + '/' + url;
}
}
return request({
...options,
url
});
}
// 获取列表
async list(data: any) {
return this.request({
url: '/list',
method: 'POST',
data
});
}
// 分页查询
async page(data: any) {
return this.request({
url: '/page',
method: 'POST',
data
});
}
// 获取信息
async info(params: any) {
return this.request({
url: '/info',
params
});
}
// 更新数据
async update(data: any) {
return this.request({
url: '/update',
method: 'POST',
data
});
}
// 删除数据
async delete(data: any) {
return this.request({
url: '/delete',
method: 'POST',
data
});
}
// 添加数据
async add(data: any) {
return this.request({
url: '/add',
method: 'POST',
data
});
}
}
+10
View File
@@ -0,0 +1,10 @@
import { hmr } from '../hooks';
import { BaseService } from './base';
// service 数据集合
export const service: Eps.Service = hmr.getData('service', {
request: new BaseService().request
});
export * from './base';
export * from './stream';
+168
View File
@@ -0,0 +1,168 @@
import axios from 'axios';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import { ElMessage } from 'element-plus';
import { endsWith } from 'lodash-es';
import { storage } from '/@/cool/utils';
import { useBase } from '/$/base';
import { router } from '../router';
import { config, isDev } from '/@/config';
// 创建 axios 实例
const request = axios.create({
timeout: import.meta.env.VITE_TIMEOUT, // 设置请求超时时间
withCredentials: false // 不携带凭证
});
// 配置 NProgress
NProgress.configure({
showSpinner: true // 显示加载指示器
});
// 请求队列,用于存储待处理的请求
let queue: Array<(token: string) => void> = [];
// 标识是否正在刷新 token
let isRefreshing = false;
// 请求拦截器
request.interceptors.request.use(
(req: any) => {
const { user } = useBase(); // 获取用户信息
if (req.url) {
// 控制请求进度条的显示
if (
!config.ignore.NProgress.some(e => req.url.match(new RegExp(`${e}.*`))) &&
(req.NProgress ?? true)
) {
NProgress.start();
}
}
// 在开发环境中打印请求信息
if (isDev) {
console.group(req.url);
console.log('method:', req.method);
console.table('data:', req.method == 'get' ? req.params : req.data);
console.groupEnd();
}
if (!req.headers) {
req.headers = {};
}
// 设置请求头中的语言
if (req.headers['language'] !== null) {
req.headers['language'] = config.i18n.locale;
}
// 验证 token
if (user.token) {
// 设置请求头中的 Authorization
if (req.headers['Authorization'] !== null) {
req.headers['Authorization'] = user.token;
}
// 忽略特定请求
if (['eps', 'refreshToken'].some(e => endsWith(req.url, e))) {
return req;
}
// 判断 token 是否过期
if (storage.isExpired('token')) {
// 判断 refreshToken 是否过期
if (storage.isExpired('refreshToken')) {
ElMessage.error('登录状态已失效,请重新登录');
user.logout();
} else {
// 如果不在刷新中,则刷新 token
if (!isRefreshing) {
isRefreshing = true;
user.refreshToken()
.then(token => {
queue.forEach(cb => cb(token)); // 处理队列中的请求
queue = [];
isRefreshing = false;
})
.catch(() => {
user.logout();
});
}
// 返回一个新的 Promise,等待 token 刷新完成
return new Promise(resolve => {
queue.push(token => {
if (req.headers) {
req.headers['Authorization'] = token; // 重新设置 token
}
resolve(req);
});
});
}
}
}
return req;
},
error => {
return Promise.reject(error); // 请求错误处理
}
);
// 响应拦截器
request.interceptors.response.use(
res => {
NProgress.done(); // 结束进度条
if (!res?.data) {
return res;
}
const { code, data, message } = res.data;
if (!code) {
return res.data; // 返回数据
}
switch (code) {
case 1000:
return data; // 成功返回数据
default:
return Promise.reject({ code, message }); // 处理错误
}
},
async error => {
NProgress.done(); // 结束进度条
if (error.response) {
const { status } = error.response;
const { user } = useBase();
if (status == 401) {
user.logout(); // 未授权,登出用户
} else {
if (!isDev) {
switch (status) {
case 403:
router.push('/403'); // 禁止访问
break;
case 500:
router.push('/500'); // 服务器错误
break;
case 502:
router.push('/502'); // 网关错误
break;
}
}
}
}
return Promise.reject({ message: error.response?.data?.message || error.message }); // 返回错误信息
}
);
export { request };
+103
View File
@@ -0,0 +1,103 @@
import { useBase } from '/$/base';
import { config } from '/@/config';
export function useStream() {
const { user } = useBase();
let abortController: AbortController | null = null;
// 调用
async function invoke({
url,
method = 'POST',
data,
cb
}: {
url: string;
method?: string;
data?: any;
cb?: (result: any) => void;
}) {
abortController = new AbortController();
let cacheText = '';
return fetch(config.baseUrl + url, {
method,
headers: {
Authorization: user.token,
'Content-Type': 'application/json'
},
body: JSON.stringify(data),
signal: abortController?.signal
})
.then(res => {
if (res.body) {
const reader = res.body.getReader();
const decoder = new TextDecoder('utf-8');
const stream = new ReadableStream({
start(controller) {
function push() {
reader.read().then(({ done, value }) => {
if (done) {
controller.close();
return;
}
let text = decoder.decode(value, { stream: true });
if (cb) {
if (cacheText) {
text = cacheText + text;
}
if (text.indexOf('data:') == 0) {
text = '\n\n' + text;
}
try {
const arr = text
.split(/\n\ndata:/g)
.filter(Boolean)
.map(e => JSON.parse(e));
arr.forEach(cb);
cacheText = '';
} catch (err) {
console.error('[parse text]', text);
cacheText = text;
}
}
controller.enqueue(text);
push();
});
}
push();
}
});
return new Response(stream);
}
return res;
})
.catch(err => {
console.error(err);
throw err;
});
}
// 取消
function cancel() {
if (abortController) {
abortController.abort();
abortController = null;
}
}
return {
invoke,
cancel
};
}
+60
View File
@@ -0,0 +1,60 @@
import type { Component, Directive, App } from 'vue';
import type { Router as VueRouter, RouteRecordRaw } from 'vue-router';
export declare type Merge<A, B> = Omit<A, keyof B> & B;
export declare interface ModuleConfig {
enable?: boolean;
name?: string;
label?: string;
description?: string;
order?: number;
version?: string;
logo?: string;
author?: string;
updateTime?: string;
demo?: { name: string; component: Component }[] | string;
doc?: string;
ignore?: {
NProgress?: string[];
token?: string[];
};
options?: {
[key: string]: any;
};
toolbar?: {
order?: number;
pc?: boolean;
h5?: boolean;
component: any;
};
index?: {
component: any;
};
components?: Component[];
views?: RouteRecordRaw[];
pages?: (RouteRecordRaw & { isPage?: boolean })[];
install?(app: App, options?: any): any;
onLoad?(events: {
hasToken: (cb: () => Promise<any> | void) => Promise<any> | void;
[key: string]: any;
}): Promise<{ [key: string]: any }> | Promise<void> | void;
}
export declare interface Module extends ModuleConfig {
name: string;
options: {
[key: string]: any;
};
value?: any;
services?: { path: string; value: any }[];
directives?: { name: string; value: Directive }[];
[key: string]: any;
}
export declare interface Router extends VueRouter {
find(path: string): { route: RouteRecordRaw; isReg: boolean };
del(name: string): void;
clear(): void;
append(data: any | any[]): void;
}
+301
View File
@@ -0,0 +1,301 @@
import { isArray, isNumber, isString, orderBy } from 'lodash-es';
import { resolveComponent } from 'vue';
import storage from './storage';
// 首字母大写
export function firstUpperCase(value: string): string {
return value.replace(/\b(\w)(\w*)/g, function ($0, $1, $2) {
return $1.toUpperCase() + $2;
});
}
// 获取方法名
export function getNames(value: any) {
return Object.getOwnPropertyNames(value.constructor.prototype);
}
// 获取地址栏参数
export function getUrlParam(name: string): string | null {
const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)');
const r = window.location.search.substr(1).match(reg);
if (r != null) return decodeURIComponent(r[2]);
return null;
}
// 文件名
export function filename(path: string): string {
return basename(path.substring(0, path.lastIndexOf('.')));
}
// 路径名称
export function basename(path: string): string {
let index = path.lastIndexOf('/');
index = index > -1 ? index : path.lastIndexOf('\\');
if (index < 0) {
return path;
}
return path.substring(index + 1);
}
// 文件扩展名
export function extname(path: string): string {
return path.substring(path.lastIndexOf('.') + 1).split(/(\?|&)/)[0];
}
// 横杠转驼峰
export function toCamel(str: string): string {
return str.replace(/([^-])(?:-+([^-]))/g, function ($0, $1, $2) {
return $1 + $2.toUpperCase();
});
}
// uuid
export function uuid(separator = '-'): string {
const s: any[] = [];
const hexDigits = '0123456789abcdef';
for (let i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
s[14] = '4';
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1);
s[8] = s[13] = s[18] = s[23] = separator;
return s.join('');
}
// 浏览器信息
export function getBrowser() {
const { clientHeight, clientWidth } = document.documentElement;
// 浏览器信息
const ua = navigator.userAgent.toLowerCase();
// 浏览器类型
let type = (ua.match(/firefox|chrome|safari|opera/g) || 'other')[0];
if ((ua.match(/msie|trident/g) || [])[0]) {
type = 'msie';
}
// 平台标签
let tag = '';
const isTocuh =
'ontouchstart' in window || ua.indexOf('touch') !== -1 || ua.indexOf('mobile') !== -1;
if (isTocuh) {
if (ua.indexOf('ipad') !== -1) {
tag = 'pad';
} else if (ua.indexOf('mobile') !== -1) {
tag = 'mobile';
} else if (ua.indexOf('android') !== -1) {
tag = 'androidPad';
} else {
tag = 'pc';
}
} else {
tag = 'pc';
}
// 浏览器内核
let prefix = '';
switch (type) {
case 'chrome':
case 'safari':
case 'mobile':
prefix = 'webkit';
break;
case 'msie':
prefix = 'ms';
break;
case 'firefox':
prefix = 'Moz';
break;
case 'opera':
prefix = 'O';
break;
default:
prefix = 'webkit';
break;
}
// 操作平台
const plat = ua.indexOf('android') > 0 ? 'android' : navigator.platform.toLowerCase();
// 屏幕信息
let screen = 'full';
if (clientWidth < 768) {
screen = 'xs';
} else if (clientWidth < 992) {
screen = 'sm';
} else if (clientWidth < 1200) {
screen = 'md';
} else if (clientWidth < 1920) {
screen = 'xl';
} else {
screen = 'full';
}
// 是否 ios
const isIOS = !!navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/);
// 是否 PC 端
const isPC = tag === 'pc';
// 是否移动端
const isMobile = isPC ? false : true;
// 是否移动端 + 屏幕宽过小
const isMini = screen === 'xs' || isMobile;
return {
height: clientHeight,
width: clientWidth,
type,
plat,
tag,
prefix,
isMobile,
isIOS,
isPC,
isMini,
screen
};
}
// 路径转数组
export function deepPaths(paths: string[], splitor?: string) {
const list: any[] = [];
paths.forEach(e => {
const arr: string[] = e.split(splitor || '/').filter(Boolean);
let c = list;
arr.forEach((a, i) => {
let d = c.find(e => e.label == a);
if (!d) {
d = {
label: a,
value: a,
children: arr[i + 1] ? [] : null
};
c.push(d);
}
if (d.children) {
c = d.children;
}
});
});
return list;
}
// 列表转树形
export function deepTree(list: any[], sort?: 'desc' | 'asc'): any[] {
const newList: any[] = [];
const map: any = {};
orderBy(list, 'orderNum', sort)
.map(e => {
map[e.id] = e;
return e;
})
.forEach(e => {
const parent = map[e.parentId];
if (parent) {
(parent.children || (parent.children = [])).push(e);
} else {
newList.push(e);
}
});
return newList;
}
// 树形转列表
export function revDeepTree(list: any[]) {
const arr: any[] = [];
let id = 0;
function deep(list: any[], parentId: number) {
list.forEach(e => {
if (!e.id) {
e.id = ++id;
}
if (!e.parentId) {
e.parentId = parentId;
}
arr.push(e);
if (e.children && isArray(e.children) && e.id) {
deep(e.children, e.id);
}
});
}
deep(list || [], 0);
return arr;
}
// 路径转对象
export function path2Obj(list: any[]) {
const data: any = {};
list.forEach(({ path, value }) => {
if (path) {
const arr: string[] = path.split('/');
const parents = arr.slice(0, arr.length - 1);
const name = basename(path).replace('.ts', '');
let curr = data;
parents.forEach(k => {
if (!curr[k]) {
curr[k] = {};
}
curr = curr[k];
});
curr[name] = value;
}
});
return data;
}
// 是否是组件
export function isComponent(name: string) {
return !isString(resolveComponent(name));
}
// 是否Promise
export function isPromise(val: any) {
return val && Object.prototype.toString.call(val) === '[object Promise]';
}
// 单位转换
export function parsePx(val: string | number) {
return isNumber(val) ? `${val}px` : val;
}
// 延迟
export function sleep(duration: number) {
return new Promise(resolve => {
setTimeout(() => {
resolve(true);
}, duration);
});
}
export { storage };
export * from './loading';
+37
View File
@@ -0,0 +1,37 @@
export const Loading = {
resolve: null as (() => void) | null,
next: null as Promise<void> | null,
async set(list: Promise<any>[]) {
try {
await Promise.all(list);
} catch (e) {
console.error('[Loading] Error: ', e);
}
if (this.resolve) {
this.resolve();
}
},
async wait() {
if (this.next) {
return this.next;
}
return Promise.resolve();
},
close() {
const el = document.getElementById('Loading');
if (el) {
setTimeout(() => {
el.classList.add('is-hide');
}, 0);
}
}
};
Loading.next = new Promise<void>(resolve => {
Loading.resolve = resolve;
});
+83
View File
@@ -0,0 +1,83 @@
import store from 'store';
export default {
// 后缀标识
suffix: '_deadtime',
/**
* 获取
* @param {string} key 关键字
*/
get(key: string) {
return store.get(key);
},
/**
* 获取全部
*/
info() {
const data: Record<string, any> = {};
store.each((value: any, key: any) => {
data[key] = value;
});
return data;
},
/**
* 设置
* @param {string} key 关键字
* @param {*} value 值
* @param {number} expires 过期时间
*/
set(key: string, value: any, expires?: number) {
store.set(key, value);
if (expires) {
const expirationTime = Date.now() + expires * 1000;
store.set(`${key}${this.suffix}`, expirationTime);
}
},
/**
* 是否过期
* @param {string} key 关键字
*/
isExpired(key: string) {
const expiration = this.getExpiration(key) || 0;
return expiration - Date.now() <= 2000;
},
/**
* 获取到期时间
* @param {string} key 关键字
*/
getExpiration(key: string) {
return this.get(key + this.suffix);
},
/**
* 移除
* @param {string} key 关键字
*/
remove(key: string) {
store.remove(key);
this.removeExpiration(key);
},
/**
* 移除到期时间
* @param {string} key 关键字
*/
removeExpiration(key: string) {
store.remove(key + this.suffix);
},
/**
* 清理
*/
clearAll() {
store.clearAll();
}
};
+14
View File
@@ -0,0 +1,14 @@
import { createApp } from 'vue';
import App from './App.vue';
import { bootstrap } from './cool';
const app = createApp(App);
// 启动
bootstrap(app)
.then(() => {
app.mount('#app');
})
.catch(err => {
console.error('COOL-ADMIN 启动失败', err);
});
@@ -0,0 +1,53 @@
import { defineComponent, type PropType } from 'vue';
import { UserFilled } from '@element-plus/icons-vue';
export default defineComponent({
name: 'cl-avatar',
props: {
modelValue: String,
src: String,
icon: {
type: null,
default: UserFilled
},
size: {
type: [String, Number] as PropType<'large' | 'default' | 'small' | number>,
default: 40
},
shape: {
type: String as PropType<'circle' | 'square'>,
default: 'square'
},
fit: {
type: String as PropType<'fill' | 'contain' | 'cover' | 'none' | 'scale-down'>,
default: 'cover'
}
},
setup(props) {
return () => {
const height = props.size + 'px';
return (
<div
class="cl-avatar"
style={{
height
}}
>
<el-avatar
style={{
height,
width: props.size + 'px'
}}
{...{
...props,
src: props.modelValue || props.src
}}
/>
</div>
);
};
}
});
+156
View File
@@ -0,0 +1,156 @@
<template v-if="text">
<div class="cl-code-json__wrap" v-if="popover">
<el-popover
width="auto"
placement="right"
popper-class="cl-code-json__popper"
effect="dark"
>
<template #reference>
<span class="cl-code-json__text">{{ text }}</span>
</template>
<viewer />
</el-popover>
</div>
<viewer v-else>
<template #op>
<slot name="op"> </slot>
</template>
</viewer>
</template>
<script lang="tsx" setup>
defineOptions({
name: 'cl-code-json'
});
import { useClipboard } from '@vueuse/core';
import { ElMessage } from 'element-plus';
import { isObject, isString } from 'lodash-es';
import { computed, defineComponent } from 'vue';
import { useI18n } from 'vue-i18n';
const props = defineProps({
content: null,
modelValue: null,
popover: Boolean,
height: {
type: [Number, String],
default: '100%'
},
maxHeight: {
type: [Number, String],
default: 300
},
title: String
});
const { copy } = useClipboard();
const { t } = useI18n();
// 文本
const text = computed(() => {
const v = props.modelValue || props.content;
if (isString(v)) {
return v;
} else if (isObject(v)) {
return JSON.stringify(v, null, 4);
} else {
return String(v);
}
});
// 视图
const viewer = defineComponent({
setup(_, { slots }) {
function toCopy() {
copy(text.value);
ElMessage.success('复制成功');
}
return () => {
return (
<div class="cl-code-json">
<div class="cl-code-json__op">
{text.value != '{}' && (
<el-button type="success" size="small" onClick={toCopy}>
{t('复制')}
</el-button>
)}
{slots.op && slots.op()}
</div>
{props.title && <div class="cl-code-json__title">{props.title}</div>}
<el-scrollbar
class="cl-code-json__content"
max-height={props.maxHeight}
height={props.height}
>
<pre>
<code>{text.value}</code>
</pre>
</el-scrollbar>
</div>
);
};
}
});
</script>
<style lang="scss">
.cl-code-json {
position: relative;
min-width: 200px;
max-width: 500px;
font-size: 14px;
&__op {
position: absolute;
right: 8px;
top: 8px;
z-index: 9;
}
&__title {
padding: 10px;
font-size: 12px;
color: var(--el-text-color-regular);
& + .cl-code-json__content {
padding-top: 0;
}
}
&__content {
padding: 10px;
code {
white-space: pre-wrap;
}
}
&__wrap {
.cl-code-json__text {
display: block;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
cursor: pointer;
&:hover {
color: var(--el-color-primary);
}
}
}
&__popper {
padding: 0 !important;
border-radius: 8px !important;
}
}
</style>
+111
View File
@@ -0,0 +1,111 @@
<template>
<div class="cl-dept-check">
<div class="cl-dept-check__search">
<el-input v-model="keyword" :placeholder="$t('输入关键字进行过滤')" />
</div>
<div class="cl-dept-check__tree">
<el-scrollbar max-height="200px">
<el-tree
ref="Tree"
node-key="id"
show-checkbox
:data="list"
:props="{
label: 'name',
children: 'children'
}"
:filter-node-method="filterNode"
:check-strictly="checkStrictly"
@check="onCheckChange"
/>
</el-scrollbar>
</div>
</div>
</template>
<script lang="ts" setup>
defineOptions({
name: 'cl-dept-check'
});
import { ref, watch } from 'vue';
import { deepTree } from '/@/cool/utils';
import { useCool } from '/@/cool';
import { useUpsert } from '@cool-vue/crud';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
modelValue: {
type: Array,
default: () => []
},
checkStrictly: Boolean
});
const emit = defineEmits(['update:modelValue']);
const { service } = useCool();
// el-tree
const Tree = ref();
// 树形列表
const list = ref();
// 关键字搜素
const keyword = ref('');
// 刷新树形列表
async function refresh() {
return service.base.sys.department.list().then(res => {
list.value = deepTree(res);
});
}
// 过滤节点
function filterNode(val: string, data: any) {
if (!val) return true;
return data.name.includes(val);
}
// 值改变
function onCheckChange(_: any, { checkedKeys }: any) {
emit('update:modelValue', checkedKeys);
}
// 监听过滤
watch(keyword, (val: string) => {
Tree.value?.filter(val);
});
useUpsert({
async onOpened() {
await refresh();
Tree.value?.setCheckedKeys(props.modelValue || []);
}
});
</script>
<style lang="scss" scoped>
.cl-dept-check {
&__search {
display: flex;
align-items: center;
.el-input {
flex: 1;
}
}
&__tree {
border: 1px solid var(--el-border-color);
margin-top: 5px;
border-radius: 4px;
box-sizing: border-box;
padding: 5px 0;
}
}
</style>
@@ -0,0 +1,87 @@
<template>
<div class="cl-dept-select">
<el-tree-select
v-model="value"
node-key="id"
:data="list"
:props="{
label: 'name',
value: 'id',
children: 'children'
}"
:multiple="multiple"
:check-strictly="checkStrictly"
:show-checkbox="multiple"
default-expand-all
@change="onChange"
@check="onCheckChange"
/>
</div>
</template>
<script lang="ts" setup>
defineOptions({
name: 'cl-dept-select'
});
import { ElMessage } from 'element-plus';
import { onMounted, ref, useModel } from 'vue';
import { useCool } from '/@/cool';
import { deepTree } from '/@/cool/utils';
const props = defineProps({
modelValue: [Array, Number, String],
multiple: Boolean,
checkStrictly: {
type: Boolean,
default: true
}
});
const emit = defineEmits(['update:modelValue', 'change']);
const { service } = useCool();
const value = useModel(props, 'modelValue');
const list = ref();
// 单选改变
function onChange(val: string) {
if (!props.multiple) {
emit('update:modelValue', val);
}
}
// 多选改变
function onCheckChange(_: any, { checkedKeys }: any) {
if (props.multiple) {
emit('update:modelValue', checkedKeys);
}
}
// 刷新
function refresh() {
service.base.sys.department
.list()
.then(res => {
list.value = deepTree(res);
})
.catch(err => {
list.value = [];
ElMessage.error(err.message);
});
}
onMounted(() => {
refresh();
});
</script>
<style lang="scss" scoped>
.cl-dept-select {
:deep(.el-select) {
width: 100%;
}
}
</style>
@@ -0,0 +1,44 @@
import { defineComponent, h, resolveComponent, ref, reactive, watch } from 'vue';
import { isComponent } from '/@/cool/utils';
import { assign } from 'lodash-es';
import { useI18n } from 'vue-i18n';
export default defineComponent({
name: 'cl-editor',
props: {
name: {
type: String,
required: true
}
},
setup(props, { slots, expose }) {
const Editor = ref();
const ex = reactive({});
const { t } = useI18n();
watch(Editor, v => {
if (v) {
assign(ex, v);
}
});
expose(ex);
return () => {
return isComponent(props.name) ? (
h(
resolveComponent(props.name),
{
...props,
ref: Editor
},
slots
)
) : (
<el-input type="textarea" rows={4} placeholder={t('请输入')} {...props} />
);
};
}
});
+41
View File
@@ -0,0 +1,41 @@
<template>
<svg :class="svgClass" :style="style" aria-hidden="true">
<use :xlink:href="iconName" />
</svg>
</template>
<script lang="ts" setup>
defineOptions({
name: 'cl-svg'
});
import { computed, reactive } from 'vue';
import { parsePx } from '/@/cool/utils';
const props = defineProps({
name: String,
className: String,
color: String,
size: [String, Number]
});
const style = reactive({
fontSize: parsePx(props.size!),
fill: props.color
});
const iconName = computed(() => `#icon-${props.name}`);
const svgClass = computed(() => {
return ['cl-svg', `cl-svg__${props.name}`, String(props.className || '')];
});
</script>
<style lang="scss" scoped>
.cl-svg {
display: inline-block;
width: 1em;
height: 1em;
fill: currentColor;
overflow: hidden;
}
</style>
+111
View File
@@ -0,0 +1,111 @@
<template>
<div
class="cl-image"
:style="{
height: style.h
}"
>
<el-image
:src="url"
:fit="fit"
:lazy="lazy"
:preview-src-list="preview ? urls : undefined"
:style="{
height: style.h,
width: style.w
}"
preview-teleported
>
<template #error>
<div class="cl-image__slot">
<el-icon :size="18"><picture-filled /></el-icon>
</div>
</template>
</el-image>
</div>
</template>
<script lang="ts" setup>
defineOptions({
name: 'cl-image'
});
import { computed, type PropType } from 'vue';
import { isArray, isNumber, isString } from 'lodash-es';
import { PictureFilled } from '@element-plus/icons-vue';
import { parsePx } from '/@/cool/utils';
const props = defineProps({
modelValue: [String, Array],
src: [String, Array],
size: {
type: [Number, Array],
default: 100
},
radius: {
type: [Number, String],
default: 0
},
lazy: Boolean,
fit: {
type: String as PropType<'' | 'contain' | 'cover' | 'none' | 'fill' | 'scale-down'>,
default: 'cover'
},
compress: String as PropType<'oss' | 'none'>,
preview: {
type: Boolean,
default: true
}
});
const urls = computed(() => {
const urls: any = props.modelValue || props.src;
if (isArray(urls)) {
return urls;
}
if (isString(urls)) {
if (urls.startsWith('data:image')) {
return [urls];
}
return (urls || '').split(',').filter(Boolean);
}
return [];
});
const style = computed(() => {
const [h, w]: any[] = isNumber(props.size) ? [props.size, props.size] : props.size;
return {
h: parsePx(h),
w: parsePx(w)
};
});
const url = computed(() => {
const v = urls.value[0];
return props.compress === 'oss'
? `${v}?x-oss-process=image/resize,m_fill,h_${style.value.h},w_${style.value.w}`
: v;
});
</script>
<style lang="scss" scoped>
.cl-image {
&__slot {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
border-radius: 6px;
background-color: var(--el-fill-color-lighter);
color: var(--el-text-color-regular);
}
:deep(.el-image__inner) {
border-radius: v-bind('parsePx(props.radius)');
}
}
</style>
@@ -0,0 +1,75 @@
<template>
<div class="cl-link">
<a v-for="item in urls" :key="item" class="cl-link__item" :href="item" :target="target">
<el-icon><icon-link /></el-icon>
<span>{{ text || filename(item) }}</span>
</a>
</div>
</template>
<script lang="ts" setup>
defineOptions({
name: 'cl-link'
});
import { computed } from 'vue';
import { isArray, isString, last } from 'lodash-es';
import { Link as IconLink } from '@element-plus/icons-vue';
const props = defineProps({
modelValue: [String, Array],
href: [String, Array],
text: String,
target: {
type: String,
default: '_blank'
}
});
const urls = computed(() => {
const urls: any = props.modelValue || props.href;
if (isArray(urls)) {
return urls;
}
if (isString(urls)) {
return (urls || '').split(',').filter(Boolean);
}
return [];
});
function filename(url: string) {
return last(url.split('/'));
}
</script>
<style lang="scss" scoped>
.cl-link {
&__item {
display: flex;
align-items: center;
color: var(--el-color-primary);
padding: 0 5px;
border-radius: 6px;
margin: 2px;
text-decoration: none;
span {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.el-icon {
margin-right: 4px;
}
&:hover {
text-decoration: underline;
}
}
}
</style>
@@ -0,0 +1,99 @@
<template>
<div class="cl-menu-check">
<el-input v-model="keyword" :placeholder="$t('输入关键字进行过滤')" />
<div class="cl-menu-check__scroller">
<el-scrollbar max-height="200px">
<el-tree
ref="Tree"
node-key="id"
show-checkbox
:data="list"
:props="{
label: 'name',
children: 'children'
}"
:filter-node-method="filterNode"
@check="onCheckChange"
/>
</el-scrollbar>
</div>
</div>
</template>
<script lang="ts" setup>
defineOptions({
name: 'cl-menu-check'
});
import { ref, watch } from 'vue';
import { deepTree } from '/@/cool/utils';
import { useCool } from '/@/cool';
import { useUpsert } from '@cool-vue/crud';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
modelValue: {
type: Array,
default: () => []
}
});
const emit = defineEmits(['update:modelValue']);
const { service } = useCool();
// el-tree 组件
const Tree = ref();
// 树形列表
const list = ref();
// 搜索关键字
const keyword = ref('');
// 刷新列表
async function refresh() {
return service.base.sys.menu.list().then(res => {
list.value = deepTree(res);
});
}
// 过滤节点
function filterNode(val: string, data: any) {
if (!val) return true;
return data.name.includes(val);
}
// 值改变
function onCheckChange(_: any, { checkedKeys, halfCheckedKeys }: any) {
emit('update:modelValue', [...checkedKeys, ...halfCheckedKeys]);
}
// 过滤监听
watch(keyword, (val: string) => {
Tree.value.filter(val);
});
useUpsert({
async onOpened() {
await refresh();
Tree.value?.setCheckedKeys(
(props.modelValue || []).filter(e => Tree.value.getNode(e)?.isLeaf)
);
}
});
</script>
<style lang="scss" scoped>
.cl-menu-check {
&__scroller {
border: 1px solid var(--el-border-color);
border-radius: 4px;
margin-top: 10px;
padding: 5px 0;
}
}
</style>
+174
View File
@@ -0,0 +1,174 @@
<template>
<div class="cl-menu-file">
<el-tooltip :content="$t('自定义输入')">
<div
class="cl-menu-file__icon"
:class="{
'is-edit': isEdit
}"
@click="toggle()"
>
<cl-svg name="edit" />
</div>
</el-tooltip>
<template v-if="isEdit">
<el-input
v-model="text"
:placeholder="$t('请输入')"
@change="onTextChange"
:ref="setRefs('input')"
/>
</template>
<template v-else>
<el-cascader
v-model="path"
:options="data"
clearable
filterable
@change="onPathChange"
/>
</template>
</div>
</template>
<script lang="ts" setup>
defineOptions({
name: 'cl-menu-file'
});
import { nextTick, ref, watch } from 'vue';
import { deepPaths } from '/@/cool/utils';
import { useCool } from '/@/cool';
const props = defineProps({
modelValue: {
type: String,
default: ''
}
});
const emit = defineEmits(['update:modelValue', 'change']);
const { refs, setRefs } = useCool();
// 扫描文件
function findFiles() {
const files = import.meta.glob(['/src/modules/*/{views,pages}/**/*', '!**/components']);
const list: string[] = [];
for (const i in files) {
if (!i.includes('base/pages')) {
list.push(i.substring(13));
}
}
return deepPaths(list);
}
// 路径
const path = ref();
// 文本
const text = ref();
// 是否编辑
const isEdit = ref(false);
// 数据列表
const data = ref(findFiles());
// 路径值改变
function onPathChange(arr: any) {
const v = 'modules/' + (arr || []).join('/');
emit('update:modelValue', v);
emit('change', v);
}
// 文本值改变
function onTextChange(v: string) {
emit('update:modelValue', v);
emit('change', v);
}
// 切换
function toggle() {
isEdit.value = !isEdit.value;
if (isEdit.value) {
nextTick(() => {
refs.input.focus();
});
}
}
watch(
() => props.modelValue,
val => {
if (val) {
if (val.includes('http')) {
text.value = val;
isEdit.value = true;
} else {
path.value = val.replace(/(modules\/|cool\/)/g, '').split('/');
}
}
},
{
immediate: true
}
);
</script>
<style lang="scss" scoped>
.cl-menu-file {
display: flex;
align-items: center;
width: 100%;
&__icon {
display: flex;
align-items: center;
justify-content: center;
margin-right: 5px;
border: 1px solid var(--el-border-color);
height: 32px;
width: 32px;
border-radius: var(--el-border-radius-base);
box-sizing: border-box;
flex-shrink: 0;
cursor: pointer;
.cl-svg {
font-size: 16px;
}
&.is-edit {
background-color: var(--el-color-primary);
color: var(--el-color-white);
border: 0;
}
&:hover:not(.is-edit) {
.cl-svg {
color: var(--el-color-primary);
}
}
}
:deep(.el-cascader) {
width: 100%;
}
.el-icon {
margin: 0 10px 0 0;
font-size: 18px;
cursor: pointer;
&:hover {
color: var(--el-color-primary);
}
}
}
</style>
+84
View File
@@ -0,0 +1,84 @@
<template>
<div class="cl-menu-icon">
<div class="cl-menu-icon__current" v-if="showIcon && modelValue">
<cl-svg :name="modelValue" />
</div>
<el-select v-model="value" filterable fit-input-width clearable>
<div class="cl-menu-icon__list">
<el-option v-for="item in list" :key="item" :value="item">
<cl-svg :name="item" />
</el-option>
</div>
</el-select>
</div>
</template>
<script lang="ts" setup>
defineOptions({
name: 'cl-menu-icon'
});
import { ref, useModel } from 'vue';
import { svgIcons } from 'virtual:svg-icons';
const props = defineProps({
modelValue: {
type: String,
default: ''
},
showIcon: Boolean
});
const emit = defineEmits(['update:modelValue']);
// 图标列表
const list = ref(svgIcons.filter(e => e.indexOf('icon-') === 0));
// 已选图标
const value = useModel(props, 'modelValue');
</script>
<style lang="scss" scoped>
.cl-menu-icon {
display: flex;
align-items: center;
&__current {
display: flex;
align-items: center;
justify-content: center;
margin-right: 5px;
border: 1px solid var(--el-border-color);
height: 32px;
width: 32px;
border-radius: var(--el-border-radius-base);
box-sizing: border-box;
flex-shrink: 0;
.cl-svg {
font-size: 16px;
}
}
&__list {
display: flex;
flex-wrap: wrap;
padding-left: 5px;
.el-select-dropdown__item {
display: flex;
align-items: center;
justify-content: center;
padding: 0;
height: 50px;
width: 50px;
border-radius: 4px;
}
.cl-svg {
font-size: 18px;
}
}
}
</style>
@@ -0,0 +1,95 @@
<template>
<div class="cl-menu-perms">
<el-cascader
v-model="value"
separator=":"
clearable
filterable
collapse-tags
collapse-tags-tooltip
:disabled="disabled"
:options="data"
:props="cascaderProps"
@change="onChange"
/>
</div>
</template>
<script lang="ts" setup>
defineOptions({
name: 'cl-menu-perms'
});
import { onMounted, ref, watch, reactive } from 'vue';
import { useCool } from '/@/cool';
import { deepPaths } from '/@/cool/utils';
const props = defineProps({
modelValue: {
type: String,
default: ''
},
disabled: Boolean
});
const emit = defineEmits(['update:modelValue']);
const { service } = useCool();
// 绑定值
const value = ref<string[][]>([]);
// 权限列表
const data = ref<any[]>([]);
// elm BUG
const cascaderProps = reactive({ multiple: true });
// 监听改变
function onChange(arr: any) {
emit('update:modelValue', arr.map((e: string[]) => e.join(':')).join(','));
}
// 监听值
watch(
() => props.modelValue,
val => {
value.value = val ? val.split(',').map(e => e.split(':')) : [];
},
{
immediate: true
}
);
onMounted(() => {
const list: any[] = [];
function deep(s: any) {
if (typeof s == 'object') {
for (const i in s) {
const { permission } = s[i];
if (permission) {
list.push(...Object.values(permission));
} else {
deep(s[i]);
}
}
}
}
deep(service);
data.value = deepPaths(list, ':');
});
</script>
<style lang="scss" scoped>
.cl-menu-perms {
line-height: 0;
:deep(.el-cascader) {
width: 100%;
}
}
</style>
@@ -0,0 +1,86 @@
<template>
<div class="cl-menu-select">
<el-tree-select
v-model="value"
:data="tree"
:props="{
label: 'name',
value: 'id',
disabled: 'disabled',
children: 'children'
}"
clearable
default-expand-all
check-strictly
filterable
:size="size"
:placeholder="placeholder"
/>
</div>
</template>
<script lang="ts" setup>
defineOptions({
name: 'cl-menu-select'
});
import { useForm } from '@cool-vue/crud';
import { cloneDeep } from 'lodash-es';
import { computed, ref, useModel, onMounted } from 'vue';
import { useCool } from '/@/cool';
import { deepTree } from '/@/cool/utils';
const props = defineProps({
modelValue: [Number, String],
type: {
type: Number,
default: 1
},
placeholder: String,
size: String
});
const emit = defineEmits(['update:modelValue']);
const { service } = useCool();
const Form = useForm();
// 绑定值
const value = useModel(props, 'modelValue', {
get(val) {
return val ? Number(val) : val;
}
});
// 菜单列表
const list = ref<any[]>([]);
// 树形列表
const tree = computed(() => {
// 过滤掉自己和下级的数据
const data = list.value.filter(
e => e.id != Form.value?.form.id && (props.type === 0 ? e.type == 0 : props.type > e.type!)
);
return deepTree(cloneDeep(data)).filter(e => !e.parentId);
});
// 刷新列表
function refresh() {
service.base.sys.menu.list().then(res => {
list.value = res;
});
}
onMounted(() => {
refresh();
});
</script>
<style lang="scss" scoped>
.cl-menu-select {
:deep(.el-select) {
width: 100%;
}
}
</style>
+61
View File
@@ -0,0 +1,61 @@
<template>
<span class="cl-number">
{{ text }} <span class="cl-number__suffix" v-if="suffix">{{ suffix }}</span>
</span>
</template>
<script setup lang="ts">
defineOptions({
name: 'cl-number'
});
import { useTransition } from '@vueuse/core';
import { computed, type PropType } from 'vue';
const props = defineProps({
modelValue: Number,
value: Number,
fixed: Number,
duration: {
type: Number,
default: 1000
},
type: {
type: String as PropType<'amount' | 'number'>,
default: 'number'
},
suffix: String
});
const transitionedValue = useTransition(
computed(() => props.modelValue || props.value! || 0),
{
duration: props.duration
}
);
const text = computed(() => {
const val = transitionedValue.value;
if (props.type === 'amount') {
// 若需要小数位数控制,可以使用 toFixed 再转数值:
const fixedVal = props.fixed !== undefined ? Number(val.toFixed(props.fixed)) : val;
// 利用 toLocaleString 增加千分位分隔符
return fixedVal.toLocaleString(undefined, {
minimumFractionDigits: props.fixed || 0,
maximumFractionDigits: props.fixed || 0
});
} else {
return val.toFixed(0);
}
});
</script>
<style lang="scss" scoped>
.cl-number {
&__suffix {
font-size: 14px;
}
}
</style>
+94
View File
@@ -0,0 +1,94 @@
import { type ModuleConfig } from '/@/cool';
import { useStore } from './store';
import { config } from '/@/config';
import { t } from '/@/plugins/i18n';
import './static/css/index.scss';
export default (): ModuleConfig => {
return {
order: 99,
ignore: {
NProgress: [
'/base/open/eps',
'/base/comm/person',
'/base/comm/permmenu',
'/base/comm/upload',
'/base/comm/uploadMode'
],
token: ['/login', '/401', '/403', '/404', '/500', '/502']
},
components: Object.values(import.meta.glob('./components/**/*.{vue,tsx}')),
views: [
{
path: '/my/info',
meta: {
label: t('个人中心')
},
component: () => import('./views/info.vue')
}
],
pages: [
{
path: '/login',
component: () => import('./pages/login/index.vue')
},
...['401', '403', '404', '500', '502'].map(code => {
return {
path: `/${code}`,
meta: {
process: false
},
component: () => import(`./pages/error/${code}.vue`)
};
})
],
install() {
// 设置标题
document.title = config.app.name;
// 设置加载文案
const loading = document.querySelector('#Loading');
if (loading) {
const name = loading.querySelector('.preload__name');
const title = loading.querySelector('.preload__title');
const subTitle = loading.querySelector('.preload__sub-title');
if (name) {
name.innerHTML = config.app.name;
}
if (title) {
title.innerHTML = t('正在加载资源...');
}
if (subTitle) {
subTitle.innerHTML = t('初次加载资源可能需要较多时间,请耐心等待');
}
}
},
async onLoad() {
const { user, menu, app } = useStore();
// token 事件
async function hasToken(cb: () => Promise<any> | void) {
if (cb) {
app.addEvent('hasToken', cb);
if (user.token) {
await cb();
}
}
}
await hasToken(async () => {
// 获取用户信息
user.get();
// 获取菜单权限
await menu.get();
});
return {
hasToken
};
}
};
};
+13
View File
@@ -0,0 +1,13 @@
import { checkPerm } from '../utils/permission';
function change(el: HTMLElement, binding: { value: any }) {
el.style.display = checkPerm(binding.value) ? el.getAttribute('_display') || '' : 'none';
}
export default {
created(el: HTMLElement, binding: { value: any }) {
el.setAttribute('_display', el.style.display || '');
change(el, binding);
},
updated: change
};
+9
View File
@@ -0,0 +1,9 @@
import { useStore } from './store';
export function useBase() {
return {
...useStore()
};
}
export * from './utils';
+143
View File
@@ -0,0 +1,143 @@
{
"删除": "Delete",
"新增成员": "Add Member",
"目录": "Directory",
"菜单": "Menu",
"权限": "Permission",
"是否显示": "Show/Hide",
"图标": "Icon",
"节点路由": "Node Route",
"路由缓存": "Route Cache",
"文件路径": "File Path",
"排序号": "Sorting Number",
"节点类型": "Node Type",
"节点名称": "Node Name",
"上级节点": "Parent Node",
"请输入节点路由,如:/test": "Please enter the node route, e.g.: /test",
"开启": "Enable",
"关闭": "Disable",
"请填写排序号": "Please fill in the sorting number",
"导入": "Import",
"如遇到问题无法导入菜单,请检查文件并尝试重新导入。": "If you encounter problems importing the menu, please check the file and try to import again.",
"角色标签": "Character Tag",
"请填写新密码": "Please fill in the new password",
"保存修改": "Save Changes",
"修改成功": "Modification Successful",
"拼命加载中": "Loading拼命",
"转移": "Transfer",
"搜索用户名、姓名": "Search Username, Name",
"用户列表": "User List",
"用户名": "Username",
"姓名": "Name",
"部门名称": "Department Name",
"角色": "Role",
"状态": "Status",
"手机号码": "Mobile Phone Number",
"选择头像": "Select Avatar",
"密码": "Password",
"密码长度在 6 到 16 个字符": "Password length should be between 6 and 16 characters",
"邮箱": "Email",
"启用": "Enable",
"禁用": "Disable",
"部门转移": "Department Transfer",
"请输入备注": "Please enter remarks",
"清空": "Clear",
"日志保存天数": "Log save days",
"搜索请求地址、用户昵称、ip": "Search request address, user nickname, IP",
"用户ID": "User ID",
"用户昵称": "User nickname",
"请求地址": "Request address",
"参数": "Parameter",
"请求时间": "Request time",
"保存成功": "Save successful",
"是否要清空日志?": "Do you want to clear the log?",
"提示": "Tip",
"清空成功": "Clear successful",
"基本信息": "Basic information",
"头像": "Avatar",
"昵称": "Nickname",
"请填写昵称": "Please fill in the nickname",
"原密码": "Original password",
"请填写原密码": "Please fill in the original password",
"新密码": "New password",
"菜单导入": "Menu Import",
"添加": "Add",
"导入成功": "Import Success",
"{file}文件格式错误:{error}": "{file} File Format Error: {error}",
"导出": "Export",
"选择菜单": "Select Menu",
"请先选择要导出的菜单": "Please select the menu to export first",
"菜单数据": "Menu Data",
"退出登录": "Log out",
"确定退出登录吗?": "Are you sure you want to log out?",
"搜索关键字": "Search Keyword",
"关闭当前": "Close Current",
"关闭其他": "Close Others",
"关闭所有": "Close All",
"{label} 没有子菜单,请先添加": "{label} has no sub-menus. Please add them first",
"快速开发后台权限管理系统": "Quick Development Background Permission Management System",
"请输入用户名": "Please enter your username",
"请输入密码": "Please enter your password",
"验证码": "Verification Code",
"登录": "Log in",
"用户名不能为空": "Username cannot be empty",
"密码不能为空": "Password cannot be empty",
"图片验证码不能为空": "Image verification code cannot be empty",
"验证码获取失败": "Failed to obtain verification code",
"马上回来": "Be right back",
"糟糕,出了点问题": "Oops, something went wrong",
"找不到您要查找的页面": "Page not found",
"您无权访问此页面": "You are not authorized to access this page",
"认证失败,请重新登录!": "Authentication failed, please log in again!",
"返回首页": "Return to home page",
"重新登录": "Log in again",
"返回登录页": "Return to login page",
"自定义输入": "Custom input",
"请输入": "Please enter",
"输入关键字进行过滤": "Enter keywords for filtering",
"复制": "Copy",
"行为": "Behavior",
"ip": "IP",
"数据类型 0-字符串 1-富文本 2-文件 ": "Data type 0 - String 1 - Rich text 2 - File",
"键": "Key",
"选择部门": "Select Department",
"请选择部门": "Please Select Department",
"转移到新部门,是否继续?": "Transfer to a new department. Continue?",
"转移成功": "Transfer Successful",
"组织架构": "Organization Structure",
"刷新": "Refresh",
"拖动排序": "Drag to Sort",
"编辑部门": "Edit Department",
"上级部门": "Superior Department",
"排序": "Sort",
"新增部门 “{name}” 成功": "Successfully added new department “{name}”",
"删除成功": "Delete Successful",
"“{name}” 部门的用户已成功转移到 “{parentName}” 部门。": "Users in the “{name}” department have been successfully transferred to the “{parentName}” department.",
"此操作将会删除 “{name}” 部门的所有用户,是否确认?": "This operation will delete all users in the “{name}” department. Are you sure?",
"直接删除": "Delete Directly",
"保留用户": "Keep Users",
"部门架构已发生改变,是否保存?": "The department structure has changed. Do you want to save?",
"更新排序成功": "Successfully updated sorting",
"新增": "Add",
"编辑": "Edit",
"个人中心": "Personal Center",
"正在加载资源...": "Loading resources...",
"初次加载资源可能需要较多时间,请耐心等待": "It may take some time for the initial resource loading. Please wait patiently.",
"搜索名称": "Search Name",
"是否关联上下级": "Whether to associate with superiors and subordinates",
"名称": "Name",
"标识": "Identifier",
"备注": "Remarks",
"功能权限": "Function Permissions",
"数据权限": "Data Permissions",
"创建时间": "Creation Time",
"更新时间": "Update Time",
"数据类型": "Data Type",
"搜索名称、keyName": "Search Name, keyName",
"字符串": "String",
"富文本": "Rich Text",
"文件": "File",
"请输入Key": "Please enter Key",
"类型": "Type",
"数据": "Data"
}
+143
View File
@@ -0,0 +1,143 @@
{
"个人中心": "个人中心",
"正在加载资源...": "正在加载资源...",
"初次加载资源可能需要较多时间,请耐心等待": "初次加载资源可能需要较多时间,请耐心等待",
"搜索名称": "搜索名称",
"是否关联上下级": "是否关联上下级",
"名称": "名称",
"标识": "标识",
"备注": "备注",
"功能权限": "功能权限",
"数据权限": "数据权限",
"创建时间": "创建时间",
"更新时间": "更新时间",
"数据类型": "数据类型",
"搜索名称、keyName": "搜索名称、keyName",
"字符串": "字符串",
"富文本": "富文本",
"文件": "文件",
"请输入Key": "请输入Key",
"类型": "类型",
"数据": "数据",
"请输入备注": "请输入备注",
"清空": "清空",
"日志保存天数": "日志保存天数",
"搜索请求地址、用户昵称、ip": "搜索请求地址、用户昵称、ip",
"用户ID": "用户ID",
"用户昵称": "用户昵称",
"请求地址": "请求地址",
"参数": "参数",
"请求时间": "请求时间",
"保存成功": "保存成功",
"是否要清空日志?": "是否要清空日志?",
"提示": "提示",
"清空成功": "清空成功",
"基本信息": "基本信息",
"头像": "头像",
"昵称": "昵称",
"请填写昵称": "请填写昵称",
"原密码": "原密码",
"请填写原密码": "请填写原密码",
"新密码": "新密码",
"请填写新密码": "请填写新密码",
"保存修改": "保存修改",
"修改成功": "修改成功",
"拼命加载中": "拼命加载中",
"转移": "转移",
"搜索用户名、姓名": "搜索用户名、姓名",
"用户列表": "用户列表",
"用户名": "用户名",
"姓名": "姓名",
"部门名称": "部门名称",
"角色": "角色",
"状态": "状态",
"手机号码": "手机号码",
"选择头像": "选择头像",
"密码": "密码",
"密码长度在 6 到 16 个字符": "密码长度在 6 到 16 个字符",
"邮箱": "邮箱",
"启用": "启用",
"禁用": "禁用",
"部门转移": "部门转移",
"选择部门": "选择部门",
"请选择部门": "请选择部门",
"转移到新部门,是否继续?": "转移到新部门,是否继续?",
"转移成功": "转移成功",
"组织架构": "组织架构",
"刷新": "刷新",
"拖动排序": "拖动排序",
"编辑部门": "编辑部门",
"上级部门": "上级部门",
"排序": "排序",
"新增部门 “{name}” 成功": "新增部门 “{name}” 成功",
"删除成功": "删除成功",
"“{name}” 部门的用户已成功转移到 “{parentName}” 部门。": "“{name}” 部门的用户已成功转移到 “{parentName}” 部门。",
"此操作将会删除 “{name}” 部门的所有用户,是否确认?": "此操作将会删除 “{name}” 部门的所有用户,是否确认?",
"直接删除": "直接删除",
"保留用户": "保留用户",
"部门架构已发生改变,是否保存?": "部门架构已发生改变,是否保存?",
"更新排序成功": "更新排序成功",
"新增": "新增",
"编辑": "编辑",
"删除": "删除",
"新增成员": "新增成员",
"目录": "目录",
"菜单": "菜单",
"权限": "权限",
"是否显示": "是否显示",
"图标": "图标",
"节点路由": "节点路由",
"路由缓存": "路由缓存",
"文件路径": "文件路径",
"排序号": "排序号",
"节点类型": "节点类型",
"节点名称": "节点名称",
"上级节点": "上级节点",
"请输入节点路由,如:/test": "请输入节点路由,如:/test",
"开启": "开启",
"关闭": "关闭",
"请填写排序号": "请填写排序号",
"导入": "导入",
"如遇到问题无法导入菜单,请检查文件并尝试重新导入。": "如遇到问题无法导入菜单,请检查文件并尝试重新导入。",
"菜单导入": "菜单导入",
"添加": "添加",
"导入成功": "导入成功",
"{file}文件格式错误:{error}": "{file}文件格式错误:{error}",
"导出": "导出",
"选择菜单": "选择菜单",
"请先选择要导出的菜单": "请先选择要导出的菜单",
"菜单数据": "菜单数据",
"退出登录": "退出登录",
"确定退出登录吗?": "确定退出登录吗?",
"搜索关键字": "搜索关键字",
"关闭当前": "关闭当前",
"关闭其他": "关闭其他",
"关闭所有": "关闭所有",
"{label} 没有子菜单,请先添加": "{label} 没有子菜单,请先添加",
"快速开发后台权限管理系统": "快速开发后台权限管理系统",
"请输入用户名": "请输入用户名",
"请输入密码": "请输入密码",
"验证码": "验证码",
"登录": "登录",
"用户名不能为空": "用户名不能为空",
"密码不能为空": "密码不能为空",
"图片验证码不能为空": "图片验证码不能为空",
"验证码获取失败": "验证码获取失败",
"马上回来": "马上回来",
"糟糕,出了点问题": "糟糕,出了点问题",
"找不到您要查找的页面": "找不到您要查找的页面",
"您无权访问此页面": "您无权访问此页面",
"认证失败,请重新登录!": "认证失败,请重新登录!",
"返回首页": "返回首页",
"重新登录": "重新登录",
"返回登录页": "返回登录页",
"自定义输入": "自定义输入",
"请输入": "请输入",
"输入关键字进行过滤": "输入关键字进行过滤",
"复制": "复制",
"行为": "行为",
"ip": "ip",
"数据类型 0-字符串 1-富文本 2-文件 ": "数据类型 0-字符串 1-富文本 2-文件 ",
"键": "键",
"角色标签": "角色标签"
}
+143
View File
@@ -0,0 +1,143 @@
{
"请填写新密码": "請填寫新密碼",
"保存修改": "保存修改",
"修改成功": "修改成功",
"拼命加载中": "拼命加載中",
"转移": "轉移",
"搜索用户名、姓名": "搜索用戶名、姓名",
"用户列表": "用戶列表",
"用户名": "用戶名",
"姓名": "姓名",
"部门名称": "部門名稱",
"角色": "角色",
"状态": "狀態",
"手机号码": "手機號碼",
"选择头像": "選擇頭像",
"密码": "密碼",
"密码长度在 6 到 16 个字符": "密碼長度在6到16個字符",
"邮箱": "郵箱",
"启用": "啟用",
"禁用": "禁用",
"部门转移": "部門轉移",
"请输入备注": "請輸入備註",
"清空": "清空",
"日志保存天数": "日誌保存天數",
"搜索请求地址、用户昵称、ip": "搜索請求地址、用戶暱稱、ip",
"用户ID": "用戶ID",
"用户昵称": "用戶暱稱",
"请求地址": "請求地址",
"参数": "參數",
"请求时间": "請求時間",
"保存成功": "保存成功",
"是否要清空日志?": "是否要清空日誌?",
"提示": "提示",
"清空成功": "清空成功",
"基本信息": "基本信息",
"头像": "頭像",
"昵称": "暱稱",
"请填写昵称": "請填寫暱稱",
"原密码": "原密碼",
"请填写原密码": "請填寫原密碼",
"新密码": "新密碼",
"角色标签": "角色標籤",
"个人中心": "個人中心",
"正在加载资源...": "正在加載資源...",
"初次加载资源可能需要较多时间,请耐心等待": "初次加載資源可能需要較多時間,請耐心等待",
"搜索名称": "搜索名稱",
"是否关联上下级": "是否關聯上下級",
"名称": "名稱",
"标识": "標識",
"备注": "備註",
"功能权限": "功能權限",
"数据权限": "數據權限",
"创建时间": "創建時間",
"更新时间": "更新時間",
"数据类型": "數據類型",
"搜索名称、keyName": "搜索名稱、keyName",
"字符串": "字串",
"富文本": "富文本",
"文件": "文件",
"请输入Key": "請輸入Key",
"类型": "類型",
"数据": "數據",
"删除": "刪除",
"新增成员": "新增成員",
"目录": "目錄",
"菜单": "菜單",
"权限": "權限",
"是否显示": "是否顯示",
"图标": "圖標",
"节点路由": "節點路由",
"路由缓存": "路由緩存",
"文件路径": "文件路徑",
"排序号": "排序號",
"节点类型": "節點類型",
"节点名称": "節點名稱",
"上级节点": "上級節點",
"请输入节点路由,如:/test": "請輸入節點路由,如:/test",
"开启": "開啟",
"关闭": "關閉",
"请填写排序号": "請填寫排序號",
"导入": "導入",
"如遇到问题无法导入菜单,请检查文件并尝试重新导入。": "如遇到問題無法導入菜單,請檢查文件並嘗試重新導入。",
"用户名不能为空": "用戶名不得為空",
"密码不能为空": "密碼不得為空",
"图片验证码不能为空": "圖片驗證碼不得為空",
"验证码获取失败": "驗證碼獲取失敗",
"马上回来": "馬上回來",
"糟糕,出了点问题": "糟糕,出了點問題",
"找不到您要查找的页面": "找不到您要查找的頁面",
"您无权访问此页面": "您無權訪問此頁面",
"认证失败,请重新登录!": "認證失敗,請重新登錄!",
"返回首页": "返回首頁",
"重新登录": "重新登錄",
"返回登录页": "返回登錄頁",
"自定义输入": "自定義輸入",
"请输入": "請輸入",
"输入关键字进行过滤": "輸入關鍵字進行過濾",
"复制": "複製",
"行为": "行為",
"ip": "IP",
"数据类型 0-字符串 1-富文本 2-文件 ": "數據類型 0-字串 1-富文本 2-文件 ",
"键": "鍵",
"菜单导入": "菜單導入",
"添加": "添加",
"导入成功": "導入成功",
"{file}文件格式错误:{error}": "{file}文件格式錯誤:{error}",
"导出": "導出",
"选择菜单": "選擇菜單",
"请先选择要导出的菜单": "請先選擇要導出的菜單",
"菜单数据": "菜單數據",
"退出登录": "退出登錄",
"确定退出登录吗?": "確定退出登錄嗎?",
"搜索关键字": "搜索關鍵字",
"关闭当前": "關閉當前",
"关闭其他": "關閉其他",
"关闭所有": "關閉所有",
"{label} 没有子菜单,请先添加": "{label} 沒有子菜單,請先添加",
"快速开发后台权限管理系统": "快速開發後台權限管理系統",
"请输入用户名": "請輸入用戶名",
"请输入密码": "請輸入密碼",
"验证码": "驗證碼",
"登录": "登錄",
"选择部门": "選擇部門",
"请选择部门": "請選擇部門",
"转移到新部门,是否继续?": "轉移到新部門,是否繼續?",
"转移成功": "轉移成功",
"组织架构": "組織架構",
"刷新": "刷新",
"拖动排序": "拖動排序",
"编辑部门": "編輯部門",
"上级部门": "上級部門",
"排序": "排序",
"新增部门 “{name}” 成功": "新增部門 “{name}” 成功",
"删除成功": "刪除成功",
"“{name}” 部门的用户已成功转移到 “{parentName}” 部门。": "“{name}” 部門的用戶已成功轉移到 “{parentName}” 部門。",
"此操作将会删除 “{name}” 部门的所有用户,是否确认?": "此操作將會刪除 “{name}” 部門的所有用戶,是否確認?",
"直接删除": "直接刪除",
"保留用户": "保留用戶",
"部门架构已发生改变,是否保存?": "部門架構已發生改變,是否保存?",
"更新排序成功": "更新排序成功",
"新增": "新增",
"编辑": "編輯"
}
+11
View File
@@ -0,0 +1,11 @@
<template>
<error-page :code="401" :desc="$t('认证失败,请重新登录!')" />
</template>
<script lang="ts" setup>
defineOptions({
name: '401'
});
import ErrorPage from './components/error-page.vue';
</script>
+11
View File
@@ -0,0 +1,11 @@
<template>
<error-page :code="403" :desc="$t('您无权访问此页面')" />
</template>
<script lang="ts" setup>
defineOptions({
name: '403'
});
import ErrorPage from './components/error-page.vue';
</script>
+11
View File
@@ -0,0 +1,11 @@
<template>
<error-page :code="404" :desc="$t('找不到您要查找的页面')" />
</template>
<script lang="ts" setup>
defineOptions({
name: '404'
});
import ErrorPage from './components/error-page.vue';
</script>
+11
View File
@@ -0,0 +1,11 @@
<template>
<error-page :code="500" :desc="$t('糟糕,出了点问题')" />
</template>
<script lang="ts" setup>
defineOptions({
name: '500'
});
import ErrorPage from './components/error-page.vue';
</script>
+11
View File
@@ -0,0 +1,11 @@
<template>
<error-page :code="502" :desc="$t('马上回来')" />
</template>
<script lang="ts" setup>
defineOptions({
name: '502'
});
import ErrorPage from './components/error-page.vue';
</script>
@@ -0,0 +1,140 @@
<template>
<div class="error-page">
<div class="error-page__wrap">
<h1 class="error-page__code">
<span v-for="c in codes" :key="c">
{{ c }}
</span>
</h1>
<p class="error-page__desc">{{ desc }}</p>
<template v-if="user.token || isLogout">
<div class="error-page__btns">
<el-button @click="home">{{ $t('返回首页') }}</el-button>
<el-button type="primary" @click="reLogin">{{ $t('重新登录') }}</el-button>
</div>
</template>
<template v-else>
<div class="error-page__btns">
<el-button type="primary" @click="toLogin">{{ $t('返回登录页') }}</el-button>
</div>
</template>
</div>
<div class="error-page__bg is-tl">
<cl-svg name="bg" />
</div>
<div class="error-page__bg is-br">
<cl-svg name="bg" />
</div>
</div>
</template>
<script lang="ts" setup>
defineOptions({
name: 'error-page'
});
import { computed, ref } from 'vue';
import { useCool } from '/@/cool';
import { useBase } from '/$/base';
const props = defineProps({
code: Number,
desc: String
});
const { router } = useCool();
const { user } = useBase();
const isLogout = ref(false);
const codes = computed(() => {
return (props.code || '').toString().split('');
});
function toLogin() {
router.push('/login');
}
async function reLogin() {
isLogout.value = true;
user.logout();
}
function home() {
router.push('/');
}
</script>
<style lang="scss" scoped>
.error-page {
background-color: #fff;
position: relative;
height: 100%;
&__wrap {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
position: relative;
z-index: 9;
}
&__bg {
position: absolute;
height: 100%;
width: 50%;
pointer-events: none;
transform: rotate(180deg) scaleY(-1);
.cl-svg {
height: 100%;
width: 100%;
fill: #2c3142;
}
&.is-tl {
left: 0;
top: 0;
transform: rotate(180deg) scaleY(-1);
}
&.is-br {
top: 0;
right: 0;
transform: scaleY(-1);
}
}
&__code {
font-size: 120px;
font-weight: normal;
color: #6c757d;
font-family: Consolas;
margin-top: -40px;
animation: dou 1s infinite linear;
position: relative;
}
&__desc {
font-size: 16px;
font-weight: 400;
color: #6c757d;
margin-top: 30px;
}
&__btns {
display: flex;
margin-top: 40px;
.el-button {
margin: 0 10px;
}
}
}
</style>
@@ -0,0 +1,110 @@
<template>
<div class="pic-captcha" @click="refresh">
<div v-if="svg" class="svg" v-html="svg" />
<img v-else-if="base64" class="base64" :src="base64" alt="" />
<template v-else>
<el-icon class="is-loading" :size="18">
<loading />
</el-icon>
</template>
</div>
</template>
<script lang="ts" setup>
defineOptions({
name: 'pic-captcha'
});
import { onMounted, ref } from 'vue';
import { ElMessageBox } from 'element-plus';
import { Loading } from '@element-plus/icons-vue';
import { useCool } from '/@/cool';
import { useI18n } from 'vue-i18n';
const emit = defineEmits(['update:modelValue', 'change']);
const { service } = useCool();
const { t } = useI18n();
// base64
const base64 = ref('');
// svg
const svg = ref('');
// 刷新
async function refresh() {
svg.value = '';
base64.value = '';
await service.base.open
.captcha({
height: 45,
width: 150,
color: '#2c3142'
})
.then(({ captchaId, data }) => {
if (data) {
if (data.includes(';base64,')) {
base64.value = data;
} else {
svg.value = data;
}
emit('update:modelValue', captchaId);
emit('change', {
base64,
svg,
captchaId
});
} else {
ElMessageBox.alert(t('验证码获取失败'), {
title: t('提示'),
type: 'error'
});
}
})
.catch(err => {
ElMessageBox.alert(err.message, {
title: t('提示'),
type: 'error'
});
});
}
onMounted(() => {
refresh();
});
defineExpose({
refresh
});
</script>
<style lang="scss" scoped>
.pic-captcha {
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
height: 45px;
width: 150px;
position: relative;
user-select: none;
.svg {
height: 100%;
position: relative;
}
.base64 {
height: 100%;
}
.is-loading {
position: absolute;
right: 15px;
}
}
</style>
+313
View File
@@ -0,0 +1,313 @@
<template>
<div class="page-login">
<div class="box">
<div class="logo">
<div class="icon">
<img src="/logo.png" alt="Logo" />
</div>
<span>{{ app.info.name }}</span>
</div>
<p class="desc">{{ $t('快速开发后台权限管理系统') }}</p>
<div class="form">
<el-form label-position="top" class="form" :disabled="saving">
<el-form-item :label="$t('用户名')">
<el-input
v-model="form.username"
:placeholder="$t('请输入用户名')"
maxlength="20"
/>
</el-form-item>
<el-form-item :label="$t('密码')">
<el-input
v-model="form.password"
type="password"
:placeholder="$t('请输入密码')"
maxlength="20"
show-password
autocomplete="new-password"
/>
</el-form-item>
<el-form-item :label="$t('验证码')">
<el-input
v-model="form.verifyCode"
:placeholder="$t('验证码')"
maxlength="4"
@keyup.enter="toLogin"
>
<template #suffix>
<pic-captcha
:ref="setRefs('picCaptcha')"
v-model="form.captchaId"
@change="
() => {
form.verifyCode = '';
}
"
/>
</template>
</el-input>
</el-form-item>
<div class="op">
<el-button type="primary" :loading="saving" @click="toLogin">
{{ $t('登录') }}
</el-button>
</div>
</el-form>
</div>
</div>
<div class="bg">
<cl-svg name="bg"></cl-svg>
</div>
<a href="https://cool-js.com" class="copyright"> Copyright © COOL </a>
</div>
</template>
<script lang="ts" setup>
defineOptions({
name: 'login'
});
import { reactive, ref, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { useCool } from '/@/cool';
import { useBase } from '/$/base';
import { storage } from '/@/cool/utils';
import { useI18n } from 'vue-i18n';
import PicCaptcha from './components/pic-captcha.vue';
const { refs, setRefs, router, service } = useCool();
const { user, app } = useBase();
const { t } = useI18n();
// 状态
const saving = ref(false);
// 表单数据
const form = reactive({
username: storage.get('username') || '',
password: '',
captchaId: '',
verifyCode: ''
});
// 演示模式
if (import.meta.env.MODE == 'demo') {
form.username = 'admin';
form.password = '123456';
}
// demo 自动登录
onMounted(() => {
// 先清除旧 token,防止路由重定向
user.clear();
form.username = 'admin';
form.password = '123456';
form.captchaId = 'auto';
form.verifyCode = '0000';
setTimeout(() => {
toLogin();
}, 800);
});
// 登录
async function toLogin() {
saving.value = true;
try {
// 登录 - demo 模式用固定账号
await service.base.open.login({
username: 'admin',
password: '123456',
captchaId: form.captchaId || 'auto',
verifyCode: form.verifyCode || '0000'
}).then(user.setToken);
// token 事件
await Promise.all(app.events.hasToken.map(e => e()));
// 设置缓存
storage.set('username', 'admin');
// 跳转首页 - demo 模式刷新页面走完整初始化
window.location.href = '/';
} catch (err) {
// 刷新验证码
refs.picCaptcha.refresh();
// 提示错误
ElMessageBox.alert((err as Error).message, {
title: t('提示'),
type: 'error'
});
}
saving.value = false;
}
</script>
<style lang="scss" scoped>
$color: #2c3142;
.page-login {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
position: relative;
background-color: #fff;
color: $color;
.bg {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 90%;
pointer-events: none;
transform: rotate(180deg) scaleY(-1);
.cl-svg {
height: 100%;
width: 100%;
}
}
.copyright {
position: absolute;
bottom: 15px;
left: 0;
text-align: center;
width: 100%;
color: var(--el-color-info);
font-size: 14px;
user-select: none;
}
.box {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
width: 50%;
position: absolute;
right: 0;
top: 0;
z-index: 9;
.logo {
height: 50px;
margin-bottom: 20px;
display: flex;
align-items: center;
user-select: none;
.icon {
border-radius: 8px;
padding: 5px;
margin-right: 10px;
background-color: $color;
img {
height: 36px;
}
}
span {
font-size: 38px;
font-weight: bold;
line-height: 1;
letter-spacing: 3px;
}
}
.desc {
font-size: 15px;
letter-spacing: 1px;
margin-bottom: 50px;
user-select: none;
max-width: 80%;
text-align: center;
}
.form {
width: 300px;
:deep(.el-form) {
.el-form-item {
margin-bottom: 20px;
}
.el-form-item__label {
color: var(--el-color-info);
padding-left: 5px;
user-select: none;
}
.el-input {
box-sizing: border-box;
font-size: 15px;
border: 0;
border-radius: 0;
background-color: #f8f8f8;
padding: 0 5px;
border-radius: 8px;
position: relative;
&__wrapper {
box-shadow: none;
background-color: transparent;
}
&__inner {
height: 45px;
color: #333;
}
&:-webkit-autofill {
-webkit-box-shadow: 0 0 0 1000px #f8f8f8 inset;
box-shadow: 0 0 0 1000px #f8f8f8 inset;
}
}
}
:deep(.pic-captcha) {
position: absolute;
right: -5px;
top: 0;
}
}
.op {
display: flex;
justify-content: center;
margin-top: 40px;
:deep(.el-button) {
height: 45px;
width: 100%;
font-size: 16px;
border-radius: 8px;
letter-spacing: 1px;
}
}
}
}
@media screen and (max-width: 1024px) {
.page-login {
.box {
width: 100%;
}
}
}
</style>
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 500 350"
>
<g transform="">
<g transform="translate(628,-17) scale(100)" opacity="0.3">
<path
d="M4.10125 0 C4.10125 0.5525 4.3542 0.8338 4.1835 1.3593 S3.6427 1.9637 3.318 2.4107 S3.0325 3.2339 2.5855 3.5587 S1.7928 3.7298 1.2674 3.9005 S0.5525 4.3988 0 4.3988 S-0.7419 4.0713 -1.2674 3.9005 S-2.1385 3.8834 -2.5855 3.5587 S-2.9932 2.8576 -3.318 2.4107 S-4.0127 1.8847 -4.1835 1.3593 S-4.1013 0.5525 -4.1013 0 S-4.3542 -0.8338 -4.1835 -1.3593 S-3.6427 -1.9637 -3.318 -2.4107 S-3.0325 -3.2339 -2.5855 -3.5587 S-1.7928 -3.7298 -1.2674 -3.9005 S-0.5525 -4.3988 0 -4.3988 S0.7419 -4.0713 1.2674 -3.9005 S2.1385 -3.8834 2.5855 -3.5587 S2.9932 -2.8576 3.318 -2.4107 S4.0127 -1.8847 4.1835 -1.3593 S4.1013 -0.5525 4.1013 0"
stroke-width="0"
transform="rotate(19)"
>
<animateTransform
attributeName="transform"
type="rotate"
dur="10s"
repeatCount="indefinite"
values="0;36"
></animateTransform>
</path>
</g>
<g transform="translate(704,-56) scale(100)" opacity="0.9">
<path
d="M4.9215 0 C4.9215 0.663 5.225 1.0006 5.0202 1.6311 S4.3713 2.3564 3.9816 2.8928 S3.639 3.8807 3.1026 4.2704 S2.1514 4.4757 1.5208 4.6806 S0.663 5.2785 0 5.2785 S-0.8903 4.8855 -1.5208 4.6806 S-2.5662 4.6601 -3.1026 4.2704 S-3.5919 3.4292 -3.9816 2.8928 S-4.8153 2.2617 -5.0202 1.6311 S-4.9215 0.663 -4.9215 0 S-5.225 -1.0006 -5.0202 -1.6311 S-4.3713 -2.3564 -3.9816 -2.8928 S-3.639 -3.8807 -3.1026 -4.2704 S-2.1514 -4.4757 -1.5208 -4.6806 S-0.663 -5.2785 0 -5.2785 S0.8903 -4.8855 1.5208 -4.6806 S2.5662 -4.6601 3.1026 -4.2704 S3.5919 -3.4292 3.9816 -2.8928 S4.8153 -2.2617 5.0202 -1.6311 S4.9215 -0.663 4.9215 0"
stroke-width="0"
transform="rotate(2.04427)"
>
<animateTransform
attributeName="transform"
type="rotate"
dur="6s"
repeatCount="indefinite"
values="0;36"
></animateTransform>
</path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,151 @@
<template>
<div class="a-menu">
<div
class="a-menu__item"
v-for="(item, index) in list"
:key="item.id"
:class="{
'is-active': index == active
}"
@click="select(index)"
>
<cl-svg class="mr-3" :name="item.icon" :size="16" v-if="item.icon" />
<span class="text-[12px] tracking-wider whitespace-nowrap">{{ item.meta?.label }}</span>
</div>
</div>
</template>
<script lang="ts" setup>
defineOptions({
name: 'a-menu'
});
import { computed, ref, watch } from 'vue';
import { useBase } from '/$/base';
import { useCool } from '/@/cool';
import { ElMessage } from 'element-plus';
import { useI18n } from 'vue-i18n';
const { router, route } = useCool();
const { menu } = useBase();
const { t } = useI18n();
// 选中标识
const active = ref(0);
// 组列表
const list = computed(() => {
return menu.group.filter(e => e.isShow);
});
// 选择导航
function select(index: number) {
if (index == active.value) {
return false;
}
// 选中的组
const item = list.value[index];
// 获取第一个菜单地址
const url = menu.getPath(item);
if (url) {
// 设置左侧菜单
menu.setMenu(index);
// 跳转
router.push(url);
} else {
ElMessage.warning(t('{label} 没有子菜单,请先添加', { label: item.meta?.label }));
}
}
// 刷新
function refresh() {
let index = 0;
function deep(e: Menu.Item, i: number) {
switch (e.type) {
case 0:
if (e.children) {
e.children.forEach(e => {
deep(e, i);
});
}
break;
case 1:
if (route.path.includes(e.path)) {
index = i;
}
break;
default:
break;
}
}
// 遍历所有分组
list.value.forEach(deep);
// 确认选择
active.value = index;
// 设置该分组下的菜单
menu.setMenu(index);
}
// 监听变化
watch(
() => [route.path, menu.group.length],
() => {
refresh();
},
{
immediate: true
}
);
</script>
<style lang="scss" scoped>
.a-menu {
display: flex;
align-items: center;
flex: 1;
user-select: none;
&__item {
display: flex;
align-items: center;
height: 32px;
padding: 0 16px 0 12px;
border: 0;
color: var(--el-color-info);
position: relative;
background-color: transparent;
border-radius: 6px;
cursor: pointer;
border: 1px solid transparent;
margin-right: 6px;
transition: all 0.3s;
&.is-active {
border-color: var(--el-color-primary-light-8);
color: var(--el-color-primary);
}
&.is-active,
&:hover {
background-color: var(--el-color-primary-light-9);
}
&:last-child {
margin-right: 0;
}
}
&__name {
margin-left: 8px;
}
}
</style>
@@ -0,0 +1,153 @@
import { defineComponent, h, watch } from 'vue';
import { useBase } from '/$/base';
import { useCool } from '/@/cool';
import { debounce } from 'lodash-es';
export default defineComponent({
name: 'b-menu',
props: {
keyWord: String
},
setup(props) {
const { router, route, browser, refs, setRefs } = useCool();
const { menu, app } = useBase();
// 页面跳转
function onSelect(url: string) {
if (url != route.path) {
router.push(url);
}
// 小屏下点击收起左侧菜单
if (browser.isMini) {
app.fold(true);
}
}
// 渲染子菜单
function renderMenu() {
function deep(list: Menu.Item[], show?: boolean) {
const keyWord = props.keyWord?.toLowerCase() || '';
function filterMenu(item: Menu.Item): boolean {
if (!item.isShow) return false;
if (show) {
return true;
}
if (item.meta?.label?.toLowerCase().includes(keyWord)) return true;
if (item.children) {
return item.children.some(filterMenu);
}
return false;
}
return list.filter(filterMenu).map(e => {
if (e.meta?.label?.toLowerCase().includes(keyWord)) {
show = true;
}
const item = (e: Menu.Item) => {
const arr = [
<cl-svg name={e.icon} size={18} />,
<span class="ml-4 tracking-wider text-[14px] mr-auto text-ellipsis overflow-hidden whitespace-nowrap">
{e.meta?.label}
</span>
];
if (e.type == 1 && e.badge) {
arr.push(
<div class={['b-menu__badge', `is-${e.badgeColor}`]}>
<span>{e.badge}</span>
</div>
);
}
return arr;
};
if (e.type == 0) {
return h(
<el-sub-menu />,
{
index: String(e.id),
key: e.id,
popperClass: 'app-slider__menu'
},
{
title() {
return item(e);
},
default() {
return deep(e.children || [], show);
}
}
);
} else {
return h(
<el-menu-item />,
{
index: e.meta?.isHome ? '/' : e.path,
key: e.id
},
{
default() {
return item(e);
}
}
);
}
});
}
return deep(menu.list);
}
// 展开所有
const expand = debounce(() => {
if (!props.keyWord) {
return;
}
const deep = (list: Menu.Item[]) => {
list.forEach(e => {
if (e.type == 0) {
try {
refs.menu?.open(String(e.id));
} catch (err) { }
if (e.children) {
deep(e.children);
}
}
});
};
deep(menu.list);
}, 300);
watch(() => props.keyWord, expand);
return () => {
return (
<div class="app-slider__menu">
<el-menu
ref={setRefs('menu')}
default-active={route.path}
background-color="transparent"
collapse-transition={false}
collapse={browser.isMini ? false : app.isFold}
onSelect={onSelect}
popper-offset={10}
>
{renderMenu()}
</el-menu>
</div>
);
};
}
});
@@ -0,0 +1,37 @@
<template>
<component v-for="item in list" :key="item.name" :is="item.component" />
</template>
<script setup lang="ts">
import { isFunction, orderBy } from "lodash-es";
import { markRaw, onMounted, shallowRef } from "vue";
import { module } from "/@/cool";
const list = shallowRef<any[]>([]);
async function refresh() {
const arr = orderBy(
module.list.filter((e) => e.enable !== false && !!e.global).map((e) => e.global),
"order"
);
list.value = await Promise.all(
arr
.filter((e) => e?.component)
.map(async (e) => {
if (e) {
const c = await (isFunction(e.component) ? e.component() : e.component);
return {
...e,
component: markRaw(c.default || c)
};
}
})
);
}
onMounted(() => {
refresh();
});
</script>
@@ -0,0 +1,279 @@
<template>
<div class="app-process">
<ul class="app-process__op">
<li class="cl-comm__icon" @click="toBack">
<cl-svg name="back" />
</li>
<li class="cl-comm__icon" @click="toRefresh">
<cl-svg name="refresh" />
</li>
<li class="cl-comm__icon" @click="toHome">
<cl-svg name="home" />
</li>
</ul>
<div class="app-process__container">
<el-scrollbar :ref="setRefs('scroller')" class="app-process__scroller">
<div class="app-process__list">
<div
v-for="(item, index) in process.list"
:key="index"
:ref="setRefs(`item-${index}`)"
class="app-process__item"
:class="{ active: item.active }"
:data-index="index"
@click="onTap(item, Number(index))"
@contextmenu.stop.prevent="openCM($event, item)"
>
<span class="label tracking-wider">
{{ item.meta.label || item.name || item.path }}
</span>
<cl-svg class="close" name="close" @mousedown.stop="onDel(Number(index))" />
</div>
</div>
</el-scrollbar>
</div>
<ul class="app-process__op">
<li class="cl-comm__icon" @click="toFull">
<cl-svg name="screen-normal" v-if="app.isFull" />
<cl-svg name="screen-full" v-else />
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
defineOptions({
name: 'app-process'
});
import { onMounted, watch } from 'vue';
import { last } from 'lodash-es';
import { useCool } from '/@/cool';
import { ContextMenu } from '@cool-vue/crud';
import { useBase } from '/$/base';
import { useI18n } from 'vue-i18n';
const { refs, setRefs, route, router, mitt } = useCool();
const { process, app } = useBase();
const { t } = useI18n();
// 刷新当前路由
function toRefresh() {
mitt.emit('view.refresh');
}
// 回首页
function toHome() {
router.push('/');
}
// 返回上一页
function toBack() {
router.back();
}
// 设置全屏
function toFull() {
app.setFull(!app.isFull);
}
// 跳转
function toPath() {
const d = process.list.find(e => e.active);
if (!d) {
const next = last(process.list);
router.push(next ? next.fullPath : '/');
}
}
// 移动到
function scrollTo(left: number) {
refs.scroller.wrapRef.scrollTo({
left,
behavior: 'smooth'
});
}
// 调整滚动位置
function adScroll(index: number) {
const el = refs[`item-${index}`];
if (el) {
scrollTo(el.offsetLeft - (refs.scroller.wrapRef.clientWidth + el.clientWidth) / 2);
}
}
// 选择
function onTap(item: Process.Item, index: number) {
adScroll(index);
router.push(item.fullPath);
}
// 删除
function onDel(index: number) {
process.remove(index);
toPath();
}
// 右键菜单
function openCM(e: any, item: Process.Item) {
ContextMenu.open(e, {
list: [
{
label: t('关闭当前'),
hidden: item.path !== route.path,
callback(done) {
done();
process.close();
toPath();
}
},
{
label: t('关闭其他'),
callback(done) {
done();
process.set(process.list.filter(e => e.fullPath == item.fullPath));
toPath();
}
},
{
label: t('关闭所有'),
callback(done) {
done();
process.clear();
toPath();
}
}
]
});
}
watch(
() => route.path,
function (val) {
adScroll(process.list.findIndex(e => e.fullPath === val) || 0);
}
);
onMounted(() => {
// 添加滚轮事件监听器
refs.scroller.wrapRef?.addEventListener(
'wheel',
function (event: WheelEvent) {
// 滚动的速度因子,可以根据需要调整
const scrollSpeed = 2;
// 计算滚动的距离
const distance = event.deltaY * scrollSpeed;
scrollTo(refs.scroller.wrapRef.scrollLeft + distance);
},
{ passive: false }
);
});
</script>
<style lang="scss" scoped>
.app-process {
display: flex;
align-items: center;
position: relative;
padding: 5px 10px;
user-select: none;
background-color: var(--el-bg-color);
margin-bottom: 10px;
overflow: hidden;
&__op {
display: flex;
align-items: center;
list-style: none;
.cl-comm__icon {
margin-right: 5px;
&:last-child {
margin-right: 0;
}
}
}
&__container {
height: 100%;
flex: 1;
position: relative;
margin: 0 5px;
}
&__scroller {
height: 40px;
width: 100%;
white-space: nowrap;
position: absolute;
left: 0;
top: 0;
}
&__item {
display: inline-flex;
align-items: center;
justify-content: space-between;
height: 26px;
padding: 0 8px;
cursor: pointer;
color: var(--el-text-color-regular);
border-radius: var(--el-border-radius-base);
margin-right: 5px;
border: 1px solid var(--el-fill-color-dark);
.close {
width: 0;
overflow: hidden;
transition: width 0.2s ease-in-out;
font-size: 14px;
border-radius: 4px;
opacity: 0;
&:hover {
background-color: rgba(0, 0, 0, 0.1);
}
}
.label {
font-size: 12px;
line-height: 1;
}
&:last-child {
margin-right: 0;
}
&:hover:not(.active) {
background-color: var(--el-fill-color-light);
}
&.active {
background-color: var(--el-color-primary);
border-color: var(--el-color-primary);
color: #fff;
}
&:hover,
&.active {
.close {
margin-left: 10px;
margin-right: -2px;
width: 14px;
opacity: 1;
}
}
}
}
</style>
@@ -0,0 +1,87 @@
<template>
<div class="route-nav">
<el-text class="font-bold" v-if="browser.isMini">
{{ lastName }}
</el-text>
<template v-else>
<el-breadcrumb :separator-icon="ArrowRightBold">
<el-breadcrumb-item v-for="(item, index) in list" :key="index">
<span class="text-[14px]">{{ item.meta?.label || item.name }}</span>
</el-breadcrumb-item>
</el-breadcrumb>
</template>
</div>
</template>
<script lang="ts" setup>
defineOptions({
name: 'route-nav'
});
import { computed } from 'vue';
import { flattenDeep, last } from 'lodash-es';
import { ArrowRightBold } from '@element-plus/icons-vue';
import { useCool } from '/@/cool';
import { useBase } from '/$/base';
const { route, browser } = useCool();
const { menu } = useBase();
// 数据列表
const list = computed(() => {
function deep(item: any) {
if (route.path === '/') {
return false;
}
if (item.path == route.path) {
return item;
} else {
if (item.children) {
const ret = item.children.map(deep).find(Boolean);
if (ret) {
return [item, ret];
} else {
return false;
}
} else {
return false;
}
}
}
return flattenDeep(menu.group.map(deep).filter(Boolean));
});
// 最后一个节点名称
const lastName = computed(() => last(list.value)?.meta?.label);
</script>
<style lang="scss" scoped>
.route-nav {
white-space: nowrap;
user-select: none;
margin-right: 10px;
:deep(.el-breadcrumb) {
.el-breadcrumb__separator {
font-size: 10px;
margin: 0 10px;
}
.el-breadcrumb__inner {
color: var(--el-text-color-regular);
}
.el-breadcrumb__item {
&:last-child {
.el-breadcrumb__inner {
color: var(--el-text-color-primary);
}
}
}
}
}
</style>
@@ -0,0 +1,188 @@
<template>
<div
class="app-slider"
:class="{
'is-collapse': app.isFold
}"
>
<div class="app-slider__logo">
<img src="/logo.png" />
<span v-if="!app.isFold || browser.isMini">{{ app.info.name }}</span>
</div>
<div class="app-slider__search">
<el-input
v-model="keyWord"
:placeholder="$t('搜索关键字')"
clearable
@focus="app.fold(false)"
>
<template #prefix>
<cl-svg name="search" :size="16" />
</template>
</el-input>
</div>
<div class="app-slider__container">
<el-scrollbar>
<b-menu :keyWord="keyWord" />
</el-scrollbar>
</div>
</div>
</template>
<script lang="ts" setup>
defineOptions({
name: 'app-slider'
});
import { useBase } from '/$/base';
import { useBrowser } from '/@/cool';
import BMenu from './bmenu';
import { ref } from 'vue';
const { browser } = useBrowser();
const { app } = useBase();
const keyWord = ref('');
</script>
<style lang="scss">
.app-slider {
$slider-menu-height: 50px;
--slider-bg-color: #2c3147;
--slider-text-color: #e5eaf3;
height: 100%;
background-color: var(--slider-bg-color);
border-right: 1px solid var(--el-border-color-extra-light);
&__logo {
display: flex;
align-items: center;
height: 66px;
padding: 0 21px;
user-select: none;
img {
height: 24px;
width: 24px;
}
span {
color: #fff;
font-weight: bold;
font-size: 20px;
margin-left: 10px;
white-space: nowrap;
letter-spacing: 1px;
}
}
&__search {
margin: 0 10px 10px 10px;
overflow: hidden;
border-radius: 6px;
.el-input__wrapper {
background-color: rgba(200, 200, 200, 0.1);
box-shadow: none;
height: 36px;
padding: 0 14px;
.el-input__inner {
color: var(--slider-text-color);
}
}
}
&__container {
height: calc(100% - 112px);
}
&__menu {
user-select: none;
.b-menu__badge {
display: flex;
align-items: center;
justify-content: center;
height: $slider-menu-height;
font-size: 10px;
height: 14px;
min-width: 14px;
padding: 0 3px;
border-radius: 4px;
background-color: rgba(255, 255, 255, 0.2);
font-weight: bold;
color: #fff;
transition: background-color 0.3s;
}
.el-menu {
width: 100%;
border-right: 0;
background-color: transparent;
&--popup {
border-radius: 6px;
padding: 5px;
&-container {
padding: 0;
}
.el-menu-item,
.el-sub-menu__title {
height: $slider-menu-height;
border-radius: 6px;
&:hover {
background-color: var(--el-fill-color-light);
}
}
}
&:not(&--popup) {
--el-menu-base-level-padding: 23px;
.el-menu-item,
.el-sub-menu__title {
height: $slider-menu-height;
color: var(--slider-text-color);
.cl-svg {
flex-shrink: 0;
}
&.is-active,
&:hover {
background-color: rgba(0, 0, 0, 0.25);
color: #fff;
}
&.is-active {
background-color: var(--el-color-primary);
}
}
}
}
}
&.is-collapse {
.app-slider__search {
.el-input__inner {
opacity: 0;
}
}
.app-slider__menu {
.el-sub-menu {
&.is-active {
background-color: rgba(0, 0, 0, 0.25);
}
}
}
}
}
</style>
@@ -0,0 +1,219 @@
<template>
<div class="app-topbar">
<div class="cl-comm__icon mr-[10px]" @click="app.fold()">
<cl-svg name="fold" v-if="app.isFold" />
<cl-svg name="expand" v-else />
</div>
<!-- 路由导航 -->
<a-menu v-if="app.info.menu.isGroup" />
<route-nav v-else />
<div class="flex1"></div>
<!-- 工具栏 -->
<ul class="app-topbar__tools">
<li v-for="(item, index) in toolbarComponents" :key="index">
<component :is="item.component" />
</li>
</ul>
<!-- 用户信息 -->
<template v-if="user.info">
<el-dropdown
hide-on-click
popper-class="app-topbar__user-popper"
:popper-options="{}"
@command="onCommand"
>
<div class="app-topbar__user">
<el-text class="mr-[10px]">{{ user.info.nickName }}</el-text>
<cl-avatar :size="26" :src="user.info.headImg" />
</div>
<template #dropdown>
<div class="user">
<cl-avatar :size="34" :src="user.info.headImg" />
<div class="det">
<el-text size="small" tag="p">{{ user.info.nickName }}</el-text>
<el-text size="small" type="info">{{ user.info.email }}</el-text>
</div>
</div>
<el-dropdown-menu>
<el-dropdown-item command="my">
<cl-svg name="my" />
<span>{{ t('个人中心') }}</span>
</el-dropdown-item>
<el-dropdown-item command="exit">
<cl-svg name="exit" />
<span>{{ t('退出登录') }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</div>
</template>
<script lang="ts" setup>
defineOptions({
name: 'app-topbar'
});
import { computed, markRaw, onMounted, reactive } from 'vue';
import { isFunction, orderBy } from 'lodash-es';
import { useBase } from '/$/base';
import { module, useCool } from '/@/cool';
import { ElMessageBox } from 'element-plus';
import { useI18n } from 'vue-i18n';
import RouteNav from './route-nav.vue';
import AMenu from './amenu.vue';
const { router, service, browser } = useCool();
const { user, app } = useBase();
const { t } = useI18n();
// 命令事件
async function onCommand(name: string) {
switch (name) {
case 'my':
router.push('/my/info');
break;
case 'exit':
ElMessageBox.confirm(t('确定退出登录吗?'), t('提示'), {
type: 'warning'
})
.then(async () => {
await service.base.comm.logout();
user.logout();
})
.catch(() => null);
break;
}
}
// 工具栏
const toolbar = reactive({
list: [] as any[],
async init() {
const arr = orderBy(
module.list.filter(e => e.enable !== false && !!e.toolbar).map(e => e.toolbar),
'order'
);
this.list = await Promise.all(
arr
.filter(e => e?.component)
.map(async e => {
if (e) {
const c = await (isFunction(e.component) ? e.component() : e.component);
return {
...e,
component: markRaw(c.default || c)
};
}
})
);
}
});
// 工具栏组件
const toolbarComponents = computed(() => {
return toolbar.list.filter(e => {
if (browser.isMini) {
return e?.h5 ?? true;
}
return e?.pc ?? true;
});
});
onMounted(() => {
toolbar.init();
});
</script>
<style lang="scss" scoped>
.app-topbar {
display: flex;
align-items: center;
height: 46px;
padding: 0 10px;
background-color: var(--el-bg-color);
border-bottom: 1px solid var(--el-border-color-extra-light);
box-sizing: border-box;
transition: height 0.2s ease-in-out;
.flex1 {
flex: 1;
}
&__tools {
display: flex;
margin-right: 10px;
& > li {
display: flex;
justify-content: center;
align-items: center;
list-style: none;
height: 45px;
cursor: pointer;
margin-left: 10px;
}
}
&__user {
display: flex;
align-items: center;
outline: none;
cursor: pointer;
white-space: nowrap;
padding: 5px 5px 5px 10px;
border-radius: 6px;
&:hover {
background-color: var(--el-fill-color-light);
}
}
:deep(.cl-comm__icon) {
&:hover {
border-color: var(--el-color-primary);
background-color: transparent;
}
}
}
</style>
<style lang="scss">
.app-topbar__user-popper {
.el-dropdown-menu__item {
padding: 6px 12px;
font-size: 12px;
}
.user {
display: flex;
align-items: center;
padding: 10px 10px;
width: 200px;
border-bottom: 1px solid var(--el-color-info-light-9);
.det {
margin-left: 10px;
flex: 1;
font-size: 12px;
}
}
.cl-svg {
margin-right: 8px;
font-size: 16px;
}
}
</style>
@@ -0,0 +1,99 @@
<template>
<div class="app-views">
<router-view v-slot="{ Component }">
<transition :name="app.info.router.transition || 'none'">
<keep-alive :key="key" :include="caches">
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
</div>
</template>
<script lang="ts" setup>
defineOptions({
name: 'app-views'
});
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { useBase } from '/$/base';
import { useCool } from '/@/cool';
const { mitt } = useCool();
const { process, app } = useBase();
// 缓存数
const key = ref(1);
// 缓存列表
const caches = computed(() => {
return process.list
.filter(e => e.meta?.keepAlive)
.map(e => {
return e.path.substring(1, e.path.length).replace(/\//g, '-');
});
});
// 刷新页面
function refresh() {
key.value += 1;
}
onMounted(() => {
mitt.on('view.refresh', refresh);
});
onUnmounted(() => {
mitt.off('view.refresh');
});
</script>
<style lang="scss" scoped>
.app-views {
flex: 1;
overflow: hidden;
margin: 0 10px 10px 10px;
width: calc(100% - 20px);
box-sizing: border-box;
border-radius: 6px;
position: relative;
.none-enter-active {
position: absolute;
}
.slide-enter-active {
position: absolute;
top: 0;
width: 100%;
transition: all 0.4s ease-in-out 0.2s;
}
.slide-leave-active {
position: absolute;
top: 0;
width: 100%;
transition: all 0.4s ease-in-out;
}
.slide-enter-to {
transform: translate3d(0, 0, 0);
opacity: 1;
}
.slide-enter-from {
transform: translate3d(-5%, 0, 0);
opacity: 0;
}
.slide-leave-to {
transform: translate3d(5%, 0, 0);
opacity: 0;
}
.slide-leave-from {
transform: translate3d(0, 0, 0);
opacity: 1;
}
}
</style>
+132
View File
@@ -0,0 +1,132 @@
<template>
<div class="app-layout" :class="{ 'is-collapse': app.isFold, 'is-full': app.isFull }">
<div class="app-layout__mask" @click="app.fold(true)"></div>
<div class="app-layout__left">
<slider />
</div>
<div class="app-layout__right">
<topbar />
<process />
<views />
</div>
</div>
</template>
<script lang="ts" setup>
defineOptions({
name: 'app-layout'
});
import { useBase } from '/$/base';
import Topbar from './components/topbar.vue';
import Slider from './components/slider.vue';
import process from './components/process.vue';
import Views from './components/views.vue';
const { app } = useBase();
</script>
<style lang="scss" scoped>
.app-global {
position: absolute;
left: 0;
top: 0;
}
.app-layout {
display: flex;
background-color: var(--bg-color);
height: 100%;
width: 100%;
overflow: hidden;
&__left {
overflow: hidden;
height: 100%;
width: 255px;
transition: left 0.2s;
}
&__right {
display: flex;
flex-direction: column;
height: 100%;
width: calc(100% - 255px);
}
&__mask {
position: fixed;
left: 0;
top: 0;
background-color: rgba(0, 0, 0, 0.5);
height: 100%;
width: 100%;
z-index: 999;
}
@media only screen and (max-width: 768px) {
.app-layout__left {
position: absolute;
left: 0;
z-index: 9999;
transition:
transform 0.3s cubic-bezier(0.7, 0.3, 0.1, 1),
box-shadow 0.3s cubic-bezier(0.7, 0.3, 0.1, 1);
}
.app-layout__right {
width: 100%;
}
&.is-collapse {
.app-layout__left {
transform: translateX(-100%);
}
.app-layout__mask {
display: none;
}
}
}
@media only screen and (min-width: 768px) {
.app-layout__left,
.app-layout__right {
transition: width 0.2s ease-in-out;
}
.app-layout__mask {
display: none;
}
&.is-collapse {
.app-layout__left {
width: 67px;
}
.app-layout__right {
width: calc(100% - 67px);
}
}
}
&.is-full {
.app-layout__left {
width: 0;
}
.app-layout__right {
width: 100%;
:deep(.a-menu),
:deep(.app-topbar) {
padding: 0;
height: 0;
overflow: hidden;
}
}
}
}
</style>
+81
View File
@@ -0,0 +1,81 @@
#app {
height: 100vh;
width: 100vw;
overflow: hidden;
}
:root {
--bg-color: var(--el-fill-color-lighter);
}
a {
text-decoration: none;
}
input,
button {
outline: none;
}
input {
&:-webkit-autofill {
box-shadow: 0 0 0px 1000px white inset;
}
}
// scrollbar
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar:horizontal {
height: 6px;
}
::-webkit-scrollbar-track {
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background-color: #0003;
border-radius: 10px;
transition: all 0.2s ease-in-out;
}
::-webkit-scrollbar-thumb:hover {
cursor: pointer;
background-color: #0000004d;
}
.dark ::-webkit-scrollbar-thumb {
background-color: #fff3;
}
.dark ::-webkit-scrollbar-thumb:hover {
background-color: #fff6;
}
// custom
.cl-comm__icon {
display: flex;
align-items: center;
justify-content: center;
height: 26px;
width: 26px;
background-color: var(--el-bg-color);
border: 1px solid var(--el-fill-color-dark);
border-radius: 6px;
transition: all 0.2s ease-in-out;
outline: none;
cursor: pointer;
flex-shrink: 0;
.cl-svg {
font-size: 16px;
color: var(--el-text-color-primary);
}
&:hover {
background-color: var(--el-fill-color-light);
}
}
+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1737111417178" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10050" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M625.8 526.7H394.4c-24.7 0-44.8-20-44.8-44.8 0-24.7 20-44.8 44.8-44.8h231.4c24.7 0 44.8 20 44.8 44.8 0 24.7-20.1 44.8-44.8 44.8zM625.8 659.3H394.4c-24.7 0-44.8-20-44.8-44.8s20-44.8 44.8-44.8h231.4c24.7 0 44.8 20 44.8 44.8s-20.1 44.8-44.8 44.8z" p-id="10051"></path><path d="M510.1 745.2c-24.7 0-44.8-20-44.8-44.8V502c0-24.7 20-44.8 44.8-44.8 24.7 0 44.8 20 44.8 44.8v198.4c0 24.7-20.1 44.8-44.8 44.8z" p-id="10052"></path><path d="M492.8 500.2c-11.5 0-22.9-4.4-31.7-13.1l-88.6-88.6c-17.5-17.5-17.5-45.8 0-63.3s45.8-17.5 63.3 0l88.6 88.6c17.5 17.5 17.5 45.8 0 63.3-8.6 8.7-20.1 13.1-31.6 13.1z" p-id="10053"></path><path d="M525.7 500.2c-11.5 0-22.9-4.4-31.7-13.1-17.5-17.5-17.5-45.8 0-63.3l90.3-90.3c17.5-17.5 45.8-17.5 63.3 0s17.5 45.8 0 63.3l-90.3 90.3c-8.7 8.7-20.1 13.1-31.6 13.1z" p-id="10054"></path><path d="M510.1 959.7C263.2 959.7 62.4 758.9 62.4 512S263.2 64.2 510.1 64.2 957.9 265.1 957.9 512 757 959.7 510.1 959.7z m0-805.9c-197.5 0-358.2 160.7-358.2 358.2s160.7 358.2 358.2 358.2S868.3 709.5 868.3 512 707.6 153.8 510.1 153.8z" p-id="10055"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg
t="1735123259958"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="8100"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="256"
height="256"
>
<path
d="M653.8 840.4c12.1 0 24.2-4.6 33.4-13.8 18.5-18.5 18.5-48.3 0-66.8L437.5 510.1l249.7-249.7c18.5-18.5 18.5-48.3 0-66.8s-48.3-18.5-66.8 0L337.3 476.7c-18.5 18.5-18.5 48.3 0 66.8l283.1 283.1c9.2 9.2 21.3 13.8 33.4 13.8z"
p-id="8101"
></path>
</svg>

After

Width:  |  Height:  |  Size: 596 B

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1736686029767" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5153" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M639.6 685.9c-11.5 0-22.9-4.4-31.7-13.1L351.2 416c-17.5-17.5-17.5-45.9 0-63.4s45.9-17.5 63.4 0l256.7 256.7c17.5 17.5 17.5 45.9 0 63.4-8.8 8.8-20.3 13.2-31.7 13.2z" p-id="5154"></path><path d="M382.8 685.9c-11.5 0-22.9-4.4-31.7-13.1-17.5-17.5-17.5-45.9 0-63.4l256.7-256.7c17.5-17.5 45.9-17.5 63.4 0s17.5 45.9 0 63.4L414.5 672.7c-8.7 8.8-20.2 13.2-31.7 13.2z" p-id="5155"></path><path d="M511.2 960.7c-114.8 0-229.5-43.7-316.9-131.1-174.8-174.8-174.8-459.1 0-633.9C369 21 653.4 21 828.1 195.8c174.8 174.8 174.8 459.1 0 633.9-87.3 87.3-202.1 131-316.9 131z m0-806.4c-91.8 0-183.6 35-253.5 104.8-139.8 139.8-139.8 367.3 0 507.1s367.3 139.8 507.1 0 139.8-367.3 0-507.1C694.9 189.3 603 154.3 511.2 154.3z m285.3 643.6h0.2-0.2z" p-id="5156"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg
t="1736594824258"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="4443"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="256"
height="256"
>
<path
d="M806.2 281.8L281.9 806.2c-17.5 17.5-46 17.5-63.6 0-17.5-17.5-17.5-46 0-63.6l524.3-524.3c17.5-17.5 46-17.5 63.6 0s17.6 46 0 63.5z"
p-id="4444"
></path>
<path
d="M806.2 806.2c-17.5 17.5-46 17.5-63.6 0L218.3 281.8c-17.5-17.5-17.5-46 0-63.6s46-17.5 63.6 0l524.3 524.3c17.6 17.7 17.6 46.1 0 63.7z"
p-id="4445"
></path>
</svg>

After

Width:  |  Height:  |  Size: 678 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1736691229720" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4788" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M883.1 308.6H141.5c-24.8 0-44.9-20.1-44.9-44.9s20.1-44.9 44.9-44.9H883c24.8 0 44.9 20.1 44.9 44.9s-20 44.9-44.8 44.9z" p-id="4789"></path><path d="M629.6 308.6H394.9c-24.8 0-44.9-20.1-44.9-44.9v-37.6c0-89.5 72.8-162.3 162.3-162.3s162.3 72.8 162.3 162.3v37.6c0 24.8-20.1 44.9-45 44.9z m-189.4-89.9h144.1c-3.7-36.5-34.6-65-72-65-37.5 0-68.4 28.5-72.1 65z" p-id="4790"></path><path d="M698.5 958.6H326c-89.5 0-162.3-72.8-162.3-162.3V263.6c0-24.8 20.1-44.9 44.9-44.9h607.2c24.8 0 44.9 20.1 44.9 44.9v532.6c0.1 89.6-72.7 162.4-162.2 162.4z m-444.9-650v487.7c0 39.9 32.5 72.4 72.4 72.4h372.5c39.9 0 72.4-32.5 72.4-72.4V308.6H253.6z" p-id="4791"></path><path d="M415.5 726.1c-24.8 0-44.9-20.1-44.9-44.9V498.1c0-24.8 20.1-44.9 44.9-44.9s44.9 20.1 44.9 44.9v183.1c0.1 24.8-20 44.9-44.9 44.9zM609 726.1c-24.8 0-44.9-20.1-44.9-44.9V498.1c0-24.8 20.1-44.9 44.9-44.9s44.9 20.1 44.9 44.9v183.1c0.1 24.8-20 44.9-44.9 44.9z" p-id="4792"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg
t="1736507350407"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="4808"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="256"
height="256"
>
<path
d="M761.5 928.3H228.3C156.5 928.3 98 869.8 98 798V277.6c0-71.8 58.5-130.3 130.3-130.3h248.8c23.2 0 42.1 18.8 42.1 42.1s-18.8 42.1-42.1 42.1H228.3c-25.5 0-46.2 20.7-46.2 46.2V798c0 25.5 20.7 46.2 46.2 46.2h533.2c25.5 0 46.2-20.7 46.2-46.2V501.2c0-23.2 18.8-42.1 42.1-42.1s42.1 18.8 42.1 42.1V798c-0.1 71.8-58.5 130.3-130.4 130.3z"
p-id="4809"
></path>
<path
d="M374.5 675.7c-11.1 0-21.8-4.4-29.7-12.3-9.1-9.1-13.6-22-12-34.8 12.3-101.9 59-198 131.6-270.5l245.2-245.2C726.2 96.2 748.4 87 772 87s45.8 9.2 62.5 25.9l60.7 60.6c34.5 34.5 34.5 90.6 0 125L650 543.8c-72.6 72.6-168.6 119.3-270.5 131.6-1.7 0.2-3.4 0.3-5 0.3zM772 171.3c-1.2 0-2.3 0.4-3 1.1L523.8 417.6c-45.1 45.1-78 101.3-95.6 162.3 61-17.6 117.2-50.5 162.3-95.6l245.2-245.2c1.7-1.7 1.7-4.4 0-6.1L775 172.4c-0.6-0.7-1.8-1.1-3-1.1z m93.5 97.6h0.2-0.2z"
p-id="4810"
></path>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg
t="1735123199043"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="7763"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="256"
height="256"
>
<path
d="M574.8 923.6H222c-67.2 0-121.8-54.7-121.8-121.8V223c0-67.2 54.7-121.8 121.8-121.8h352.8c67.2 0 121.8 54.7 121.8 121.8v50.6c0 22.9-18.5 41.4-41.4 41.4s-41.4-18.5-41.4-41.4V223c0-21.5-17.5-39-39-39H222c-21.5 0-39 17.5-39 39v578.8c0 21.5 17.5 39 39 39h352.8c21.5 0 39-17.5 39-39v-44.5c0-22.9 18.5-41.4 41.4-41.4s41.4 18.5 41.4 41.4v44.5c0 67.1-54.7 121.8-121.8 121.8z"
p-id="7764"
></path>
<path
d="M860.6 553.8H470.3c-22.9 0-41.4-18.5-41.4-41.4s18.5-41.4 41.4-41.4h390.4c22.9 0 41.4 18.5 41.4 41.4s-18.6 41.4-41.5 41.4z"
p-id="7765"
></path>
<path
d="M747.6 693.1c-10.6 0-21.2-4-29.3-12.1-16.2-16.2-16.2-42.4 0-58.6l110-110-110-110c-16.2-16.2-16.2-42.4 0-58.6 16.2-16.2 42.4-16.2 58.6 0l139.2 139.3c16.2 16.2 16.2 42.4 0 58.6L776.9 680.9c-8.1 8.1-18.7 12.2-29.3 12.2z"
p-id="7766"
></path>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1736678565688" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4477" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M125.28125027 160.43749973m35.15624946 0l703.12500054 0q35.15625027 0 35.15624946 35.15625027l0 0q0 35.15625027-35.15624946 35.15625027l-703.12500054 0q-35.15625027 0-35.15624946-35.15625027l0 0q0-35.15625027 35.15624946-35.15625027Z" p-id="4478"></path><path d="M441.68750027 371.37499973m35.15624946 0l386.71875054 0q35.15625027 0 35.15624946 35.15625027l0 0q0 35.15625027-35.15624946 35.15625027l-386.71875054 0q-35.15625027 0-35.15624946-35.15625027l0 0q0-35.15625027 35.15624946-35.15625027Z" p-id="4479"></path><path d="M441.68750027 582.31249973m35.15624946 0l386.71875054 0q35.15625027 0 35.15624946 35.15625027l0 0q0 35.15625027-35.15624946 35.15625027l-386.71875054 0q-35.15625027 0-35.15624946-35.15625027l0 0q0-35.15625027 35.15624946-35.15625027Z" p-id="4480"></path><path d="M125.28125027 793.24999973m35.15624946 0l703.12500054 0q35.15625027 0 35.15624946 35.15625027l0 0q0 35.15625027-35.15624946 35.15625027l-703.12500054 0q-35.15625027 0-35.15624946-35.15625027l0 0q0-35.15625027 35.15624946-35.15625027Z" p-id="4481"></path><path d="M282.95703152 378.16015625a35.15625027 35.15625027 0 0 1 44.89453098 53.92968777l-3.375 2.81249973L219.04296902 512l105.43359348 77.09765625a35.15625027 35.15625027 0 0 1 9.98437473 45.38671902l-2.35546821 3.72656196a35.15625027 35.15625027 0 0 1-45.42187554 10.01953125l-3.72656196-2.39062473-144.21093777-105.46875a35.15625027 35.15625027 0 0 1-3.375-53.96484348l3.375-2.81250054 144.21093777-105.46875z" p-id="4482"></path></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1736864587458" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4348" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M828.74 92A105 105 0 0 1 932 197v631.74A105 105 0 0 1 827 932H515.78a15 15 0 0 1-15-15v-60a15 15 0 0 1 15-15h311.22a15.06 15.06 0 0 0 15-15V197a15.06 15.06 0 0 0-15-15H196.7a15 15 0 0 0-14.7 15v300a15 15 0 0 1-15 15h-60A15 15 0 0 1 92 497V195.26A105 105 0 0 1 197 92zM302 486.8a15 15 0 0 1 15 15v310.98a15 15 0 0 1-24.6 11.52l-186.6-155.46a15 15 0 0 1 0-23.04l186.6-155.46a15 15 0 0 1 9.6-3.48z m300 125.52a15 15 0 0 1 15 15v60a15 15 0 0 1-15 15H317v-90z" p-id="4349"></path></svg>

After

Width:  |  Height:  |  Size: 814 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1736680594235" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4503" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M639.6 685.9c-11.5 0-22.9-4.4-31.7-13.1L351.2 416c-17.5-17.5-17.5-45.9 0-63.4s45.9-17.5 63.4 0l256.7 256.7c17.5 17.5 17.5 45.9 0 63.4-8.8 8.8-20.3 13.2-31.7 13.2z" p-id="4504"></path><path d="M382.8 685.9c-11.5 0-22.9-4.4-31.7-13.1-17.5-17.5-17.5-45.9 0-63.4l256.7-256.7c17.5-17.5 45.9-17.5 63.4 0s17.5 45.9 0 63.4L414.5 672.7c-8.7 8.8-20.2 13.2-31.7 13.2z" p-id="4505"></path><path d="M511.2 960.7c-114.8 0-229.5-43.7-316.9-131.1-174.8-174.8-174.8-459.1 0-633.9C369 21 653.4 21 828.1 195.8c174.8 174.8 174.8 459.1 0 633.9-87.3 87.3-202.1 131-316.9 131z m0-806.4c-91.8 0-183.6 35-253.5 104.8-139.8 139.8-139.8 367.3 0 507.1s367.3 139.8 507.1 0 139.8-367.3 0-507.1C694.9 189.3 603 154.3 511.2 154.3z m285.3 643.6h0.2-0.2z" p-id="4506"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1736678563154" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4316" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M860.92578152 160.43749973H160.43749973a35.15625027 35.15625027 0 1 0 0 70.31250054h700.48828179a35.15625027 35.15625027 0 0 0 0-70.31250054z m-315.3515625 210.9375H160.43749973a35.15625027 35.15625027 0 1 0 0 70.31250054h385.13671929a35.15625027 35.15625027 0 1 0 0-70.31250054z m0 210.9375H160.43749973a35.15625027 35.15625027 0 0 0 0 70.31250054h385.13671929a35.15625027 35.15625027 0 1 0 0-70.31250054z m315.3515625 210.9375H160.43749973a35.15625027 35.15625027 0 0 0 0 70.31250054h700.48828179a35.15625027 35.15625027 0 0 0 0-70.31250054zM740.62109348 378.16015625a34.94531277 34.94531277 0 0 0-48.9375 7.62890652c-10.546875 14.44921875-8.4375 34.31250027 4.21875 46.30078125l3.375 2.81249973L804.32421848 512l-105.046875 77.09765625c-14.41406223 10.546875-18.42187473 30.12890598-9.98437473 45.38671902l2.39062473 3.72656196c10.546875 14.48437527 30.02343723 18.49218777 45.24609375 10.01953125l3.69140625-2.39062473 143.75390625-105.46875c17.9296875-13.14843723 19.05468723-39.16406277 3.33984429-53.96484348l-3.33984429-2.81250054-143.75390625-105.46875z" p-id="4317"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg
t="1736591017958"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="4282"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="256"
height="256"
>
<path
d="M512 88.0000003A435.6 435.6 0 0 0 72.00000031 518.8000003 431.59999969 431.59999969 0 0 0 372.8 928.0000003c22.00000031 4.00000031 30-9.19999969 30-20.80000031v-73.2c-122.4 25.99999969-148.39999969-57.6-148.39999969-57.6a114.40000031 114.40000031 0 0 0-48.80000062-63.19999969c-40.00000031-26.4 3.19999969-25.99999969 3.20000062-26.00000062a92.4 92.4 0 0 1 67.2 44.4 94.8 94.8 0 0 0 127.99999969 35.60000062 93.19999969 93.19999969 0 0 1 28.00000031-57.6c-97.60000031-10.8-199.99999969-47.59999969-200.00000062-212.80000031a166.00000031 166.00000031 0 0 1 44.4-116.4 151.2 151.2 0 0 1 4.40000062-113.59999969s37.2-11.59999969 120 43.99999969a427.2 427.2 0 0 1 219.99999938 0c84-55.60000031 120-43.99999969 120-43.99999969a151.2 151.2 0 0 1 4.40000062 113.59999969A166.00000031 166.00000031 0 0 1 792.00000031 496.0000003c0 165.6-103.2 202.00000031-200.00000062 212.79999938a100.00000031 100.00000031 0 0 1 30 80.00000062v117.99999938c0 13.99999969 7.99999969 25.2 30 20.80000031A432 432 0 0 0 951.99999969 518.8000003 435.6 435.6 0 0 0 512 88.0000003"
p-id="4283"
></path>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg
t="1736671760764"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="7110"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="128"
height="128"
>
<path
d="M725 920.1H614c-24.6 0-44.6-20-44.6-44.6v-214H456.6v214c0 24.6-20 44.6-44.6 44.6H298.5c-68.8 0-124.9-56-124.9-124.8v-259l-61.7 1c-19.4 0.9-35.7-11.2-42.4-28.8-6.7-17.6-1.6-37.4 12.6-49.7L483.9 113c16.7-14.4 41.5-14.4 58.2 0l401.7 345.8c14.2 12.2 19.3 32.1 12.6 49.7-6.7 17.6-24.3 28.9-42.4 28.8l-64.1-1v259c0 68.8-56 124.8-124.9 124.8z m-66.3-89.2H725c19.6 0 35.6-16 35.6-35.6V490.9c0-12 4.8-23.4 13.3-31.8 5.6-5.6 12.6-9.5 20.2-11.4L513 205.7 231.3 448.2c6.8 2.1 13.1 5.8 18.3 10.9 8.5 8.4 13.3 19.9 13.3 31.8v304.3c0 19.6 16 35.6 35.6 35.6h68.8V658.4c0-47.5 38.6-86.1 86.1-86.1h119.2c47.5 0 86.1 38.6 86.1 86.1v172.5z"
p-id="7111"
></path>
</svg>

After

Width:  |  Height:  |  Size: 1000 B

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1614441341007" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11418" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M848.896 132.192c-10.048-5.664-22.4-5.408-32.352 0.544-0.448 0.288-45.664 27.328-98.688 27.328-53.184 0-99.776-27.232-100.16-27.424a29.488 29.488 0 0 0-3.456-1.792c-3.2-1.408-79.008-34.752-149.696-34.752-70.08 0-151.936 32.96-155.36 34.368-12.032 4.896-19.936 16.64-19.936 29.632v416a32.041 32.041 0 0 0 14.24 26.624c8.928 5.984 20.192 7.04 30.08 2.912 19.68-8.224 80.992-29.536 127.68-29.536 51.52 0 115.776 24.768 126.208 28.928 11.712 6.304 68.544 35.072 133.088 35.072 72.032 0 127.776-35.616 130.08-37.152 9.088-5.92 14.592-16 14.592-26.848v-416c0.032-11.584-6.24-22.208-16.32-27.904z m-271.68 763.744H224.768V128.064c0-17.664-14.336-32-32-32s-32 14.336-32 32v768c0 0.064 0.032 0.096 0.032 0.16-16.96 0.8-30.56 14.528-30.56 31.712 0 17.696 14.336 32 32 32h414.976c17.696 0 32-14.304 32-32s-14.304-32-32-32z" p-id="11419"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1676622023768" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="1470" xmlns:xlink="http://www.w3.org/1999/xlink"
width="64" height="64">
<path
d="M373.9996544 320.53819093l298.27051947 0c20.11542827 0 39.48256427-8.5139072 52.3939168-23.95154773 30.781264-36.58213013 59.972352-81.49120213 66.52151146-121.72205867 13.19190933-80.5555168-21.33188373-121.44150293-121.44150293-121.44150293s-66.2407424 65.67941653-105.16197973 71.1059232c-29.003696 4.02308587-27.9746336-34.24302293-73.53840747-71.01233387-32.74622507-26.2904416-69.42173227 47.99647147-112.6468256 48.464208-50.42895573 0.65491627-121.34791253-13.28549867-121.34791253 72.8834912 0 34.80456213 30.5007072 81.86534827 63.80825813 120.87996374C334.2365344 311.36915413 353.50986667 320.53819093 373.9996544 320.53819093zM746.08935147 397.25758827c-15.90516267-18.05709013-39.0146144-28.16181333-63.05975254-28.16181334L364.7372416 369.09577493c-23.0158624 0-45.18962773 9.26241387-61.09500373 25.91629547C203.62599467 499.79990507 111.3755808 649.40300693 111.3755808 758.4944832c0 96.18012373 96.6478592 217.6216256 215.84384427 217.6216256l369.5636224 0c119.19598507 0 215.84384427-120.4122272 215.84384426-217.6216256C912.62710507 647.4382592 838.43356907 502.04542187 746.08935147 397.25758827zM648.8801664 529.645696l-63.99522453 116.10858453c-5.61368747 10.1980992 1.777568 22.73509227 13.37908906 22.73509227l18.15068054 0c8.4205312 0 15.3438368 6.8299296 15.3438368 15.3438368l0 0c0 8.4205312-6.8299296 15.3438368-15.3438368 15.3438368l-45.6575776 0c-8.88826667 0-15.99875307 7.11069867-16.0923424 15.99875307l0 7.5784352c0 8.4205312 6.8299296 15.3438368 15.3438368 15.3438368l46.40586986 0c8.4205312 0 15.3438368 6.8299296 15.3438368 15.3438368s-6.8299296 15.3438368-15.3438368 15.3438368l-46.40586986 0c-8.4205312 0-15.3438368 6.8299296-15.3438368 15.3438368l0 39.0146144c0 8.4205312-6.8299296 15.3438368-15.3438368 15.3438368l-27.78745387 0c-8.4205312 0-15.3438368-6.8299296-15.3438368-15.3438368l0-39.0146144c0-8.4205312-6.8299296-15.3438368-15.3438368-15.3438368l-45.9381344 0c-8.4205312 0-15.3438368-6.8299296-15.3438368-15.3438368s6.8299296-15.3438368 15.3438368-15.3438368l45.844544 0c8.51412053 0 15.34405013-6.8299296 15.4374272-15.3438368l0.0935904-7.4848448c0.0935904-8.88826667-7.11069867-16.1859328-16.0923424-16.1859328l-45.28321813 0c-8.4205312 0-15.3438368-6.8299296-15.3438368-15.3438368l0 0c0-8.4205312 6.8299296-15.3438368 15.3438368-15.3438368l17.682944 0c11.6951104 0 18.99277653-12.53699307 13.37908906-22.73509227l-63.99543786-116.10837227c-5.61368747-10.1980992 1.777568-22.7353056 13.37908906-22.7353056l30.5007072 0c5.80065387 0 11.13378453 3.27458027 13.65985814 8.4205312l52.11314773 103.94552534c5.61368747 11.2271616 21.70602987 11.2271616 27.41330667 0l52.11314773-103.94552534c2.619664-5.1457376 7.858992-8.4205312 13.65985813-8.4205312l30.5007072 0C647.10238507 507.0039808 654.4938528 519.4475968 648.8801664 529.645696z"
p-id="1471"></path>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1614441249036" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4552" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M364.8 590.62857143H182.85714286c-60.34285714 0-109.71428571-49.37142857-109.71428572-109.71428572V182.85714286c0-60.34285714 49.37142857-109.71428571 109.71428572-109.71428572h181.94285714c60.34285714 0 109.71428571 49.37142857 109.71428571 109.71428572v298.05714285c0 60.34285714-49.37142857 109.71428571-109.71428571 109.71428572zM841.14285714 358.4H659.2c-60.34285714 0-109.71428571-49.37142857-109.71428571-109.71428571V182.85714286c0-60.34285714 49.37142857-109.71428571 109.71428571-109.71428572H841.14285714c60.34285714 0 109.71428571 49.37142857 109.71428572 109.71428572v65.82857143c0 60.34285714-49.37142857 109.71428571-109.71428572 109.71428571zM364.8 950.85714286H182.85714286c-60.34285714 0-109.71428571-49.37142857-109.71428572-109.71428572v-65.82857143c0-60.34285714 49.37142857-109.71428571 109.71428572-109.71428571h181.94285714c60.34285714 0 109.71428571 49.37142857 109.71428571 109.71428571v65.82857143c0 60.34285714-49.37142857 109.71428571-109.71428571 109.71428572z m476.34285714 0H659.2c-60.34285714 0-109.71428571-49.37142857-109.71428571-109.71428572V543.08571429c0-60.34285714 49.37142857-109.71428571 109.71428571-109.71428572H841.14285714c60.34285714 0 109.71428571 49.37142857 109.71428572 109.71428572V841.14285714c0 60.34285714-49.37142857 109.71428571-109.71428572 109.71428572z" p-id="4553"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1614441250987" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4692" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M825.42250667 584.80298667H628.39239111l-29.53557333-99.30865778c74.05568-33.86026667 125.62545778-108.54172445 125.62545777-195.44291556 0-118.69297778-95.98862222-214.90005333-214.37553777-214.90005333-118.40170667 0-214.37553778 96.22186667-214.37553778 214.90005333 0 86.91484445 51.56977778 161.59744 125.62545778 195.44291556l-29.53557333 99.30865778h-197.02897778a70.59000889 70.59000889 0 0 0-70.59000889 70.59000888v131.0572089c0 17.31697778 14.03904 31.34122667 31.34008889 31.34122666h709.05628444a31.36967111 31.36967111 0 0 0 31.36967111-31.37080889V655.36341333a70.54563555 70.54563555 0 0 0-70.54563555-70.56042666z m48.71509333 291.24266666h-728.07537778a21.84533333 21.84533333 0 0 0-21.84533333 21.84533334v29.12711111a21.84533333 21.84533333 0 0 0 21.84533333 21.84533333h728.09016889a21.84533333 21.84533333 0 0 0 21.84533334-21.84533333v-29.12711111a21.84533333 21.84533333 0 0 0-21.86012445-21.84533334z" p-id="4693"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1614441301181" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8337" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M864.35351563 176.78515625c-22.93945313 3.95507813-46.40625 6.06445313-70.48828126 6.06445313-101.42578125 0-194.32617188-36.82617188-265.95703124-97.73437501-9.31640625-7.91015625-23.02734375-7.91015625-32.34375001 0-71.63085938 60.99609375-164.53125 97.734375-265.95703124 97.734375-23.73046875 0-47.02148438-2.02148438-69.60937501-5.88867187-15.1171875-2.54882813-28.91601563 9.140625-29.00390625 24.52148437-0.17578125 95.88867188-0.3515625 209.26757813-0.3515625 213.13476563 0 413.96484375 346.72851563 520.92773438 378.80859376 529.98046875 1.58203125 0.43945313 3.1640625 0.43945313 4.74609374 0C546.18945313 935.6328125 890.80859375 829.28515625 893.00585938 418.57226562l0.35156249-217.08984374c0-15.46875-13.88671875-27.33398438-29.00390625-24.69726563zM541.8828125 562.00976563v135.79101562c0 16.5234375-13.359375 29.8828125-29.8828125 29.8828125s-29.8828125-13.359375-29.8828125-29.8828125V562.00976563c-59.94140625-13.62304688-104.67773438-67.1484375-104.67773438-131.22070313 0-74.26757813 60.20507813-134.56054688 134.56054688-134.56054688S646.56054688 356.43359375 646.56054688 430.7890625c0 64.07226563-44.73632813 117.68554688-104.67773438 131.22070313z" p-id="8338"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1614441322440" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10017" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M509.27 62.598c-248.668 0-450.254 199.753-450.254 446.161s201.586 446.162 450.255 446.162 450.255-199.754 450.255-446.162S757.939 62.598 509.27 62.598z m0 148.72c55.666 0 107.78 15.033 152.468 41.2L250.676 659.842c-26.404-44.281-41.575-95.922-41.575-151.084 0-164.27 134.386-297.441 300.17-297.441z m0 594.883c-55.666 0-107.78-15.033-152.469-41.198l411.062-407.326C794.27 401.96 809.44 453.6 809.44 508.76c0 164.277-134.394 297.442-300.17 297.442z" p-id="10018"></path></svg>

After

Width:  |  Height:  |  Size: 853 B

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1736858680178" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3103" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M760.59989333 606.7602011a74.07995221 74.07995221 0 0 1 84.95407446 14.66064669l81.99281778 82.0413622c19.6608 19.61225443 27.1367589 49.4189989 16.55390776 74.61395001C703.80202667 1350.66661888-326.66661888 320.2465189 245.97238557 79.89930667c25.24349667-10.58284999 55.00169443-3.10689223 74.66249443 16.55390776l82.04136334 82.04136335c22.52496555 22.57351111 28.4474789 56.79786667 14.61209999 84.90552889l-78.44901888 159.47093333 262.33818112 262.33818112 159.47093333-78.44902002z" p-id="3104"></path></svg>

After

Width:  |  Height:  |  Size: 849 B

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1736858592311" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2788" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M598.32603476 112.7773127a140.9021248 140.9021248 0 0 1 133.6691498 96.33008672L746.8368743 253.67943749h93.9347506A117.4184377 117.4184377 0 0 1 958.1900615 371.0978752v352.255312a187.8695001 187.8695001 0 0 1-187.86949899 187.8695001h-516.64112502a187.8695001 187.8695001 0 0 1-187.86949899-187.8695001V371.0978752A117.4184377 117.4184377 0 0 1 183.2283751 253.67943749h93.9347506l14.84168974-44.57203807A140.9021248 140.9021248 0 0 1 425.67396524 112.7773127h172.65206952zM512 723.3531872a164.3858119 164.3858119 0 1 0 0-328.7716249 164.3858119 164.3858119 0 0 0 0 328.7716249z" p-id="2789"></path></svg>

After

Width:  |  Height:  |  Size: 940 B

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1614441255776" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4972" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M891.733 170.667H132.267c-38.4 0-68.267 29.866-68.267 68.266v102.4h896v-102.4c0-38.4-29.867-68.266-68.267-68.266z m0 704c38.4 0 68.267-29.867 68.267-68.267V426.667H64V806.4c0 38.4 29.867 68.267 68.267 68.267h759.466zM192 554.667h253.867c19.2 0 32 12.8 32 32s-12.8 32-32 32H187.733c-17.066-2.134-27.733-14.934-27.733-32 0-19.2 12.8-32 32-32z m0 128h128c19.2 0 32 12.8 32 32s-12.8 32-32 32H187.733c-17.066-2.134-27.733-14.934-27.733-32 0-19.2 12.8-32 32-32z" p-id="4973"></path></svg>

After

Width:  |  Height:  |  Size: 859 B

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1614441257585" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5112" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M259.2 896a64 64 0 1 0 128 0 64 64 0 1 0-128 0zM768 896a64 64 0 1 0 128 0 64 64 0 1 0-128 0zM940.8 284.8c-16-19.2-38.4-28.8-64-28.8H249.6l-22.4-112c-6.4-44.8-44.8-80-92.8-80H96c-19.2 0-32 16-32 32s12.8 32 32 32h38.4c12.8 0 25.6 9.6 28.8 25.6L192 310.4l51.2 371.2c6.4 48 44.8 86.4 89.6 86.4h486.4c44.8 0 83.2-32 89.6-73.6L960 355.2c3.2-25.6-3.2-51.2-19.2-70.4z" p-id="5113"></path></svg>

After

Width:  |  Height:  |  Size: 763 B

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1614441297938" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8056" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M512 65.984C266.08 65.984 65.984 266.08 65.984 512c0 245.952 200.064 446.016 446.016 446.016 245.952 0 446.016-200.064 446.016-446.016 0-245.92-200.064-446.016-446.016-446.016zM512 672c-88.224 0-160-71.776-160-160s71.776-160 160-160 160 71.776 160 160-71.776 160-160 160z" p-id="8057"></path></svg>

After

Width:  |  Height:  |  Size: 675 B

@@ -0,0 +1,17 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg
t="1695871550331"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="1803"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="64"
height="64"
>
<path
d="M926.96688799 198.385496l-402.29783999-118.688976c-3.778488-1.11132001-7.77924-1.778112-11.77999201-1.778112-4.000752 0-7.77924 0.444528-11.55772799 1.555848l-381.18276 111.132c-5.334336 0.222264-10.224144 1.778112-14.669424 4.22301599l-7.112448 2.000376c-17.78112 5.11207201-30.00564 21.337344-30.00564 40.00752L67.249736 752.04512c0 17.336592 10.668672 32.89507199 26.893944 39.11846401L496.219256 943.19216001c4.667544 1.778112 9.779616 2.667168 14.66942399 2.66716799 4.88980799 0 10.00188-0.88905599 14.66942401-2.66716799l403.186896-150.69499201c16.225272-6.001128 27.11620801-21.559608 27.116208-38.8962l0.889056-514.985688c0-18.670176-12.002256-34.89544799-29.78337601-40.229784zM530.670176 149.04288801l234.48852001 69.12410399c7.77924 2.22264 7.77924 13.33584 0.22226399 15.780744l-234.043992 74.236176c-12.669048 4.000752-26.004888 4.000752-38.673936 0l-233.154936-75.12523199c-7.77924-2.444904-7.556976-13.558104 0.222264-15.78074401l235.37757601-68.45731201c11.557728-3.11169601 24.004512-3.11169601 35.56223999 0.22226402zM157.48892 268.398656l308.94696 99.352008c7.77924 2.444904 13.11357601 9.779616 13.113576 18.003384v454.52988c0 13.11357601-13.11357601 22.2264-25.56036 17.558856L144.375344 740.709656c-7.334712-2.88943199-12.22452-9.779616-12.22452-17.78112001l0.88905601-436.52649599c-0.222264-12.89131199 12.446784-21.781872 24.44903999-18.003384z"
p-id="1804"
></path>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1614441329307" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10578" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M517.46416016 442.25V126.28583984A64.28583984 64.28583984 0 0 1 592.35839844 62.96416016a450 450 0 0 1 368.67832031 368.67832031 64.28583984 64.28583984 0 0 1-63.32167969 74.89335937H581.75a64.28583984 64.28583984 0 0 1-64.28583984-64.28583984z m-21.53583985 117.96416016h299.08916016a64.28583984 64.28583984 0 0 1 64.28583984 75.21416015A401.78583984 401.78583984 0 1 1 388.57167969 165.5a64.28583984 64.28583984 0 0 1 75.21416015 64.28583984v298.28583985a32.14248047 32.14248047 0 0 0 32.14248047 32.14248047z" p-id="10579"></path></svg>

After

Width:  |  Height:  |  Size: 917 B

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1614441339081" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11278" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M960.14 330.74c0-57.69-46.92-104.61-104.64-104.61S750.86 273.05 750.86 330.74c0 49.41 34.5 90.69 80.64 101.61-41.46 82.65-96.24 159.48-125.58 159.48-28.56 0-86.13-83.31-144.36-242.94 41.88-18.42 70.41-59.97 70.41-107.94 0-65.28-53.13-118.38-118.41-118.38-65.31 0-118.44 53.13-118.44 118.38 0 48.09 28.65 89.7 70.65 108.03C403.79 520.7 349.85 583.97 327.71 583.97c-41.13 0-98.25-79.98-135.42-151.65 46.17-10.89 80.73-52.2 80.73-101.64 0-57.69-46.95-104.61-104.64-104.61S63.74 273.05 63.74 330.74c0 39.54 22.32 73.62 54.78 91.41L139.25 839.81a29.955 29.955 0 0 0 13.86 23.82c4.35 2.79 109.41 67.65 360.45 67.65s356.1-65.1 360.45-67.86a30.1209375 30.1209375 0 0 0 13.86-24l18.48-417.81c31.92-17.88 53.79-51.69 53.79-90.87z" p-id="11279"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1676620716973" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="2460" xmlns:xlink="http://www.w3.org/1999/xlink"
width="64" height="64">
<path
d="M512 70.99999966c243.55327466 0 441.00000034 197.44672466 441.00000034 441.00000034S755.55327466 953.00000034 512 953.00000034 70.99999966 755.55327466 70.99999966 512 268.44672534 70.99999966 512 70.99999966z m221.80095 329.38290068a33.075 33.075 0 0 0-46.76805 0.06615L545.835725 542.04312534l-84.5397-92.10285068a33.075 33.075 0 0 0-48.17924966-0.59535l-123.36975 127.89000068a33.075 33.075 0 1 0 47.60594932 45.93014966l98.97142534-102.59865 84.15382534 91.6839a33.075 33.075 0 0 0 47.793375 0.99225l165.59549966-166.09162466a33.075 33.075 0 0 0-0.06615-46.76805z"
p-id="2461"></path>
</svg>

After

Width:  |  Height:  |  Size: 957 B

+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1676620724969" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="3062" xmlns:xlink="http://www.w3.org/1999/xlink"
width="64" height="64">
<path
d="M72.10250036 211.01750035a439.89749964 138.91499999 0 1 0 879.79499928 0 439.89749964 138.91499999 0 1 0-879.79499928 0Z"
p-id="3063"></path>
<path
d="M512 720.3725c-193.32337464 0-357.47460036-39.35925036-416.51347536-93.99914964a13.79889037 13.79889037 0 0 0-23.38402428 9.95557464V812.98249965c0 76.63477464 197.02777536 138.91499999 439.89749964 138.91499999s439.89749964-62.28022536 439.89749964-138.91499999v-176.65357465c0-12.27082536-14.35455036-18.29047464-23.38402428-9.95557464C869.47460036 681.01324964 705.32337464 720.3725 512 720.3725z"
p-id="3064"></path>
<path
d="M512 419.39000036c-193.32337464 0-357.47460036-39.35925036-416.51347536-93.99915071a13.79889037 13.79889037 0 0 0-23.38402428 9.95557571V512c0 76.63477464 197.02777536 138.91499999 439.89749964 138.91499999s439.89749964-62.28022536 439.89749964-138.91499999v-176.65357464c0-12.27082536-14.35455036-18.29047464-23.38402428-9.95557571C869.47460036 380.03075 705.32337464 419.39000036 512 419.39000036z"
p-id="3065"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1736858393545" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5221" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M842.10725888 172.18370333H657.63555555v-38.83614778c0-33.98163001-24.27259221-58.25422222-58.25422222-58.25422222h-174.76266666C390.63703666 75.09333333 366.36444445 99.36592555 366.36444445 133.34755555V172.18370333H181.89274112C147.91111111 172.18370333 123.6385189 196.45629667 123.6385189 230.43792555v29.12711111C123.6385189 293.54666667 147.91111111 317.81925888 181.89274112 317.81925888h660.21451776c33.98163001 0 58.25422222-24.27259221 58.25422222-58.25422222v-29.12711111c0-33.98163001-24.27259221-58.25422222-58.25422222-58.25422222zM817.83466667 385.7825189H206.16533333c-19.41807445 0-29.12711111 14.56355555-29.12711111 33.98162887l33.98163001 373.79792668C220.72888889 880.94340779 293.54666667 948.90666667 380.928 948.90666667h271.85303666c87.38133333 0 160.19911111-67.96325888 169.90814891-155.34459222l33.98162886-373.79792668c0-19.41807445-9.70903666-33.98163001-29.1271111-33.98162887z m-393.216 383.5069622c-33.98163001 0-58.25422222-19.41807445-63.10874112-53.39970332l-14.56355556-140.78103666c0-33.98163001 19.41807445-58.25422222 53.39970446-63.10874112s58.25422222 19.41807445 63.10873998 53.39970333l14.56355556 140.78103779c0 33.98163001-19.41807445 58.25422222-53.39970332 63.10873998z m233.01688888-53.39970332c0 33.98163001-29.12711111 53.39970333-63.10874112 53.39970332-33.98163001 0-53.39970333-29.12711111-53.39970332-63.10873998l14.56355556-140.78103779c0-33.98163001 29.12711111-53.39970333 63.10874112-53.39970333 33.98163001 0 53.39970333 29.12711111 53.39970332 63.10874112l-14.56355556 140.78103666z" p-id="5222"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1676621525727" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="1886" xmlns:xlink="http://www.w3.org/1999/xlink"
width="64" height="64">
<path
d="M471.99999969 471.99999969c88.00000031 0 160.00000031 72 160.00000031 160.00000031v199.99999969c0 43.99999969-36 79.99999969-79.99999969 80.00000062H152c-43.99999969 0-79.99999969-36-79.99999969-80.00000062v-199.99999969c0-88.00000031 72-160.00000031 159.99999938-160.00000031h240zM692 552.00000031H792.00000031c88.00000031 0 160.00000031 72 159.99999938 159.99999938v120c0 43.99999969-36 79.99999969-79.99999969 80.00000062h-160.00000031v-280.00000031c0-28.00000031-7.99999969-55.99999969-19.99999969-79.99999969zM720.00000031 272c67.99999969 0 120 52.00000031 120 120s-52.00000031 120-120 120-120-52.00000031-120-120 55.99999969-120 120-120zM351.99999969 111.99999969c88.00000031 0 160.00000031 72 160.00000031 160.00000031s-72 160.00000031-160.00000031 160.00000031-160.00000031-72-159.99999938-160.00000031 72-160.00000031 159.99999938-160.00000031z"
p-id="1887"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1676622469986" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="1521" xmlns:xlink="http://www.w3.org/1999/xlink"
width="64" height="64">
<path
d="M933.92 314.72c27.36-27.36 27.36-71.04 0-98.4l-132.96-132.96c-26.88-27.36-71.04-27.36-98.4 0l-135.36 135.36 231.36 231.84 135.36-135.84z"
p-id="1522"></path>
<path
d="M160.64 625.28c-13.92 13.92-24.48 31.68-30.72 50.4L72.8 884.96c-7.68 24.96 6.24 51.36 31.2 59.52 9.12 2.88 19.2 2.88 28.32 0l209.28-57.12c19.2-6.24 36.48-16.32 50.88-30.72L444.8 804.32l-231.36-231.84-52.8 52.8zM325.28 98.72c-29.28-29.76-77.76-29.76-107.52 0l-0.48 0.48-125.28 125.28c-29.76 29.76-29.76 77.76 0 107.52L151.52 392l110.4-110.88c14.88-14.88 38.88-14.88 53.76 0 14.88 14.88 14.88 38.88 0 53.76L205.28 444.8l53.76 53.76L324.8 432.8c14.4-14.88 38.4-15.36 53.28-0.48l0.48 0.48c14.88 14.88 14.88 38.88 0 53.76l-65.28 65.76 53.76 53.76 109.92-110.4c14.88-14.88 38.88-14.88 53.76 0s14.88 38.88 0 53.76l-111.36 109.44L473.6 713.6l0.48-0.48 64.8-65.28c14.88-14.88 38.88-14.88 53.76 0s14.88 38.88 0 53.76L527.36 766.4l-0.48 0.48 53.76 53.76 109.92-109.92c14.88-14.88 38.88-14.88 53.76 0 14.88 14.88 14.88 38.88 0 53.76l-110.4 109.44 64.32 64.32c29.76 29.76 77.76 29.76 107.52 0.48l0.48-0.48 125.76-125.76c29.76-29.76 29.76-77.76 0-107.52L325.28 98.72z"
p-id="1523"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1676620747474" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="3509" xmlns:xlink="http://www.w3.org/1999/xlink"
width="64" height="64">
<path
d="M858.61262222 956.47971556h-693.22524444c-53.01134222 0-97.86709332-44.85575112-97.86709334-101.94488889v-232.43434667h888.95943112v232.43434667c0 57.08913778-44.85575112 101.94488889-97.86709334 101.94488889z m-236.51214222-220.20096H177.62076444v110.10048h444.47971556v-110.10048z m224.27875556 0h-110.10048v110.10048h110.10048v-110.10048zM67.52028444 165.38737778C67.52028444 108.29823999 112.37603556 63.44248888 165.38737778 63.44248888h693.22524444c53.01134222 0 97.86709332 44.85575112 97.86709334 101.9448889v399.62396444H67.52028444V165.38737778zM246.94328888 471.22204445l81.55591112-138.64504889 187.57859556 126.41166222 101.94488888-159.03402666 199.81198222 114.17827556 44.85575112-81.55591112-277.29009777-175.3452089-93.78929779 163.11182223-203.88977778-110.10048001-122.33386666 216.12316446 81.5559111 44.85575111z"
p-id="3510"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1736864127187" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2818" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M804.72746667 282.80604445h-35.49866667v361.54026666c0 12.56106667-4.36906667 25.12213333-13.65333333 33.86026667-16.93013333 16.384-42.5984 16.384-59.52853334 2.73066667l-84.10453333-67.1744h-0.54613333l-84.10453334 67.1744c-8.192 6.5536-18.0224 9.8304-28.39893333 9.8304-6.5536 0-13.1072-1.6384-19.6608-4.36906667-15.83786667-7.64586667-25.66826667-23.48373333-25.66826667-40.41386667V282.80604445H266.78613333c-35.49866667 0-66.62826667-26.2144-69.90506666-61.71306667-3.82293333-40.41386667 27.8528-74.27413333 67.72053333-74.27413333H823.296c24.02986667 0 45.32906667-18.0224 46.42133333-42.05226667 1.6384-26.2144-19.11466667-47.5136-44.78293333-47.5136H265.14773333c-86.8352 0-157.83253333 70.99733333-157.83253333 157.83253333v604.02346667c0 74.27413333 60.07466667 134.3488 134.3488 134.3488h560.87893333c75.3664 0 136.53333333-61.16693333 136.53333334-136.53333333V417.15484445c-0.54613333-74.27413333-60.6208-134.3488-134.3488-134.3488z" p-id="2819"></path><path d="M583.54346667 521.46631111c8.192-6.5536 18.0224-9.8304 28.39893333-9.8304 9.8304 0 19.6608 3.2768 28.39893333 9.8304l38.77546667 31.1296h0.54613333V370.18737778H544.768v181.8624s0 0.54613333 0.54613333 0l38.22933334-30.58346667z" p-id="2820"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1614441331528" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10718" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M928.768 87.232c-0.032-7.456-1.216-13.696-2.848-19.328-31.328-8.288-72.128 1.248-106.624 17.984-7.072 3.424-23.84 13.632-48.672 28.736-10.272 6.272-19.328 11.776-23.328 14.048-0.384 0.224-0.608 0.64-0.96 0.864C678.56 88.256 599.296 64 514.272 64c-247.04 0-448 200.96-448 448 0 82.816 22.976 160.192 62.304 226.784C82.112 810.4 59.072 862.112 65.088 897.152c1.248 7.168 13.6 8.32 20.384 8.32 138.432 0 454.848-303.776 494.208-342.496C753.504 392 928.864 182.976 928.768 87.232zM306.272 610.688c-44.096 0-80-35.872-80-80 0-44.096 35.904-80 80-80s80 35.904 80 80c0 44.128-35.904 80-80 80z m140.8-224c-52.928 0-96-43.072-96-96s43.072-96 96-96 96 43.072 96 96-43.072 96-96 96zM895.904 278.56C828.8 378.208 723.296 497.856 618.048 601.344c-28.64 28.192-202.272 196.448-348.096 286.112 0.128-0.096 0.224-0.16 0.352-0.224C340.576 933.088 424.288 960 514.272 960c247.04 0 448-200.96 448-448-0.032-85.6-24.544-165.344-66.368-233.44zM640.032 800c-35.296 0-64-28.704-64-64s28.704-64 64-64 64 28.704 64 64-28.704 64-64 64z" p-id="10719"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1736862443982" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2941" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M417.07812475 69.03125a63.28124988 63.28124988 0 0 0-63.28124987 63.28124988v31.64062537a63.28124988 63.28124988 0 0 0 63.28124987 63.28124987h189.8437505a63.28124988 63.28124988 0 0 0 63.28124987-63.28124988V132.31249988a63.28124988 63.28124988 0 0 0-63.28124988-63.28124988h-189.84375049zM243.05468738 132.31249988h31.64062537v31.64062537A142.38281287 142.38281287 0 0 0 417.07812475 306.33593725h189.8437505a142.38281287 142.38281287 0 0 0 142.382812-142.382812V132.31249988h31.64062537a94.92187525 94.92187525 0 0 1 94.92187525 94.92187524v632.81249963a94.92187525 94.92187525 0 0 1-94.92187525 94.92187525h-537.89062524a94.92187525 94.92187525 0 0 1-94.92187525-94.92187525v-632.81249963A94.92187525 94.92187525 0 0 1 243.05468738 132.31249988z" p-id="2942"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

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