chore: init frontend with knowledge module
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<router-view />
|
||||
<cool />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Cool from '/@/cool/index.vue';
|
||||
</script>
|
||||
@@ -0,0 +1,9 @@
|
||||
import { host, value } from './proxy';
|
||||
|
||||
export default {
|
||||
// 根地址
|
||||
host,
|
||||
|
||||
// 请求地址
|
||||
baseUrl: `/${value}`
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
@@ -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()]);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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 };
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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} />
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { useStore } from './store';
|
||||
|
||||
export function useBase() {
|
||||
return {
|
||||
...useStore()
|
||||
};
|
||||
}
|
||||
|
||||
export * from './utils';
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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-文件 ",
|
||||
"键": "键",
|
||||
"角色标签": "角色标签"
|
||||
}
|
||||
@@ -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}” 部門的所有用戶,是否確認?",
|
||||
"直接删除": "直接刪除",
|
||||
"保留用户": "保留用戶",
|
||||
"部门架构已发生改变,是否保存?": "部門架構已發生改變,是否保存?",
|
||||
"更新排序成功": "更新排序成功",
|
||||
"新增": "新增",
|
||||
"编辑": "編輯"
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |