初始化
58
frontend/src/App.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<!--element-plus语言国际化,全局修改为中文-->
|
||||
<!-- 测试升级功能 - 2025-10-24 -->
|
||||
<el-config-provider :locale="locale" :size="assemblySize" :button="buttonConfig">
|
||||
<router-view />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getBrowserLang } from '@/utils'
|
||||
import { useTheme } from '@/hooks/useTheme'
|
||||
import { ElConfigProvider } from 'element-plus'
|
||||
import { type LanguageType } from './stores/interface'
|
||||
import { useGlobalStore } from '@/stores/modules/global'
|
||||
import en from 'element-plus/es/locale/lang/en'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
|
||||
defineOptions({
|
||||
name: 'App'
|
||||
})
|
||||
|
||||
const globalStore = useGlobalStore()
|
||||
|
||||
// init theme
|
||||
const { initTheme } = useTheme()
|
||||
initTheme()
|
||||
|
||||
// init language
|
||||
const i18n = useI18n()
|
||||
onMounted(() => {
|
||||
const language = globalStore.language ?? getBrowserLang()
|
||||
i18n.locale.value = language
|
||||
globalStore.setGlobalState('language', language as LanguageType)
|
||||
})
|
||||
|
||||
// element language
|
||||
const locale = computed(() => {
|
||||
if (globalStore.language == 'zh') return zhCn
|
||||
if (globalStore.language == 'en') return en
|
||||
return getBrowserLang() == 'zh' ? zhCn : en
|
||||
})
|
||||
|
||||
// element assemblySize
|
||||
const assemblySize = computed(() => globalStore.assemblySize)
|
||||
|
||||
// element button config
|
||||
const buttonConfig = reactive({ autoInsertSpace: false })
|
||||
|
||||
document.getElementById('loadingPage')?.remove()
|
||||
</script>
|
||||
<style scoped>
|
||||
#app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
13
frontend/src/api/activate/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import http from '@/api'
|
||||
|
||||
export const generateApplicationCode = () => {
|
||||
return http.post(`/activate/generateApplicationCode`)
|
||||
}
|
||||
|
||||
export const verifyActivationCode = (activationCode: string) => {
|
||||
return http.post(`/activate/verifyActivationCode`, { activationCode })
|
||||
}
|
||||
|
||||
export const getLicense = () => {
|
||||
return http.post(`/activate/getLicense`)
|
||||
}
|
||||
16
frontend/src/api/activate/interface/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// 激活模块
|
||||
export namespace Activate {
|
||||
export interface ActivateModule {
|
||||
/**
|
||||
* 是否永久激活,1 表示永久激活
|
||||
*/
|
||||
permanently: number;
|
||||
}
|
||||
|
||||
export type ActivationCodePlaintext = Record<string, ActivateModule>;
|
||||
|
||||
export interface ActivationModuleStatus extends ActivateModule {
|
||||
key: string;
|
||||
label: string;
|
||||
}
|
||||
}
|
||||
47
frontend/src/api/helper/axiosCancel.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// ? 暂未使用,目前使用全局 Loading 来控制重复请求
|
||||
import { CustomAxiosRequestConfig } from "../index";
|
||||
import qs from "qs";
|
||||
|
||||
// 声明一个 Map 用于存储每个请求的标识 和 取消函数
|
||||
let pendingMap = new Map<string, AbortController>();
|
||||
|
||||
// 序列化参数
|
||||
export const getPendingUrl = (config: CustomAxiosRequestConfig) =>
|
||||
[config.method, config.url, qs.stringify(config.data), qs.stringify(config.params)].join("&");
|
||||
|
||||
export class AxiosCanceler {
|
||||
/**
|
||||
* @description: 添加请求
|
||||
* @param {Object} config
|
||||
* @return void
|
||||
*/
|
||||
addPending(config: CustomAxiosRequestConfig) {
|
||||
// 在请求开始前,对之前的请求做检查取消操作
|
||||
this.removePending(config);
|
||||
const url = getPendingUrl(config);
|
||||
const controller = new AbortController();
|
||||
config.signal = controller.signal;
|
||||
pendingMap.set(url, controller);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 移除请求
|
||||
* @param {Object} config
|
||||
*/
|
||||
removePending(config: CustomAxiosRequestConfig) {
|
||||
const url = getPendingUrl(config);
|
||||
// 如果在 pending 中存在当前请求标识,需要取消当前请求
|
||||
const controller = pendingMap.get(url);
|
||||
controller && controller.abort();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 清空所有pending
|
||||
*/
|
||||
removeAllPending() {
|
||||
pendingMap.forEach(controller => {
|
||||
controller && controller.abort();
|
||||
});
|
||||
pendingMap.clear();
|
||||
}
|
||||
}
|
||||
43
frontend/src/api/helper/checkStatus.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
/**
|
||||
* @description: 校验网络请求状态码
|
||||
* @param {Number} status
|
||||
* @return void
|
||||
*/
|
||||
export const checkStatus = (status: number) => {
|
||||
switch (status) {
|
||||
case 400:
|
||||
ElMessage.error("请求失败!请您稍后重试");
|
||||
break;
|
||||
// case 401:
|
||||
// ElMessage.error("登录失效!请您重新登录");
|
||||
// break;
|
||||
case 403:
|
||||
ElMessage.error("当前账号无权限访问!");
|
||||
break;
|
||||
case 404:
|
||||
ElMessage.error("你所访问的资源不存在!");
|
||||
break;
|
||||
case 405:
|
||||
ElMessage.error("请求方式错误!请您稍后重试");
|
||||
break;
|
||||
case 408:
|
||||
ElMessage.error("请求超时!请您稍后重试");
|
||||
break;
|
||||
case 500:
|
||||
ElMessage.error("服务异常!");
|
||||
break;
|
||||
case 502:
|
||||
ElMessage.error("网关错误!");
|
||||
break;
|
||||
case 503:
|
||||
ElMessage.error("服务不可用!");
|
||||
break;
|
||||
case 504:
|
||||
ElMessage.error("网关超时!");
|
||||
break;
|
||||
default:
|
||||
ElMessage.error("请求失败!");
|
||||
}
|
||||
};
|
||||
242
frontend/src/api/index.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
import axios, {
|
||||
AxiosError,
|
||||
type AxiosInstance,
|
||||
type AxiosRequestConfig,
|
||||
type AxiosResponse,
|
||||
type InternalAxiosRequestConfig
|
||||
} from 'axios'
|
||||
import { showFullScreenLoading, tryHideFullScreenLoading } from '@/components/Loading/fullScreen'
|
||||
import { LOGIN_URL } from '@/config'
|
||||
import { type ResultData } from '@/api/interface'
|
||||
import { ResultEnum } from '@/enums/httpEnum'
|
||||
import { checkStatus } from './helper/checkStatus'
|
||||
import { useUserStore } from '@/stores/modules/user'
|
||||
import router from '@/routers'
|
||||
import { refreshToken } from '@/api/user/login'
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill'
|
||||
|
||||
export interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
const config = {
|
||||
// 默认地址请求地址,可在 .env 开头文件中修改
|
||||
baseURL: import.meta.env.VITE_API_URL as string,
|
||||
// 设置超时时间(60s)
|
||||
timeout: 60000,
|
||||
// 跨域时候允许携带凭证
|
||||
withCredentials: true,
|
||||
// post请求指定数据类型以及编码
|
||||
headers: { 'Content-Type': 'application/json;charset=utf-8' }
|
||||
}
|
||||
|
||||
class RequestHttp {
|
||||
service: AxiosInstance
|
||||
|
||||
public constructor(config: AxiosRequestConfig) {
|
||||
// 创建实例
|
||||
this.service = axios.create(config)
|
||||
|
||||
/**
|
||||
* @description 请求拦截器
|
||||
* 客户端发送请求 -> [请求拦截器] -> 服务器
|
||||
* token校验(JWT) : 接受服务器返回的 token,存储到 vuex/pinia/本地储存当中
|
||||
*/
|
||||
this.service.interceptors.request.use(
|
||||
(config: CustomAxiosRequestConfig) => {
|
||||
isFirst = true
|
||||
const userStore = useUserStore()
|
||||
// 当前请求不需要显示 loading,在 api 服务中通过指定的第三个参数: { loading: false } 来控制
|
||||
config.loading ?? (config.loading = true)
|
||||
config.loading && showFullScreenLoading()
|
||||
if (config.headers && typeof config.headers.set === 'function') {
|
||||
config.headers.set('Authorization', 'Bearer ' + userStore.accessToken)
|
||||
config.headers.set('Is-Refresh-Token', userStore.isRefreshToken + '')
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
let isFirst = true
|
||||
/**
|
||||
* @description 响应拦截器
|
||||
* 服务器换返回信息 -> [拦截统一处理] -> 客户端JS获取到信息
|
||||
*/
|
||||
this.service.interceptors.response.use(
|
||||
async (response: AxiosResponse) => {
|
||||
const { data } = response
|
||||
const userStore = useUserStore()
|
||||
tryHideFullScreenLoading()
|
||||
|
||||
if (data.code === ResultEnum.ACCESSTOKEN_EXPIRED) {
|
||||
// 用长token去换短token
|
||||
userStore.setAccessToken(userStore.refreshToken)
|
||||
userStore.setIsRefreshToken(true)
|
||||
const result = await refreshToken()
|
||||
if (result) {
|
||||
//获取新token成功的话
|
||||
// 有新的token后,重新请求
|
||||
userStore.setAccessToken(result.data.accessToken)
|
||||
userStore.setRefreshToken(result.data.refreshToken)
|
||||
userStore.setIsRefreshToken(false)
|
||||
userStore.setExp(Date.now() + 1000 * 60 * 60 * 24 * 30)
|
||||
response.config.headers.Authorization = `Bearer ${result.data.accessToken}` //重新请求前需要将更新后的新token更换掉之前无效的token,不然会死循环
|
||||
const resp = await this.service.request(response.config)
|
||||
return resp
|
||||
} else {
|
||||
// 刷新失效,跳转登录页
|
||||
}
|
||||
}
|
||||
// 登陆失效
|
||||
if (data.code === ResultEnum.OVERDUE) {
|
||||
//console.log('登陆失效')
|
||||
userStore.setAccessToken('')
|
||||
userStore.setRefreshToken('')
|
||||
userStore.setIsRefreshToken(false)
|
||||
userStore.setUserInfo({ id: '', name: '' })
|
||||
userStore.setExp(0)
|
||||
await router.replace(LOGIN_URL)
|
||||
if (isFirst) {
|
||||
//临时处理token失效弹窗多次
|
||||
ElMessage.error(data.message)
|
||||
isFirst = false
|
||||
}
|
||||
return Promise.reject(data)
|
||||
}
|
||||
// 全局错误信息拦截(防止下载文件的时候返回数据流,没有 code 直接报错)
|
||||
if (data.code && data.code !== ResultEnum.SUCCESS) {
|
||||
if (data.message.includes('&')) {
|
||||
let formattedMessage = data.message.split('&').join('<br>')
|
||||
if (data.message.includes(':')) {
|
||||
formattedMessage = formattedMessage.replace(':', '')
|
||||
}
|
||||
ElMessage.error({ message: formattedMessage, dangerouslyUseHTMLString: true })
|
||||
return Promise.reject(data)
|
||||
}
|
||||
|
||||
ElMessage.error(data.message)
|
||||
return Promise.reject(data)
|
||||
}
|
||||
// 成功请求(在页面上除非特殊情况,否则不用处理失败逻辑)
|
||||
|
||||
if (userStore.exp <= Date.now() && userStore.exp !== 0) {
|
||||
userStore.setAccessToken('')
|
||||
userStore.setRefreshToken('')
|
||||
userStore.setIsRefreshToken(false)
|
||||
userStore.setUserInfo({ id: '', name: '' })
|
||||
userStore.setExp(0)
|
||||
ElMessage.error('登录已过期,请重新登录!')
|
||||
await router.replace(LOGIN_URL)
|
||||
return Promise.reject(data)
|
||||
}
|
||||
// 对于blob类型的响应,返回完整的response对象以保留响应头
|
||||
if (response.config.responseType === 'blob') {
|
||||
return response
|
||||
}
|
||||
return data
|
||||
},
|
||||
async (error: AxiosError) => {
|
||||
const { response } = error
|
||||
tryHideFullScreenLoading()
|
||||
//console.log('error', error.message)
|
||||
// 请求超时 && 网络错误单独判断,没有 response
|
||||
if (error.message.indexOf('timeout') !== -1) ElMessage.error('请求超时!请您稍后重试')
|
||||
if (error.message.indexOf('Network Error') !== -1) ElMessage.error('网络错误!请您稍后重试')
|
||||
// 根据服务器响应的错误状态码,做不同的处理
|
||||
if (response) checkStatus(response.status)
|
||||
// 服务器结果都没有返回(可能服务器错误可能客户端断网),断网处理:可以跳转到断网页面
|
||||
if (!window.navigator.onLine) router.replace('/500')
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 常用请求方法封装
|
||||
*/
|
||||
get<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
|
||||
return this.service.get(url, { params, ..._object })
|
||||
}
|
||||
|
||||
post<T>(url: string, params?: object | string, _object = {}): Promise<ResultData<T>> {
|
||||
return this.service.post(url, params, _object)
|
||||
}
|
||||
|
||||
put<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
|
||||
return this.service.put(url, params, _object)
|
||||
}
|
||||
|
||||
delete<T>(url: string, params?: any, _object = {}): Promise<ResultData<T>> {
|
||||
return this.service.delete(url, { params, ..._object })
|
||||
}
|
||||
|
||||
download(url: string, params?: object, _object = {}): Promise<BlobPart> {
|
||||
return this.service.post(url, params, { ..._object, responseType: 'blob' }).then(res => res.data)
|
||||
}
|
||||
|
||||
downloadWithHeaders(url: string, params?: object, _object = {}): Promise<AxiosResponse<Blob>> {
|
||||
return this.service.post(url, params, { ..._object, responseType: 'blob' })
|
||||
}
|
||||
|
||||
upload(url: string, params?: object, _object = {}): Promise<BlobPart> {
|
||||
return this.service.post(url, params, {
|
||||
..._object,
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 针对excel的上传,默认返回的是blob类型,Excel没问题时返回json特殊处理
|
||||
*/
|
||||
uploadExcel(url: string, params?: object, _object = {}): Promise<BlobPart> {
|
||||
return this.service
|
||||
.post(url, params, {
|
||||
..._object,
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
responseType: 'blob'
|
||||
})
|
||||
.then(res => res.data)
|
||||
}
|
||||
|
||||
// 添加SSE连接方法
|
||||
sse(url: string, params?: any): EventSource {
|
||||
const userStore = useUserStore()
|
||||
// 构造带参数的URL
|
||||
let requestUrl = config.baseURL + url
|
||||
if (params) {
|
||||
const searchParams = new URLSearchParams()
|
||||
for (const key in params) {
|
||||
if (Object.prototype.hasOwnProperty.call(params, key)) {
|
||||
searchParams.append(key, String(params[key]))
|
||||
}
|
||||
}
|
||||
requestUrl += '?' + searchParams.toString()
|
||||
}
|
||||
|
||||
// 创建EventSource连接
|
||||
const eventSource = new EventSourcePolyfill(requestUrl, {
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + userStore.accessToken
|
||||
},
|
||||
// 增加超时时间到1200秒
|
||||
heartbeatTimeout: 1200000
|
||||
})
|
||||
|
||||
// 设置默认的Authorization头部
|
||||
eventSource.addEventListener('open', function () {
|
||||
//console.log('SSE连接已建立')
|
||||
})
|
||||
// 添加错误处理
|
||||
eventSource.addEventListener('error', function (err) {
|
||||
console.error('SSE连接错误:', err)
|
||||
})
|
||||
|
||||
return eventSource
|
||||
}
|
||||
}
|
||||
|
||||
export default new RequestHttp(config)
|
||||
58
frontend/src/api/interface/index.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { storeToRefs } from 'pinia';
|
||||
/**
|
||||
* 该接口声明文件用来声明通用的接口定义,比如 请求参数Base、响应Base、分页等
|
||||
*/
|
||||
|
||||
/**
|
||||
* 请求响应参数(不包含data)
|
||||
*/
|
||||
export interface Result {
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求响应参数(包含data)
|
||||
*/
|
||||
export interface ResultData<T = any> extends Result {
|
||||
map(arg0: (item: any) => { label: any; value: any; }): { label: string; value: string; }[] | { label: string; value: string; }[];
|
||||
data: T;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 分页请求参数
|
||||
*/
|
||||
export interface ReqPage {
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页响应参数
|
||||
*/
|
||||
export interface ResPage<T> {
|
||||
records: T[];
|
||||
current: number;
|
||||
size: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dict 字典属性
|
||||
* id: 唯一标识
|
||||
* label: 名称
|
||||
* code: 类型下唯一标识
|
||||
*/
|
||||
export interface Dict {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
value?: string;
|
||||
sort?:number;
|
||||
algoDescribe?: string;
|
||||
children?: Dict[];
|
||||
}
|
||||
|
||||
|
||||
|
||||
9
frontend/src/api/system/config/serviceName.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
// 系统模块前缀
|
||||
export const ADMIN = "/admin";
|
||||
|
||||
// 用户模块前缀
|
||||
export const USER = "/user-boot";
|
||||
|
||||
|
||||
// todo... 其他业务模块前缀
|
||||
33
frontend/src/api/system/dictionary/dictData/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import http from '@/api'
|
||||
import {type Dict} from '@/api/system/dictionary/interface'
|
||||
|
||||
//获取字典数据
|
||||
export const getDictDataListByTypeId = (params: Dict.ReqDictDataParams) => {
|
||||
return http.post(`/dictData/listByTypeId`, params)
|
||||
}
|
||||
|
||||
//添加字典数据
|
||||
export const addDictData = (params: Dict.ResDictData) => {
|
||||
return http.post(`/dictData/add`, params)
|
||||
}
|
||||
|
||||
//编辑字典数据
|
||||
export const updateDictData = (params: Dict.ResDictData) => {
|
||||
return http.post(`/dictData/update`, params)
|
||||
}
|
||||
|
||||
//删除字典数据
|
||||
export const deleteDictData = (params: string[]) => {
|
||||
return http.post(`/dictData/delete`, params)
|
||||
}
|
||||
|
||||
export const getDicDataById = (params: string) => {
|
||||
return http.post('/dictData/getDicDataById', params)
|
||||
}
|
||||
|
||||
//导出字典数据
|
||||
export const exportDictData = (params: Dict.ReqDictDataParams) => {
|
||||
return http.download(`/dictData/export`, params)
|
||||
}
|
||||
|
||||
|
||||
29
frontend/src/api/system/dictionary/dictTree/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import http from '@/api'
|
||||
import { type Dict } from '@/api/system/dictionary/interface'
|
||||
import { c } from 'vite/dist/node/types.d-aGj9QkWt'
|
||||
|
||||
//获取字典类型
|
||||
export const getDictTreeByCode = (params: Dict.ResDictTree) => {
|
||||
const code = params.code || ''
|
||||
return http.get(`/dictTree/getTreeByCode?code=${code}`, { loading: true })
|
||||
}
|
||||
|
||||
export const getDictTreeByName = (params: Dict.ResDictTree) => {
|
||||
const name = params.name || ''
|
||||
return http.get(`/dictTree/getTreeByName?name=${name}`)
|
||||
}
|
||||
|
||||
//添加字典类型
|
||||
export const addDictTree = (params: Dict.ResDictTree) => {
|
||||
return http.post(`/dictTree/add`, params)
|
||||
}
|
||||
|
||||
//编辑字典类型
|
||||
export const updateDictTree = (params: Dict.ResDictTree) => {
|
||||
return http.post(`/dictTree/update`, params)
|
||||
}
|
||||
|
||||
//删除字典类型
|
||||
export const deleteDictTree = (params: Dict.ResDictTree) => {
|
||||
return http.post(`/dictTree/delete?id=${params.id}`)
|
||||
}
|
||||
27
frontend/src/api/system/dictionary/dictType/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import http from '@/api'
|
||||
import { type Dict } from '@/api/system/dictionary/interface'
|
||||
|
||||
//获取字典类型
|
||||
export const getDictTypeList = (params: Dict.ReqDictTypeParams) => {
|
||||
return http.post(`/dictType/list`, params)
|
||||
}
|
||||
|
||||
//添加字典类型
|
||||
export const addDictType = (params: Dict.ResDictType) => {
|
||||
return http.post(`/dictType/add`, params)
|
||||
}
|
||||
|
||||
//编辑字典类型
|
||||
export const updateDictType = (params: Dict.ResDictType) => {
|
||||
return http.post(`/dictType/update`, params)
|
||||
}
|
||||
|
||||
//删除字典类型
|
||||
export const deleteDictType = (params: string[]) => {
|
||||
return http.post(`/dictType/delete`, params)
|
||||
}
|
||||
|
||||
//导出字典类型
|
||||
export const exportDictType=(params: Dict.ReqDictTypeParams)=>{
|
||||
return http.download(`/dictType/export`, params)
|
||||
}
|
||||
169
frontend/src/api/system/dictionary/interface/index.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import type { ReqPage, ResPage } from '@/api/interface'
|
||||
|
||||
export namespace Dict {
|
||||
|
||||
/**
|
||||
* 一个单表的CRUD需要申明一下几个对象
|
||||
* 1、表格分页查询对象,字段:查询字段?、页码、每页条数;
|
||||
* 2、新增、修改、根据id查询返回的对象;
|
||||
* 3、表格查询分页返回的对象;
|
||||
*/
|
||||
|
||||
/**
|
||||
* 字典类型表格分页查询参数
|
||||
*/
|
||||
export interface ReqDictTypeParams extends ReqPage{
|
||||
name?: string; // 名称
|
||||
code?: string; // 编码
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典类型新增、修改、根据id查询返回的对象
|
||||
*/
|
||||
export interface ResDictType {
|
||||
id: string; // 字典类型表Id
|
||||
name: string; // 名称
|
||||
code: string; // 编码
|
||||
sort: number; // 排序
|
||||
openLevel: number; // 开启等级:0-不开启;1-开启,默认不开启
|
||||
openDescribe: number; // 开启描述:0-不开启;1-开启,默认不开启
|
||||
remark?: string | null; // 描述
|
||||
state: number; // 状态:0-删除 1-正常
|
||||
createBy?: string | null; // 创建用户
|
||||
createTime?: string | null; // 创建时间
|
||||
updateBy?: string | null; // 更新用户
|
||||
updateTime?: string | null; // 更新时间
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典类型表格查询分页返回的对象;
|
||||
*/
|
||||
export interface ResDictTypePage extends ResPage<ResDictType> {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 字典数据表格分页查询参数
|
||||
*/
|
||||
export interface ReqDictDataParams extends ReqPage{
|
||||
typeId: string; // 类型id 必填
|
||||
name?: string; // 名称
|
||||
code?: string; // 编码
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典数据新增、修改、根据id查询返回的对象
|
||||
*/
|
||||
export interface ResDictData {
|
||||
id: string; // 字典数据表Id
|
||||
typeId: string; // 字典类型表Id
|
||||
name: string; // 名称
|
||||
code: string; // 编码
|
||||
sort: number; // 排序
|
||||
openValue?: number | null;
|
||||
level?: number | null; // 事件等级:0-普通;1-中等;2-严重 (默认为0)
|
||||
algoDescribe?: number | null; // 与高级算法内部Id描述对应
|
||||
value?: string | null; // 字典针对电压等级
|
||||
dictValue?:string|null;
|
||||
state: number; // 状态:0-删除 1-正常
|
||||
createBy?: string | null; // 创建用户
|
||||
createTime?: string | null; // 创建时间
|
||||
updateBy?: string | null; // 更新用户
|
||||
updateTime?: string | null; // 更新时间
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典数据表格查询分页返回的对象;
|
||||
*/
|
||||
export interface ResDictDataPage extends ResPage<ResDictData> {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 电能质量指标字典数据表格分页查询参数
|
||||
*/
|
||||
export interface ReqDictPqParams extends ReqPage{
|
||||
id: string; // 类型id 必填
|
||||
name?: string; // 名称
|
||||
phase?: string;//相别
|
||||
dataType?: string;//数据模型(epd、pqd...)
|
||||
}
|
||||
|
||||
/**
|
||||
* 电能质量指标字典数据新增、修改、根据id查询返回的对象
|
||||
*/
|
||||
export interface ResDictPq {
|
||||
id: string;//指标字典表Id
|
||||
name: string;//指标名称
|
||||
phase: string;//相别
|
||||
dataType: string;//数据模型(epd、pqd...)
|
||||
otherName?: string ;//别名(默认与Name相同,主要是为了适配不同数据库里面字段)
|
||||
showName?:string | null;//显示名称
|
||||
sort:number;//排序
|
||||
type?: string | null;//指标数据类型(整型、浮点型、枚举型这些的)
|
||||
unit?: string | null;//单位
|
||||
harmStart?:number | null;//起始次数
|
||||
harmEnd?:number | null;//结束次数
|
||||
classId: string ;//数据表表名
|
||||
statMethod?:string;//数据统计类型(最大、最小、平均、CP95)
|
||||
systemType?:string | null;//系统类别(区分用能/电能)
|
||||
tranFlag?:number ;//数据是否上送(0:不上送 1:上送)
|
||||
tranRule?:string | null;//上送规则 变化:“change”周期 :“ period”
|
||||
eventType?:string | null;//evt的事件类别 "1"、"2";
|
||||
storeFlag?:string ;//sts、di的是否存储 1:存储 0:不存 储;
|
||||
curSts?:number | null;//sts、do的当前值;
|
||||
ctlSts?:number;//do的是否可远程控制 1:是 0:否;
|
||||
maxNum?:number | null;//设置最大值
|
||||
minNum?: number| null;//设置最小值
|
||||
setValue?:string | null;//参数为enum可设置的所有值序列
|
||||
strlen?:number | null;//参数string可设置字符串的长度上 限
|
||||
defaultValue?:string | null; //参数缺省值、告警code值
|
||||
resourcesId?:string ; //报表数据来源(统计表表名)
|
||||
limitName?:string | null; //限值字段名称
|
||||
limitTable?:string | null;//限值表名
|
||||
formula?:string ;//超标判断方式
|
||||
primaryFormula?:string | null;//二次值转一次值公式
|
||||
state:number;//状态:0-删除 1-正常
|
||||
createBy?:string | null;//创建用户
|
||||
createTime?:string | null;//创建时间
|
||||
updateBy?:string | null;//更新用户
|
||||
updateTime?:string | null;//更新时间
|
||||
}
|
||||
|
||||
/**
|
||||
* 电能质量指标字典数据表格查询分页返回的对象;
|
||||
*/
|
||||
export interface ResDictPqPage extends ResPage<ResDictPq> {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 树形字典数据新增、修改、根据id查询返回的对象
|
||||
*/
|
||||
export interface ResDictTree {
|
||||
id: string;//指标字典表Id
|
||||
pid: string;//
|
||||
pids: string;//
|
||||
name:string;//
|
||||
code:string;//
|
||||
sort:number;//
|
||||
remark?:string;//
|
||||
state?:number;//'状态(字典 0正常 1停用 2删除)
|
||||
createBy?:string | null;//
|
||||
createTime?:string | null;//
|
||||
updateBy?:string | null;//
|
||||
updateTime?:string | null;//
|
||||
level?:number | null;//
|
||||
extend?:string | null;//对应type,不同类型可自定义配置
|
||||
type?:number | null;//用于区分多种类型的字典树 0.台账对象类型 1.自定义报表指标类型
|
||||
children?: ResDictTree[];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
16
frontend/src/api/system/log/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type {AuditLog } from '@/api/system/log/interface/log.ts'
|
||||
import http from '@/api'
|
||||
|
||||
/**
|
||||
* @name 审计日志管理模块
|
||||
*/
|
||||
|
||||
//获取审计日志
|
||||
export const getAuditLog = (params: AuditLog.ReqAuditLogParams) => {
|
||||
return http.post(`/sysLog/list`, params)
|
||||
}
|
||||
|
||||
|
||||
export const exportCsv = (params: AuditLog.ReqAuditLogParams) => {
|
||||
return http.download(`/sysLog/exportCSV`, params)
|
||||
}
|
||||
34
frontend/src/api/system/log/interface/log.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { ReqPage, ResPage } from '@/api/interface'
|
||||
|
||||
// 审计日志管理模块
|
||||
export namespace AuditLog {
|
||||
/**
|
||||
* 审计日志分页查询参数
|
||||
*/
|
||||
export interface ReqAuditLogParams extends ReqPage {
|
||||
id: string; //审计日志Id 必填
|
||||
createTime?: string; //创建时间
|
||||
}
|
||||
|
||||
/**
|
||||
* 审计日志详情
|
||||
*/
|
||||
export interface ResAuditLog {
|
||||
id: string;//审计日志Id
|
||||
operate_Type:string;//操作类型
|
||||
ip:string;//操作IP
|
||||
result: string;//事件结果
|
||||
remark: string;//事件描述
|
||||
level:number;//告警等级
|
||||
warn:number;//告警标志
|
||||
create_By:string;//创建用户
|
||||
create_Time:string;//创建时间
|
||||
sort:number;//排序
|
||||
}
|
||||
|
||||
/**
|
||||
* 审计日志分页结果
|
||||
*/
|
||||
export interface ResAuditLogPage extends ResPage<ResAuditLog> {
|
||||
}
|
||||
}
|
||||
35
frontend/src/api/user/function/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import http from "@/api";
|
||||
import type { Function } from "@/api/user/interface/function";
|
||||
|
||||
|
||||
|
||||
// 获取资源
|
||||
export const getFunctionList = (params:Function.ResFunction) => {
|
||||
const name = params.name || '';
|
||||
return http.get<Function.ResFunction>(`/sysFunction/getTree?keyword=${name}`)
|
||||
}
|
||||
|
||||
|
||||
// 获取资源不包括按钮
|
||||
export const getFunctionListNoButton = () => {
|
||||
return http.get<Function.ResFunction>(`/sysFunction/functionTreeNoButton`)
|
||||
}
|
||||
|
||||
|
||||
//添加菜单列表
|
||||
export const addFunction = (params: Function.ResFunction) => {
|
||||
return http.post(`/sysFunction/add`,params);
|
||||
};
|
||||
|
||||
//删除菜单列表
|
||||
export const deleteFunction = (params: Function.ResFunction) => {
|
||||
return http.post(`/sysFunction/delete?id=${params.id}`);
|
||||
};
|
||||
|
||||
//编辑菜单列表
|
||||
export const updateFunction = (params: Function.ResFunction) => {
|
||||
return http.post(`/sysFunction/update`, params);
|
||||
};
|
||||
|
||||
|
||||
|
||||
45
frontend/src/api/user/interface/function.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { ReqPage, ResPage } from '@/api/interface'
|
||||
|
||||
// 菜单管理模块
|
||||
export namespace Function {
|
||||
|
||||
/**
|
||||
* 菜单管理表格分页查询参数
|
||||
*/
|
||||
export interface ReqFunctionParams extends ReqPage{
|
||||
name?: string; // 名称
|
||||
code?: string; // 编码
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 菜单管理新增、修改、根据id查询返回的对象
|
||||
*/
|
||||
export interface ResFunction {
|
||||
id: string;//资源表Id
|
||||
pid:string;//节点(0为根节点)
|
||||
pids?:string | null;//节点上层所有节点
|
||||
name: string;//名称
|
||||
code:string;//资源标识
|
||||
path:string;//路由路径
|
||||
component:string ;//组件地址
|
||||
icon?:string;//图标
|
||||
sort:number;//排序
|
||||
type:number;//资源类型0-菜单、1-按钮、2-公共资源、3-服务间调用资源
|
||||
remark?: string | null;//权限资源描述
|
||||
state:number;//权限资源状态
|
||||
create_By?:string | null;//创建人
|
||||
create_Time?:string | null;//创建时间
|
||||
update_By?:string | null;//更新人
|
||||
update_Time?:string | null;//更新时间
|
||||
children?: ResFunction[] | null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 菜单管理表格查询分页返回的对象;
|
||||
*/
|
||||
export interface ResFunctionPage extends ResPage<ResFunction> {
|
||||
|
||||
}
|
||||
}
|
||||
93
frontend/src/api/user/interface/role.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import type { ReqPage, ResPage } from '@/api/interface'
|
||||
|
||||
// 角色管理模块
|
||||
export namespace Role {
|
||||
|
||||
|
||||
/**
|
||||
* 用户数据表格分页查询参数
|
||||
*/
|
||||
export interface ReqRoleParams extends ReqPage{
|
||||
id: string; // 装置序号用户ID 必填
|
||||
name?: string; //用户名(别名)
|
||||
code?: string; //角色代码
|
||||
}
|
||||
//角色接口
|
||||
export interface RoleBO {
|
||||
id: string; //角色类型ID
|
||||
name: string; //角色类型名称
|
||||
code: string; //角色代码
|
||||
type: number; //角色类型
|
||||
remark?:string; //角色描述
|
||||
state:number;
|
||||
createBy?:string; //
|
||||
createTime?: string; // 创建时间
|
||||
updateBy?: string; //
|
||||
updateTime?: string; // 更新时间
|
||||
}
|
||||
|
||||
//角色接口
|
||||
export interface RoleFunctionId {
|
||||
id: string[]; //菜单id
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户表格查询分页返回的对象;
|
||||
*/
|
||||
export interface ResRolePage extends ResPage<RoleBO> {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// export interface Permission{
|
||||
// key: string; //权限名称
|
||||
// label: string; //权限ID
|
||||
// disabled:boolean; //是否拥有该权限
|
||||
// }
|
||||
|
||||
// 角色列表
|
||||
// export interface ResRoleList {
|
||||
// id: string; //角色类型ID
|
||||
// rolename: string; //角色类型名称
|
||||
// status: number; //角色类型状态
|
||||
// describe:string; //角色描述
|
||||
// permissionList?:Permission[]; //角色权限列表
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
// export interface ReqRoleParams extends ReqPage {
|
||||
// id: string; //角色类型ID
|
||||
// rolename: string; //角色类型名称
|
||||
// status: number; //角色类型状态
|
||||
// describe:string; //角色描述
|
||||
// permissionList?:Permission[]; //角色权限列表
|
||||
// }
|
||||
// 角色字典
|
||||
// export interface ResStatus {
|
||||
// roleLabel: string;
|
||||
// roleValue: number;
|
||||
// }
|
||||
|
||||
// export interface ResGender {
|
||||
// genderLabel: string;
|
||||
// genderValue: number;
|
||||
// }
|
||||
|
||||
//角色权限列表
|
||||
// export interface ResPermissionList {
|
||||
// key: string;
|
||||
// label: string;
|
||||
// disable?: Permission[];
|
||||
// }
|
||||
|
||||
// export interface ResRole {
|
||||
// id: string;
|
||||
// name: string;
|
||||
// children?: ResDepartment[];
|
||||
// }
|
||||
}
|
||||
79
frontend/src/api/user/interface/user.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
// 登录模块
|
||||
import type { ReqPage, ResPage } from '@/api/interface'
|
||||
|
||||
export namespace Login {
|
||||
export interface ReqLoginForm {
|
||||
username: string
|
||||
password: string
|
||||
checked: boolean
|
||||
}
|
||||
export interface ResLogin {
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
userInfo: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
export interface ResAuthButtons {
|
||||
[key: string]: string[]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 用户管理模块
|
||||
export namespace User {
|
||||
|
||||
/**
|
||||
* 用户数据表格分页查询参数
|
||||
*/
|
||||
export interface ReqUserParams extends ReqPage{
|
||||
id: string; // 装置序号用户ID 必填
|
||||
name?: string; //用户名(别名)
|
||||
loginTime?: string;//最后一次登录时间
|
||||
}
|
||||
|
||||
// 用户接口
|
||||
export interface ResUser {
|
||||
id: string; //用户ID,作为唯一标识
|
||||
name: string; //用户名(别名)
|
||||
loginName: string;//登录名
|
||||
deptId?: number;//部门ID
|
||||
password: string; //密码
|
||||
phone?: string; //手机号
|
||||
email?: string; //邮箱
|
||||
loginTime?: string;//最后一次登录时间
|
||||
loginErrorTimes: number;//登录错误次数
|
||||
lockTime?: string; //用户密码错误锁定时间
|
||||
state:number;//0-删除;1-正常;2-锁定;3-待审核;4-休眠;5-密码过期
|
||||
createBy?: string;//创建用户
|
||||
createTime?: string;//创建时间
|
||||
updateBy?: string;//更新用户
|
||||
updateTime?: string;//更新时间
|
||||
roleIds?: string[]; //
|
||||
roleNames?:string[]; //
|
||||
roleCodes?:string[]; //
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
// 用户接口
|
||||
export interface ResPassWordUser {
|
||||
id: string; //用户ID,作为唯一标识
|
||||
oldPassword: string; //密码
|
||||
newPassword: string; //新密码
|
||||
surePassword:string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 用户表格查询分页返回的对象;
|
||||
*/
|
||||
export interface ResUserPage extends ResPage<ResUser> {
|
||||
|
||||
}
|
||||
// // 用户+分页
|
||||
// export interface ReqUserParams extends ReqPage,UserBO {
|
||||
|
||||
// }
|
||||
}
|
||||
|
||||
47
frontend/src/api/user/login/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type {Login} from '@/api/user/interface/user'
|
||||
import {ADMIN as rePrefix} from '@/api/system/config/serviceName'
|
||||
import http from '@/api'
|
||||
import type {Dict} from '@/api/interface'
|
||||
|
||||
/**
|
||||
* @name 登录模块
|
||||
*/
|
||||
// 用户登录
|
||||
export const loginApi = (params: { username: string; password: string}) => {
|
||||
return http.post<Login.ResLogin>(`${rePrefix}/login`, params, {loading: false})
|
||||
// return http.post<Login.ResLogin>(`/Register1`, params, { loading: false })
|
||||
}
|
||||
// 获取菜单列表
|
||||
export const getAuthMenuListApi = () => {
|
||||
return http.get<Menu.MenuOptions[]>(`/sysFunction/getMenu`, {}, {loading: false})
|
||||
// return http.post<Menu.MenuOptions[]>(`/Register2`, {}, { loading: false })
|
||||
}
|
||||
// 获取按钮权限
|
||||
export const getAuthButtonListApi = () => {
|
||||
return http.get<Login.ResAuthButtons>(`/sysFunction/getButton`, {}, {loading: false})
|
||||
// return http.post<Login.ResAuthButtons>(`/Register3`, {}, { loading: false })
|
||||
}
|
||||
// 用户退出登录
|
||||
export const logoutApi = () => {
|
||||
return http.post(`${rePrefix}/logout`)
|
||||
}
|
||||
|
||||
//获取下拉框列表
|
||||
export const getDictList = () => {
|
||||
return http.get<Dict>('/dictData/dictDataCache')
|
||||
}
|
||||
|
||||
//token刷新
|
||||
export const refreshToken = () => {
|
||||
return http.get<Login.ResLogin>(`${rePrefix}/refreshToken`,
|
||||
{},
|
||||
{loading: false}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取RSA公钥
|
||||
*/
|
||||
export const getPublicKey = (username: string) => {
|
||||
return http.get(`/admin/getPublicKey?username=${username}`, {}, {loading: false})
|
||||
}
|
||||
46
frontend/src/api/user/role/index.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { Role } from '@/api/user/interface/role'
|
||||
import type { Function } from '@/api/user/interface/function'
|
||||
import http from '@/api'
|
||||
|
||||
/**
|
||||
* @name 角色管理模块
|
||||
*/
|
||||
// 获取角色列表
|
||||
export const getRoleList = (params: Role.ReqRoleParams) => {
|
||||
return http.post(`/sysRole/list`, params)
|
||||
|
||||
// return http.post<ResPage<Role.ResRoleList>>(`/RoleList_Post`, params)
|
||||
// return http.post<ResPage<Role.ResRoleList>>(`${rePrefix}/role/list`, params)
|
||||
}
|
||||
|
||||
// 新增角色
|
||||
export const addRole = (params: Role.RoleBO) => {
|
||||
return http.post(`/sysRole/add`, params)
|
||||
}
|
||||
|
||||
// 编辑角色
|
||||
export const editRole = (params: Role.RoleBO) => {
|
||||
return http.post(`/sysRole/update`, params)
|
||||
}
|
||||
|
||||
// 删除角色
|
||||
export const deleteRole = (params: { id: string[] }) => {
|
||||
return http.post(`/sysRole/delete`, params)
|
||||
}
|
||||
|
||||
|
||||
// 获取资源
|
||||
export const getFunctionList = () => {
|
||||
return http.get<Function.ResFunction>(`/sysFunction/getTree?keyword=`)
|
||||
}
|
||||
|
||||
|
||||
//获取角色id绑定的菜单
|
||||
export const getRoleFunction = (params:Role.RoleBO) => {
|
||||
return http.post(`/sysFunction/getFunctionsByRoleId?id=${params.id}`)
|
||||
}
|
||||
|
||||
//角色分配菜单
|
||||
export const assignFunction = (params:Role.RoleBO,param:Role.RoleFunctionId) => {
|
||||
return http.post(`/sysFunction/assignFunctionByRoleId`,{ roleId: params.id,functionIds:param.id })
|
||||
}
|
||||
43
frontend/src/api/user/user/index.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { Role } from '@/api/user/interface/role'
|
||||
import type { User } from '@/api/user/interface/user'
|
||||
import http from '@/api'
|
||||
|
||||
/**
|
||||
* @name 用户管理模块
|
||||
*/
|
||||
// 获取用户列表
|
||||
export const getUserList = (params: User.ReqUserParams) => {
|
||||
return http.post(`/sysUser/list`, params)
|
||||
}
|
||||
|
||||
|
||||
// 新增用户
|
||||
export const addUser = (params: User.ResUser) => {
|
||||
return http.post(`/sysUser/add`, params)
|
||||
}
|
||||
|
||||
// 编辑用户
|
||||
export const updateUser = (params: User.ResUser) => {
|
||||
return http.post(`/sysUser/update`, params)
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
export const deleteUser = (params: string[] ) => {
|
||||
return http.post(`/sysUser/delete`, params)
|
||||
}
|
||||
|
||||
|
||||
// 获取角色列表
|
||||
export const getRoleList = () => {
|
||||
return http.get<Role.RoleBO>(`/sysRole/simpleList`)
|
||||
}
|
||||
|
||||
//修改密码
|
||||
export const updatePassWord = (params: User.ResPassWordUser) => {
|
||||
return http.post(`/sysUser/updatePassword`,params)
|
||||
}
|
||||
|
||||
// 获取所有用户
|
||||
export const getAllUser= () => {
|
||||
return http.get(`/sysUser/getAll`)
|
||||
}
|
||||
BIN
frontend/src/assets/fonts/DIN.otf
Normal file
BIN
frontend/src/assets/fonts/MetroDF.ttf
Normal file
BIN
frontend/src/assets/fonts/YouSheBiaoTiHei.ttf
Normal file
14
frontend/src/assets/fonts/font.scss
Normal file
@@ -0,0 +1,14 @@
|
||||
@font-face {
|
||||
font-family: YouSheBiaoTiHei;
|
||||
src: url("./YouSheBiaoTiHei.ttf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: MetroDF;
|
||||
src: url("./MetroDF.ttf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: DIN;
|
||||
src: url("./DIN.Otf");
|
||||
}
|
||||
48
frontend/src/assets/iconfont/iconfont.scss
Normal file
@@ -0,0 +1,48 @@
|
||||
@font-face {
|
||||
font-family: iconfont; /* Project id 2667653 */
|
||||
src: url("iconfont.ttf?t=1694681005434") format("truetype");
|
||||
}
|
||||
.iconfont {
|
||||
font-family: iconfont !important;
|
||||
font-size: 20px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
cursor: pointer;
|
||||
}
|
||||
.icon-yiwen::before {
|
||||
font-size: 15px;
|
||||
content: "\e693";
|
||||
}
|
||||
.icon-xiala::before {
|
||||
content: "\e62b";
|
||||
}
|
||||
.icon-tuichu::before {
|
||||
content: "\e645";
|
||||
}
|
||||
.icon-xiaoxi::before {
|
||||
font-size: 21.2px;
|
||||
content: "\e61f";
|
||||
}
|
||||
.icon-zhuti::before {
|
||||
font-size: 22.4px;
|
||||
content: "\e638";
|
||||
}
|
||||
.icon-sousuo::before {
|
||||
content: "\e611";
|
||||
}
|
||||
.icon-contentright::before {
|
||||
content: "\e8c9";
|
||||
}
|
||||
.icon-contentleft::before {
|
||||
content: "\e8ca";
|
||||
}
|
||||
.icon-fangda::before {
|
||||
content: "\e826";
|
||||
}
|
||||
.icon-suoxiao::before {
|
||||
content: "\e641";
|
||||
}
|
||||
.icon-zhongyingwen::before {
|
||||
content: "\e8cb";
|
||||
}
|
||||
BIN
frontend/src/assets/iconfont/iconfont.ttf
Normal file
1
frontend/src/assets/icons/loading.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1731580516863" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1484" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M469.333333 85.333333m42.666667 0l0 0q42.666667 0 42.666667 42.666667l0 128q0 42.666667-42.666667 42.666667l0 0q-42.666667 0-42.666667-42.666667l0-128q0-42.666667 42.666667-42.666667Z" fill="#f38d0d" opacity=".8" p-id="1485"></path><path d="M469.333333 725.333333m42.666667 0l0 0q42.666667 0 42.666667 42.666667l0 128q0 42.666667-42.666667 42.666667l0 0q-42.666667 0-42.666667-42.666667l0-128q0-42.666667 42.666667-42.666667Z" fill="#f38d0d" opacity=".4" p-id="1486"></path><path d="M938.666667 469.333333m0 42.666667l0 0q0 42.666667-42.666667 42.666667l-128 0q-42.666667 0-42.666667-42.666667l0 0q0-42.666667 42.666667-42.666667l128 0q42.666667 0 42.666667 42.666667Z" fill="#f38d0d" opacity=".2" p-id="1487"></path><path d="M298.666667 469.333333m0 42.666667l0 0q0 42.666667-42.666667 42.666667l-128 0q-42.666667 0-42.666667-42.666667l0 0q0-42.666667 42.666667-42.666667l128 0q42.666667 0 42.666667 42.666667Z" fill="#f38d0d" opacity=".6" p-id="1488"></path><path d="M783.530667 180.138667m30.169889 30.169889l0 0q30.169889 30.169889 0 60.339779l-90.509668 90.509668q-30.169889 30.169889-60.339779 0l0 0q-30.169889-30.169889 0-60.339779l90.509668-90.509668q30.169889-30.169889 60.339779 0Z" fill="#f38d0d" opacity=".1" p-id="1489"></path><path d="M330.965333 632.661333m30.16989 30.16989l0 0q30.169889 30.169889 0 60.339778l-90.509668 90.509668q-30.169889 30.169889-60.339779 0l0 0q-30.169889-30.169889 0-60.339778l90.509668-90.509668q30.169889-30.169889 60.339779 0Z" fill="#f38d0d" opacity=".5" p-id="1490"></path><path d="M843.861333 783.530667m-30.169889 30.169889l0 0q-30.169889 30.169889-60.339779 0l-90.509668-90.509668q-30.169889-30.169889 0-60.339779l0 0q30.169889-30.169889 60.339779 0l90.509668 90.509668q30.169889 30.169889 0 60.339779Z" fill="#f38d0d" opacity=".3" p-id="1491"></path><path d="M391.338667 330.965333m-30.16989 30.16989l0 0q-30.169889 30.169889-60.339778 0l-90.509668-90.509668q-30.169889-30.169889 0-60.339779l0 0q30.169889-30.169889 60.339778 0l90.509668 90.509668q30.169889 30.169889 0 60.339779Z" fill="#f38d0d" opacity=".7" p-id="1492"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
1
frontend/src/assets/icons/loginout.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1724311309043" class="icon" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12103" width="24.0234375" height="24" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M1001.65696 463.424l-332.224-332.416C648.76096 110.4 624.05696 104.256 598.52096 113.792c-26.176 9.856-41.152 33.28-41.216 64.384L557.30496 328.32 492.28096 328.32 305.65696 328.384C266.48896 328.512 239.16096 355.712 239.03296 394.624c-0.128 78.4-0.128 156.736 0 235.072 0.128 37.696 27.776 65.28 65.792 65.664l252.48 0.064L557.24096 724.48c-0.128 40.768-0.256 81.408 0.576 122.112 0.256 15.936 6.528 33.28 16.256 45.184 11.904 14.656 28.224 22.72 45.952 22.72l0 0c17.536 0 35.2-8 49.728-22.464 110.848-110.72 221.568-221.504 332.288-332.352C1031.60896 530.176 1031.48096 493.248 1001.65696 463.424z" p-id="12104" fill="#ffffff"></path><path d="M387.70496 805.696 387.64096 805.696l-72.576 0.128-103.872-0.064c-52.48 0-82.624-30.144-82.624-82.688L128.56896 300.48c0-52.416 30.144-82.496 82.752-82.496l68.8-0.064 75.584 0.064 42.048-0.064c30.08 0 46.528-15.104 48.896-44.8 0.896-11.072 0.64-22.336 0.256-34.88C446.13696 106.944 428.60096 89.664 397.68896 89.6L321.33696 89.6C280.44096 89.6 239.54496 89.6 198.71296 89.856 186.16896 89.92 173.30496 91.328 160.44096 94.08 68.21696 113.728 0.88896 196.416 0.37696 290.688 0.12096 341.504 0.18496 392.384 0.24896 443.136l0.064 67.648L0.18496 581.76C0.12096 632.768-0.00704 683.712 0.44096 734.784c0.896 107.776 88.768 197.056 195.904 199.04 26.368 0.512 52.8 0.64 79.168 0.64l125.376-0.256c24.768 0 42.112-15.04 45.184-39.232 1.088-8.832 1.216-17.856 1.28-26.88l0-4.48c0.128-13.312 0.256-31.552-12.544-44.48C422.45696 806.784 404.66496 805.696 387.70496 805.696z" p-id="12105" fill="#ffffff"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
1
frontend/src/assets/icons/out_login.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1724675866949" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7939" width="24" height="24" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M336.64 186.368a42.666667 42.666667 0 0 1-12.970667 58.965333C228.693333 306.048 170.666667 408.32 170.666667 520.021333c0 181.589333 152.533333 329.386667 341.333333 329.386667s341.333333-147.797333 341.333333-329.386667c0-109.994667-56.32-210.901333-148.821333-272a42.666667 42.666667 0 1 1 47.018667-71.253333C867.584 253.44 938.666667 380.885333 938.666667 520.064c0 229.333333-191.317333 414.72-426.666667 414.72s-426.666667-185.386667-426.666667-414.72C85.333333 378.709333 158.634667 249.6 277.674667 173.44a42.666667 42.666667 0 0 1 58.922666 12.928zM512 85.333333a42.666667 42.666667 0 0 1 42.666667 42.666667v256a42.666667 42.666667 0 0 1-85.333334 0V128a42.666667 42.666667 0 0 1 42.666667-42.666667z" fill="#ffffff" p-id="7940"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
frontend/src/assets/icons/preTest.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1732514869986" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16631" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M365.631638 689.60904H222.155932c-18.512994 0-37.025989 18.512994-37.025988 37.025988s18.512994 37.025989 37.025988 37.025989h143.475706c13.884746 0 23.141243-4.628249 32.397741-18.512994s4.628249-23.141243 0-37.025989-23.141243-18.512994-32.397741-18.512994zM467.453107 527.620339c4.628249-9.256497 4.628249-23.141243 0-37.025989-4.628249-9.256497-18.512994-18.512994-32.39774-18.512994H217.527684c-13.884746 0-23.141243 4.628249-32.39774 18.512994-4.628249 9.256497-4.628249 23.141243 0 37.025989 4.628249 9.256497 18.512994 18.512994 32.39774 18.512994h217.527683c13.884746 0 27.769492-9.256497 32.39774-18.512994z" fill="#bfbfbf" p-id="16632"></path><path d="M620.185311 944.162712H171.245198c-27.769492 0-50.910734-9.256497-69.423729-27.769492-18.512994-18.512994-27.769492-41.654237-27.769492-69.423728V171.245198c0-27.769492 9.256497-50.910734 27.769492-69.423729s41.654237-27.769492 69.423729-27.769492h675.724294c27.769492 0 50.910734 9.256497 69.423728 27.769492 18.512994 18.512994 27.769492 41.654237 27.769492 69.423729v448.940113c0 18.512994 18.512994 37.025989 37.025989 37.025988s37.025989-13.884746 37.025988-37.025988V171.245198c0-46.282486-18.512994-87.936723-50.910734-120.334464C934.906215 18.512994 893.251977 0 846.969492 0H171.245198c-46.282486 0-87.936723 18.512994-120.334464 50.910734C18.512994 83.308475 0 124.962712 0 171.245198v675.724294c0 46.282486 18.512994 87.936723 50.910734 120.334463 32.39774 32.39774 74.051977 50.910734 120.334464 50.910734h448.940113c18.512994 0 37.025989-18.512994 37.025988-37.025988-4.628249-18.512994-18.512994-37.025989-37.025988-37.025989z" fill="#bfbfbf" p-id="16633"></path><path d="M837.712994 291.579661c0-18.512994-18.512994-37.025989-37.025988-37.025989H217.527684c-18.512994 0-37.025989 18.512994-37.025989 37.025989s18.512994 37.025989 37.025989 37.025989h583.159322c18.512994 0 37.025989-18.512994 37.025988-37.025989zM1008.958192 958.047458l-138.847458-138.847458c64.79548-92.564972 50.910734-222.155932-37.025988-291.579661-87.936723-74.051977-212.899435-69.423729-296.20791 13.884746-78.680226 78.680226-87.936723 208.271186-13.884746 296.207909 74.051977 87.936723 199.014689 101.821469 291.579661 37.025989l138.847458 138.847458c13.884746 13.884746 37.025989 13.884746 50.910735 0s18.512994-41.654237 4.628248-55.538983z m-212.899435-161.988701c-27.769492 27.769492-64.79548 41.654237-101.821469 41.654237-69.423729 0-129.59096-50.910734-143.475706-115.706214s23.141243-134.219209 87.936723-161.988701 138.847458-4.628249 175.873446 55.538983c37.025989 50.910734 27.769492 129.59096-18.512994 180.501695z" fill="#bfbfbf" p-id="16634"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
1
frontend/src/assets/icons/xianxingdaoyu.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M574 342.8m-38.3 0a38.3 38.3 0 1 0 76.6 0 38.3 38.3 0 1 0-76.6 0Z" fill="#F2B843" /><path d="M627 697c15.2-20.7 45.3-294-45-383.3-3-6.1-0.4-13.5 5.7-16.5 6.2-3 13.5-0.5 16.5 5.7C689.8 370.2 719.9 573 705.1 697H627z" fill="#EA800C" /><path d="M617.8 307.4m-38.3 0a38.3 38.3 0 1 0 76.6 0 38.3 38.3 0 1 0-76.6 0Z" fill="#F2B843" /><path d="M608 272.5L461 502.8c-33.6-47.5-37.2-112.5-3.9-164.6 33.2-52.1 93.7-76.2 150.9-65.7z" fill="#3DC38A" /><path d="M742.5 132.3L569.9 299.8c-19.2-47.5-9.1-103.9 29.9-141.8 39.1-37.9 95.8-46.3 142.7-25.7z" fill="#2F9B77" /><path d="M608.7 289.2l-239.6 21.1c15.1-49 58.5-86.4 112.7-91.1 54.2-4.8 103.5 24.4 126.9 70z" fill="#2F9B77" /><path d="M594.7 269.9L408.5 168.4c35-28.6 85.2-34.8 127.3-11.9 42.1 23 64 68.6 58.9 113.4z" fill="#3DC38A" /><path d="M825.5 331.8l-271.4-31.4c28-51 84.9-82.7 146.3-75.6 61.3 7 109.5 51 125.1 107z" fill="#3DC38A" /><path d="M75.3 868.9c0-86.5 104.3-173 233-173s233 86.5 233 173h-466z" fill="#2F9B77" /><path d="M938.2 868.9c0-116.2-130.9-232.3-292.3-232.3S353.5 752.7 353.5 868.9h584.7z" fill="#3DC38A" /><path d="M858.9 701.5c-28.1-23.1-60.4-41.4-95.9-54.3-14.5-5.3-29.3-9.5-44.3-12.8 0.2-51.3-5.5-106.3-16.2-155.9-11.9-54.8-29.5-101.6-51.2-136.3 5.6-5.3 9.7-11.8 12.2-19.1l160.8 18.6c0.4 0 0.8 0.1 1.2 0.1 2.9 0 5.7-1.3 7.6-3.5 2.2-2.5 2.9-6 2-9.2-8.3-29.8-25.1-56.3-48.5-76.7-24-20.9-53.5-33.9-85.1-37.5-9.7-1.1-19.3-1.3-28.9-0.7l76.8-74.6c2.4-2.3 3.5-5.7 2.9-8.9-0.6-3.3-2.8-6-5.8-7.4-52.3-23-112.6-12.1-153.7 27.7-7.2 7-13.6 14.7-19.1 23.1-9.4-10.5-20.6-19.4-33.1-26.2-44.7-24.3-99-19.2-138.4 12.9-2.6 2.1-3.9 5.4-3.6 8.7s2.2 6.3 5.2 7.9l62.5 34c-50.2 9.8-91.2 46.2-106.6 96-1 3.2-0.3 6.6 1.8 9.2 1.9 2.4 4.8 3.7 7.8 3.7h0.9l94.5-8.3c-5.8 6.4-11 13.3-15.8 20.8-17.2 26.9-25.7 57.9-24.7 89.7 1 31 11 60.8 28.9 86 1.9 2.7 4.9 4.2 8.2 4.2h0.2c3.3-0.1 6.4-1.8 8.2-4.6L549 383.9c7.5 4.6 16.1 7.1 25.2 7.1 13.4 0 25.9-5.5 34.8-14.7 27.2 70.9 29.2 175.3 21.8 250.6-34.9 1.5-69.1 8.3-101.8 20.2-35.5 12.9-67.8 31.2-95.9 54.3-3.2 2.6-6.3 5.3-9.4 8.1-35.7-15.5-75.4-23.6-115.1-23.6-63.1 0-123.8 19.9-170.9 56.1-45.8 35.3-72.1 81.5-72.1 126.9 0 5.5 4.5 10 10 10h862.9c5.5 0 10-4.5 10-10-0.3-59.7-32.8-120.7-89.6-167.4z m-226.2-370c-3.3 2.1-7 3.4-10.9 3.9-1-6.4-3.2-12.5-6.5-17.9l27.6 3.2c-2.3 4.4-5.8 8.1-10.2 10.8z m66.6-96.8c27.6 3.2 53.3 14.5 74.3 32.7 16.6 14.5 29.4 32.4 37.5 52.6l-152.7-17.7c-0.4-0.1-0.8-0.2-1.2-0.2-0.4 0-0.8-0.1-1.2-0.1l-65.3-7.6c-1-0.3-2-0.4-2.9-0.3l-5.5-0.6c-0.1 0-0.2-0.1-0.3-0.1-0.7-0.1-1.3-0.2-2-0.2l-8.8-1c5.3-7.5 11.3-14.5 18-20.8 0.5-0.4 1-0.8 1.4-1.3 8.7-8 18.4-14.9 29.1-20.5 8-4.2 16.4-7.6 24.9-10.2 0.5-0.1 0.9-0.2 1.4-0.4 17-4.8 35.1-6.4 53.3-4.3z m-92.5-69.4c31.5-30.5 76.2-41.2 117.4-29l-87 84.4c-9.3 2.9-18.4 6.6-27.1 11.2-2.2 1.2-4.3 2.4-6.5 3.6-2.8-15.7-8.5-30.8-17-44.4 5.5-9.5 12.3-18.2 20.2-25.8z m-75.8 0.1c14.4 7.9 26.4 18.6 35.7 31.9 10.5 15.1 16.8 32.7 18.4 51-1.2 1-2.5 2-3.6 3l-74-40.3c-0.8-0.7-1.8-1.2-2.8-1.5l-77.2-42.1c31.3-18.8 70.6-20 103.5-2z m-48.2 63.8c5.2-0.5 10.3-0.6 15.4-0.4l68.2 37.1c-5.1 5.7-9.9 11.8-14.2 18.2l-60 5.3-108.1 9.5c17.8-39.1 55-66 98.7-69.7zM461.2 484c-24.3-43.9-23-97.5 4.5-140.6 8.5-13.4 19-24.9 31.3-34.4l48.3-4.2s0 0.1 0.1 0.1c1.5 3 4.4 5 7.7 5.3l17.8 2.1-32.1 50.3c-0.6 0.7-1 1.4-1.4 2.2L461.2 484zM574 371c-5.2 0-10.1-1.4-14.4-3.9l29.3-45.9 1.1-1.8c7.6 5.2 12.4 13.9 12.4 23.4v2.1c-0.1 1.8-0.5 3.8-1.1 6.1-0.2 0.6-0.4 1.1-0.6 1.7-4.2 10.9-14.8 18.3-26.7 18.3z m47.8-15.5c4.3-0.3 8.6-1.3 12.6-2.7 20.5 32.6 37.2 77.3 48.6 129.9 10.2 47.1 15.7 99.1 15.8 148-15.9-2.5-31.9-3.8-48.1-4 2.7-28.8 5.6-76.5 1.8-130.4-4-57.6-14.3-104.8-30.7-140.8zM149.6 757.9c43.6-33.5 99.9-52 158.7-52 34.2 0 68.3 6.5 99.4 18.8-38.4 40-61.1 87.2-63.9 134.2h-258c3.6-35.9 26.4-72.2 63.8-101z m391.7 101H363.8c3.4-50.5 32.8-101.8 81.7-142 26.4-21.7 56.6-38.8 90-50.9 35.4-12.8 72.5-19.4 110.4-19.4 37.9 0 75 6.5 110.3 19.4 33.4 12.1 63.6 29.3 90 50.9 48.9 40.2 78.2 91.5 81.6 142H541.3z" fill="#4D3500" /></svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
1
frontend/src/assets/icons/xianxingdiqiu.svg
Normal file
|
After Width: | Height: | Size: 10 KiB |
1
frontend/src/assets/icons/xianxingditu.svg
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
1
frontend/src/assets/icons/xianxingfanchuan.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M489.6 541.1V166l283.5 375.1H489.6" fill="#3DC38A" /><path d="M489.6 101.3l-323.2 491h323.2z" fill="#F2B843" /><path d="M489.6 715.7c-16.3 0-29.6-13.2-29.6-29.6V95c0-16.3 13.2-29.6 29.6-29.6 16.3 0 29.6 13.2 29.6 29.6v591.1c-0.1 16.3-13.3 29.6-29.6 29.6z" fill="#EA800C" /><path d="M489.6 608.4H145.3c-16.3 0-29.6-13.2-29.6-29.6 0-16.3 13.2-29.6 29.6-29.6h344.3c16.3 0 29.6 13.2 29.6 29.6-0.1 16.3-13.3 29.6-29.6 29.6z" fill="#EA800C" /><path d="M783.8 557.2H503.1c-16.3 0-29.6-13.2-29.6-29.6 0-16.3 13.2-29.6 29.6-29.6h280.7c16.3 0 29.6 13.2 29.6 29.6 0 16.3-13.3 29.6-29.6 29.6z" fill="#EA800C" /><path d="M752.4 759.8l-67-88.8c-7.9-10.5-20.3-16.7-33.5-16.7H302c-18.3 0-34.5 11.9-40 29.3l-25 76.2h515.4z" fill="#2F9B77" /><path d="M920.8 704.6c15.6-0.8 26.8 14.8 21.1 29.3l-54.5 138.8-28.6 72.8H133.7L81.5 775c-4.1-13.4 5.5-27 19.4-27.8l143.3-7.5 676.6-35.1z" fill="#3DC38A" /><path d="M802.3 791.8m-27 0a27 27 0 1 0 54 0 27 27 0 1 0-54 0Z" fill="#F2B843" /><path d="M947.5 707.7c-6.3-8.7-16.4-13.6-27.2-13.1l-196.8 10.2-30-39.9c-9.8-12.9-25.3-20.6-41.5-20.6H529.2v-77.1h254.7c21.8 0 39.6-17.8 39.6-39.6S805.7 488 783.9 488h-38.3L529.2 201.7V95c0-21.8-17.8-39.6-39.6-39.6S450 73.2 450 95v48.2L189.3 539.3h-44c-21.8 0-39.6 17.8-39.6 39.6s17.8 39.6 39.6 39.6H450v25.8H302c-22.7 0-42.6 14.6-49.5 36.2l-16.2 49.6-135.9 7.1c-9.7 0.6-18.5 5.5-24.1 13.5-5.6 8-7.1 17.9-4.3 27.2l52.2 170.5c1.3 4.2 5.2 7.1 9.6 7.1h725.1c4.1 0 7.8-2.5 9.3-6.3l28.6-72.8 54.5-138.8c3.8-10 2.4-21.2-3.8-29.9zM720.5 488H529.2V234.9L720.5 488zM450 179.6v359.7H213.3L450 179.6z m20 428.8c0-5.5-4.5-10-10-10-0.5 0-0.9 0-1.3 0.1H145.3c-10.8 0-19.6-8.8-19.6-19.6s8.8-19.6 19.6-19.6H460c5.5 0 10-4.5 10-10V95c0-10.8 8.8-19.6 19.6-19.6s19.6 8.8 19.6 19.6v403c0 5.5 4.5 10 10 10h264.7c10.8 0 19.6 8.8 19.6 19.6s-8.8 19.6-19.6 19.6H519.2c-5.5 0-10 4.5-10 10v87.1H470v-35.9z m-198.5 78.3s0-0.1 0 0c4.3-13.4 16.5-22.4 30.5-22.4h350c9.9 0 19.5 4.8 25.5 12.7l21.9 29.1L257.7 729l13.8-42.3z m661.1 43.5L878.1 869 852 935.5H141.1l-50-163.4c-1-3.4-0.5-7 1.6-9.9 2-2.9 5.3-4.8 8.8-5l141.5-7.4h0.6c0.6 0 1.1-0.1 1.7-0.1l473.6-24.6h0.7l201.7-10.5c4-0.2 7.6 1.5 9.9 4.8 2.3 3.2 2.8 7.2 1.4 10.8z" fill="#4D3500" /><path d="M802.3 754.8c-20.4 0-37 16.6-37 37s16.6 37 37 37 37-16.6 37-37-16.6-37-37-37z m0 54c-9.4 0-17-7.6-17-17s7.6-17 17-17 17 7.6 17 17-7.6 17-17 17z" fill="#4D3500" /></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
1
frontend/src/assets/icons/xianxingfeiji.svg
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
1
frontend/src/assets/icons/xianxinglvhangriji.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M252.8 862.9h-73.4c-6.8 0-12.4-5.6-12.4-12.4V134.4c0-6.8 5.6-12.4 12.4-12.4h73.4l5.3 6.8v728.8l-5.3 5.3z" fill="#F2B843" /><path d="M338.5 942l-42.9-18.1-42.8 18.1v-79.1l7.5-5.3h71.3l7 5.3-0.1 79.1z" fill="#EA800C" /><path d="M844.6 327.9h-40.7l-5.3-5.8v-93.3l5.3-7.2h40.7c6.8 0 12.4 5.6 12.4 12.4v81.5c0 6.9-5.6 12.4-12.4 12.4z" fill="#F2B843" /><path d="M844.6 540.5h-40.7l-5.3-7.3v-90.6l5.3-8.4h40.7c6.8 0 12.4 5.6 12.4 12.4v81.5c0 6.8-5.6 12.4-12.4 12.4z" fill="#EA800C" /><path d="M844.6 753h-40.7l-5.3-6.7v-92.1l5.3-7.5h40.7c6.8 0 12.4 5.6 12.4 12.4v81.5c0 6.8-5.6 12.4-12.4 12.4z" fill="#2F9B77" /><path d="M791.8 862.9h-539V122h539.1c6.7 0 12 5.4 12 12v716.8c0 6.7-5.4 12.1-12.1 12.1z" fill="#3DC38A" /><path d="M680.3 661.1H343.7c-14 0-25.5-11.5-25.5-25.5s11.5-25.5 25.5-25.5h336.6c14 0 25.5 11.5 25.5 25.5s-11.5 25.5-25.5 25.5zM680.3 779.8H343.7c-14 0-25.5-11.5-25.5-25.5s11.5-25.5 25.5-25.5h336.6c14 0 25.5 11.5 25.5 25.5s-11.5 25.5-25.5 25.5z" fill="#2F9B77" /><path d="M594.8 511.1c-79.2 45.7-180.5 18.6-226.3-60.6S350 270 429.2 224.2s180.5-18.6 226.3 60.6 18.5 180.6-60.7 226.3z" fill="#3DC38A" /><path d="M523.7 318.1c-1.2 0.3-3.5 0.9-4.5 1.7-1.2 1 0.7 2.3 0 2.9-1.2 1-2.1 2.7-4.7 2.6-5.5-0.3-13.9-7.5-19-10.2 8.1-8.3-4.7-9.6-9.7-9.1-6.5 0.5-17.7 0-23.5 3.2-4.1 2.3-9.5 11.7-12.8 15.5-4.6 5.2-9.1 9.8-12.1 16-10.9 23 5.9 49.4 32.2 46.3 7.3-0.9 14.9-5.5 21.4 0.6 4.8 4.5 2.3 8.7 3.5 13.9 1.1 4.5 6.1 7.3 7.8 11.4 3.3 7.9 1.2 12.9-1.1 20.2-3.1 9.6 4.9 21 8 30.2 1.6 4.7 6.1 17.7 10.7 19.8 7.1 3.2 18.1-6.4 22.4-11.6 3.3-4.1 2.2-8.2 4.4-12 2.4-4.1 5.4-5.3 7.1-10.6 1.8-5.7 3.7-7.1 7.5-11.3 6.2-6.7 5.2-10.6 2.7-18.6-5.1-16.5 13.5-24.2 21.8-36.3 8.7-12.6-8.2-8.4-14.8-12.8-6.8-4.4-9.8-12.9-13.1-19.9s-6-17.5-11.4-23.3c-4.2-4.3-16.9-6.4-22.8-8.6zM609.2 428.8c-2.6 8.9-5.3 17.8-7.9 26.7-1.8 6.2-8.4 26.6-17.5 13.6-3.5-5-0.6-11.4 1.3-16 2.4-5.8-0.9-8.7 0.9-14.1 2-6.2 10-6.4 13.8-10.8 2.7-3 4.3-7.9 6.2-11.5 1 4 2.1 8.1 3.2 12.1z" fill="#F2B843" /><path d="M655.4 284.9c-28.5-49.4-78.7-78.5-131.6-82.4l-21.6 27.2-46.4 16.3-12.5 30.2 19.2 26.9 2-5.7c3.4-9.5 12.4-15.8 22.5-15.8h31.7l36.4 12.8 12.5 12v7.6l17.4 33.4 5.1-1.9c11-4 20.9-10.4 29.2-18.7l-4.2-6.3c-1.4-2 0.1-4.7 2.5-4.7h10.2c3 0 5.8 1.3 7.7 3.6l8.3 9.7c0.8 0.9 1.4 2 1.8 3.1l9.1 25.2 4.4-4.4c2.7-2.7 4.1-6.5 4.2-10.4 0-3.1 1.4-6.1 3.7-8.2l3.4-3c0.8-0.8 1.8-1.4 2.8-1.8-3.6-15.3-9.5-30.4-17.8-44.7zM407.6 291.3l7.9-8.5c5.8-6.2 7.4-15.2 4.2-23-3.4-8.3-10.9-13.6-19.2-14.8-29.2 26.5-47.4 62.2-52.6 100 6.2 5.4 12.6 11.2 12.6 11.7-0.1 1 23.2-2.9 23.2-2.9l-12-17.5 17.2-12.3 18.7-32.7zM423.8 456.4c7.5-2.4 11.6-10.4 9.1-17.9l-2.1-6.6c-0.9-2.9-0.9-5.9 0-8.8 2.7-8.1-2.4-16.8-10.8-18.4l-16.8-3.3-30.6-23.2-25.3 8.2c2.5 21.9 9.4 43.7 21.2 64.1 10.9 18.8 24.9 34.7 41 47.4l7.5-39.3 6.8-2.2z" fill="#2F9B77" /><path d="M844.6 337.9c12.4 0 22.4-10 22.4-22.4V234c0-12.4-10-22.4-22.4-22.4h-30.7V134c0-12.1-9.9-22-22-22H179.4c-12.4 0-22.4 10-22.4 22.4v716.1c0 12.4 10 22.4 22.4 22.4h63.4V942c0 3.4 1.7 6.5 4.5 8.3 2.8 1.9 6.3 2.2 9.4 0.9l38.9-16.5 39 16.5c1.2 0.5 2.6 0.8 3.9 0.8 1.9 0 3.9-0.6 5.5-1.7 2.8-1.9 4.5-5 4.5-8.3v-69.1h443.3c12.2 0 22.1-9.9 22.1-22.1V763h30.7c12.4 0 22.4-10 22.4-22.4v-81.5c0-12.4-10-22.4-22.4-22.4h-30.7v-86.2h30.7c12.4 0 22.4-10 22.4-22.4v-81.5c0-12.4-10-22.4-22.4-22.4h-30.7v-86.3h30.7z m0-106.3c1.3 0 2.4 1.1 2.4 2.4v81.5c0 1.3-1.1 2.4-2.4 2.4h-30.7v-86.3h30.7zM177 850.5V134.4c0-1.3 1.1-2.4 2.4-2.4h63.4v720.9h-63.4c-1.3 0-2.4-1.1-2.4-2.4z m151.5 76.4l-29-12.2c-2.5-1-5.3-1-7.8 0l-28.9 12.2v-54h65.7v54z m465.4-76.1c0 1.2-0.9 2.1-2.1 2.1h-529V132h529.1c1.1 0 2 0.9 2 2v716.8z m50.7-194.1c1.3 0 2.4 1.1 2.4 2.4v81.5c0 1.4-1 2.4-2.4 2.4h-30.7v-86.3h30.7z m0-212.5c1.3 0 2.4 1.1 2.4 2.4v81.5c0 1.3-1.1 2.4-2.4 2.4h-30.7v-86.3h30.7z" fill="#4D3500" /><path d="M680.3 600.1H343.7c-19.6 0-35.5 15.9-35.5 35.5s15.9 35.5 35.5 35.5h336.6c19.6 0 35.5-15.9 35.5-35.5s-15.9-35.5-35.5-35.5z m0 51H343.7c-8.5 0-15.5-7-15.5-15.5s7-15.5 15.5-15.5h336.6c8.5 0 15.5 7 15.5 15.5s-7 15.5-15.5 15.5zM680.3 718.8H343.7c-19.6 0-35.5 15.9-35.5 35.5s15.9 35.5 35.5 35.5h336.6c19.6 0 35.5-15.9 35.5-35.5s-15.9-35.5-35.5-35.5z m0 51H343.7c-8.5 0-15.5-7-15.5-15.5s7-15.5 15.5-15.5h336.6c8.5 0 15.5 7 15.5 15.5s-7 15.5-15.5 15.5zM512.3 543.2c29.8 0 59.9-7.6 87.5-23.5 40.6-23.4 69.7-61.3 81.9-106.7 12.2-45.4 6-92.7-17.5-133.3-23.5-40.6-61.4-69.7-106.7-81.8-45.3-12.1-92.7-5.9-133.3 17.6-40.6 23.5-69.7 61.4-81.9 106.7-12.2 45.3-6 92.7 17.5 133.3 32.6 56.3 91.7 87.7 152.5 87.7zM361.6 327.4c10.8-40.2 36.6-73.7 72.6-94.6 24-13.9 50.6-20.9 77.6-20.9 13.5 0 27.1 1.8 40.5 5.4 40.2 10.8 73.7 36.5 94.6 72.5 20.8 36 26.3 77.9 15.5 118.1-10.8 40.2-36.6 73.7-72.6 94.5-74.3 42.9-169.7 17.3-212.6-56.9-20.8-36-26.4-77.9-15.6-118.1z" fill="#4D3500" /></svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
1
frontend/src/assets/icons/xianxingtianqiyubao.svg
Normal file
|
After Width: | Height: | Size: 13 KiB |
1
frontend/src/assets/icons/xianxingxiangjipaizhao.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M72 440.5h880v286.2H72z" fill="#F2B843" /><path d="M72 726.6V834c0 19.8 16 35.8 35.8 35.8h808.5c19.8 0 35.8-16 35.8-35.8V726.6H72zM916.2 297.4H708.7L647 174.1c-6.1-12.1-18.5-19.8-32-19.8H408.9c-13.5 0-25.9 7.7-32 19.8l-61.7 123.3h-64.4v-35.8c0-19.8-16-35.8-35.8-35.8h-35.8c-19.8 0-35.8 16-35.8 35.8v35.8h-35.8c-19.8 0-35.8 16-35.8 35.8v107.3h880V333.1c0.2-19.7-15.8-35.7-35.6-35.7z" fill="#3DC38A" /><path d="M726.6 583.5c0 118.3-95.9 214.6-214.6 214.6-118.8 0-214.6-96.4-214.6-214.6 0-118.3 95.9-214.6 214.6-214.6 118.8 0 214.6 96.4 214.6 214.6z" fill="#EA800C" /><path d="M512 440.5c78.9 0 143.1 64.2 143.1 143.1S590.9 726.7 512 726.7s-143.1-64.2-143.1-143.1S433.1 440.5 512 440.5z" fill="#FFFFFF" /><path d="M773.1 386.8c9.9 0 17.9-8 17.9-17.9s-8-17.9-17.9-17.9-17.9 8-17.9 17.9c0.1 9.9 8.1 17.9 17.9 17.9zM565.7 207.9H458.3c-9.9 0-17.9 8-17.9 17.9s8 17.9 17.9 17.9h107.3c9.9 0 17.9-8 17.9-17.9s-8-17.9-17.8-17.9zM512 744.5c88.8 0 161-72.2 161-161s-72.2-161-161-161-161 72.2-161 161 72.2 161 161 161z m0-286.2c69 0 125.2 56.2 125.2 125.2S581 708.7 512 708.7s-125.2-56.2-125.2-125.2S443 458.3 512 458.3z" fill="#2F9B77" /><path d="M440.5 601.4c9.9 0 17.9-8 17.9-17.9 0-29.6 24.1-53.7 53.7-53.7 9.9 0 17.9-8 17.9-17.9s-8-17.9-17.9-17.9c-49.3 0-89.4 40.1-89.4 89.4-0.1 10 7.9 18 17.8 18z" fill="#3DC38A" /><path d="M844.7 386.8h35.8c9.9 0 17.9-8 17.9-17.9s-8-17.9-17.9-17.9h-35.8c-9.9 0-17.9 8-17.9 17.9s8 17.9 17.9 17.9z" fill="#2F9B77" /><path d="M773.1 396.8c15.4 0 27.9-12.5 27.9-27.9S788.5 341 773.1 341s-27.9 12.5-27.9 27.9v0.1c0.2 15.3 12.7 27.8 27.9 27.8z m0-35.8c4.4 0 7.9 3.5 7.9 7.9s-3.5 7.9-7.9 7.9c-4.3 0-7.8-3.6-7.9-8 0-4.3 3.6-7.8 7.9-7.8zM458.3 253.7h107.3c15.4 0 27.9-12.5 27.9-27.9s-12.5-27.9-27.8-27.9H458.3c-15.4 0-27.9 12.5-27.9 27.9s12.5 27.9 27.9 27.9z m0-35.8h107.4c4.3 0 7.8 3.5 7.8 7.9s-3.5 7.9-7.9 7.9H458.3c-4.4 0-7.9-3.5-7.9-7.9s3.5-7.9 7.9-7.9zM512 754.5c94.3 0 171-76.7 171-171s-76.7-171-171-171-171 76.7-171 171 76.7 171 171 171z m0-322c83.3 0 151 67.7 151 151s-67.7 151-151 151-151-67.7-151-151 67.7-151 151-151z" fill="#4D3500" /><path d="M512 718.7c74.5 0 135.2-60.7 135.2-135.2S586.5 448.3 512 448.3 376.8 509 376.8 583.5 437.5 718.7 512 718.7z m0-250.4c63.5 0 115.2 51.7 115.2 115.2S575.5 698.7 512 698.7 396.8 647 396.8 583.5 448.5 468.3 512 468.3z" fill="#4D3500" /><path d="M468.4 583.5c0-24.1 19.6-43.7 43.7-43.7 15.4 0 27.9-12.5 27.9-27.9S527.5 484 512.1 484c-54.8 0-99.4 44.6-99.4 99.4-0.1 7.5 2.8 14.5 8 19.8 5.3 5.3 12.3 8.2 19.8 8.2 15.4 0 27.9-12.5 27.9-27.9z m-35.7 0s0-0.1 0 0c0-43.9 35.6-79.5 79.4-79.5 4.4 0 7.9 3.5 7.9 7.9s-3.5 7.9-7.9 7.9c-35.1 0-63.7 28.6-63.7 63.7 0 4.4-3.5 7.9-7.9 7.9-2.1 0-4.1-0.8-5.6-2.3-1.4-1.5-2.2-3.5-2.2-5.6zM844.7 396.8h35.8c15.4 0 27.9-12.5 27.9-27.9S895.9 341 880.5 341h-35.8c-15.4 0-27.9 12.5-27.9 27.9s12.5 27.9 27.9 27.9z m0-35.8h35.8c4.4 0 7.9 3.5 7.9 7.9s-3.5 7.9-7.9 7.9h-35.8c-4.4 0-7.9-3.5-7.9-7.9s3.5-7.9 7.9-7.9z" fill="#4D3500" /><path d="M916.5 287.3H715.2l-58.9-117.8c-7.8-15.6-23.5-25.3-40.9-25.3H409.1c-17.4 0-33.1 9.7-40.9 25.3l-58.9 117.8H261v-25.8c0-25.3-20.5-45.8-45.8-45.8h-35.8c-25.3 0-45.8 20.5-45.8 45.8v25.8h-25.8c-25.3 0-45.8 20.5-45.8 45.8V834c0 25.1 20.5 45.7 45.8 45.8h808.4c25.3 0 45.8-20.5 45.8-45.8V442.8c0.2-0.8 0.3-1.6 0.3-2.4V333.1c0-25.3-20.5-45.8-45.8-45.8z m-737.1-51.6h35.8c14.2 0 25.8 11.6 25.8 25.8v25.9h-87.4v-25.9c0-14.2 11.6-25.8 25.8-25.8zM82 450.5h249.1c-27.5 37.3-43.7 83.3-43.7 133 0 49.8 16.3 95.8 43.8 133.1H82V450.5z m430-71.6c112.8 0 204.6 91.8 204.6 204.6S624.8 788.1 512 788.1s-204.6-91.8-204.6-204.6S399.2 378.9 512 378.9zM942 834c0 14.2-11.6 25.8-25.8 25.8H107.9C93.6 859.7 82 848.2 82 834v-97.4h265.8c41 44 99.4 71.5 164.2 71.5s123.1-27.5 164.2-71.5H942V834z m0-117.4H692.8c27.5-37.3 43.8-83.3 43.8-133.1 0-49.7-16.3-95.7-43.7-133H942v266.1z m0.3-286.2H676.2c-41-44-99.4-71.5-164.2-71.5s-123.2 27.6-164.3 71.6H82v-97.4c0-14.2 11.6-25.8 25.8-25.8h34.4c0.4 0.1 0.9 0.1 1.3 0.1h108c0.5 0 0.9 0 1.3-0.1h62.6c3.8 0 7.2-2.1 8.9-5.5L386 178.5c4.4-8.8 13.3-14.3 23.1-14.3h206.2c9.8 0 18.7 5.5 23.1 14.3l61.7 123.3c1.7 3.4 5.2 5.5 8.9 5.5h207.5c14.2 0 25.8 11.6 25.8 25.8v97.3z" fill="#4D3500" /></svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
1
frontend/src/assets/icons/xianxingxiarilengyin.svg
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
1
frontend/src/assets/icons/xianxingyoulun.svg
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
1
frontend/src/assets/icons/xianxingzijiayou.svg
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
1
frontend/src/assets/icons/守时检测.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1732514910835" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="17830" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M960.24 688.95c-18.01-3.5-35.36 8.28-38.83 26.25-20.79 107.56-103.26 189.89-205.21 204.89-18.11 2.66-30.63 19.5-27.97 37.61 2.42 16.47 16.57 28.32 32.74 28.32 1.61 0 3.24-0.11 4.87-0.36 129.77-19.09 234.52-122.71 260.64-257.88 3.48-17.97-8.26-35.35-26.24-38.83z" fill="#666666" p-id="17831"></path><path d="M719.89 875.33c5.11 0 10.31-1.2 15.17-3.7 64.25-33.18 116-89.16 145.74-157.64 7.29-16.79-0.41-36.31-17.21-43.6-16.78-7.27-36.29 0.43-43.6 17.21-23.66 54.5-64.63 98.94-115.33 125.12-16.27 8.4-22.65 28.4-14.25 44.66 5.89 11.41 17.48 17.95 29.48 17.95zM312.62 540.45c-21.23-21.19-55.78-21.21-76.99 0.02L53.07 723.03c-10.29 10.29-15.97 23.96-15.97 38.52 0 14.54 5.66 28.22 15.96 38.51l104.58 104.58c10.61 10.62 24.55 15.91 38.5 15.91 13.94 0 27.88-5.3 38.51-15.91l182.57-182.57c21.23-21.23 21.23-55.77 0-77l-104.6-104.62zM196.13 849.39l-87.83-87.83 165.84-165.84 87.83 87.83-165.84 165.84zM643.92 418.3c10.29 10.29 23.97 15.97 38.53 15.97 14.54 0 28.22-5.68 38.5-15.96l182.57-182.56 0.02-0.01c21.2-21.23 21.2-55.78-0.02-76.99L798.93 54.16c-21.22-21.22-55.76-21.24-77 0L539.34 236.74c-21.2 21.23-21.2 55.78 0.02 76.99L643.92 418.3z m116.51-308.9l87.84 87.84-165.83 165.83-87.84-87.83L760.43 109.4zM596.09 714.69c-20.79-20.8-18.77-56.66 4.51-79.95l28.56-28.56c11.6-11.59 26.54-18.28 42.09-18.83 14.49-0.9 28.11 4.55 37.85 14.32 0.01 0.01 0.02 0.01 0.03 0.02l10.18 10.18c12.95 12.95 33.92 12.95 46.87 0 12.95-12.94 12.95-33.93 0-46.87L756 554.84c-0.01-0.01-0.01-0.02-0.02-0.03-0.02-0.02-0.04-0.03-0.06-0.05L623.06 421.89c-0.01-0.01-0.01-0.02-0.02-0.03-0.02-0.02-0.04-0.03-0.06-0.05l-85.92-85.92c-43.22-43.22-113.54-43.22-156.76 0l-49.99 49.99c-43.22 43.22-43.22 113.54 0 156.76l229.12 229.12a33.037 33.037 0 0 0 23.43 9.71c8.48 0 16.96-3.24 23.43-9.71 12.95-12.94 12.95-33.93 0-46.87l-10.2-10.2z m-13.8-155.39l-28.57 28.56c-15.36 15.36-25.55 33.65-31.65 52.8l-58.92-58.92c-20.79-20.8-18.77-56.66 4.51-79.95l28.56-28.56c11.6-11.59 26.54-18.28 42.09-18.83 14.33-0.82 28.09 4.55 37.85 14.32 0.01 0.01 0.02 0.01 0.03 0.02l58.83 58.83c-19.51 6.26-37.69 16.7-52.73 31.73z m-218.16-95.04c0-11.9 4.64-23.09 13.05-31.51l49.99-49.99c16.85-16.83 46.2-16.83 63.03 0l11.88 11.88c-19.51 6.25-37.69 16.69-52.72 31.73l-28.56 28.56c-15.36 15.36-25.56 33.64-31.66 52.79l-11.95-11.95c-8.43-8.41-13.06-19.61-13.06-31.51z" fill="#666666" p-id="17832"></path><path d="M723.12 632.91c-12.94-12.94-33.93-12.94-46.87 0l-49.11 49.11c-12.94 12.94-12.94 33.93 0 46.87 12.94 12.94 33.93 12.94 46.87 0l49.11-49.11c12.94-12.94 12.94-33.92 0-46.87z" fill="#666666" p-id="17833"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
1
frontend/src/assets/icons/完成.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1732514403639" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13633" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M387.018497 210.626092c17.179264 0 31.099299-13.925151 31.099299-31.099299l0-10.366092 186.597839 0 0 10.366092c0 17.175171 13.920034 31.099299 31.099299 31.099299s31.099299-13.925151 31.099299-31.099299L666.914232 96.593965c0-17.175171-13.920034-31.099299-31.099299-31.099299s-31.099299 13.925151-31.099299 31.099299l0 10.366092L418.117796 106.960057l0-10.366092c0-17.175171-13.920034-31.099299-31.099299-31.099299s-31.099299 13.925151-31.099299 31.099299l0 82.932828C355.919199 196.700941 369.83821 210.626092 387.018497 210.626092z" fill="#999999" p-id="13634"></path><path d="M400.416646 314.944995c-17.179264 0-31.099299 13.925151-31.099299 31.099299l0 30.340005c0 17.175171 13.920034 31.099299 31.099299 31.099299s31.099299-13.925151 31.099299-31.099299L431.515944 346.044294C431.516967 328.869123 417.596933 314.944995 400.416646 314.944995z" fill="#999999" p-id="13635"></path><path d="M622.416785 314.944995c-17.179264 0-31.099299 13.925151-31.099299 31.099299l0 30.340005c0 17.175171 13.920034 31.099299 31.099299 31.099299s31.099299-13.925151 31.099299-31.099299L653.516084 346.044294C653.517107 328.869123 639.597073 314.944995 622.416785 314.944995z" fill="#999999" p-id="13636"></path><path d="M511.103584 546.380018c43.773969 0 74.783217-30.18344 86.212524-48.016597 9.182127-14.33038 5.021364-33.210379-9.202593-42.569538-14.213723-9.379625-33.428344-5.385661-42.974767 8.706289-0.132006 0.197498-13.555737 19.680225-34.035164 19.680225-19.903306 0-32.29452-18.039865-33.124422-19.290345-9.151427-14.349823-28.173666-18.72855-42.650379-9.718339-14.588253 9.06547-19.072381 28.245298-10.001795 42.832528C436.493306 515.968381 467.116767 546.380018 511.103584 546.380018z" fill="#999999" p-id="13637"></path><path d="M463.633433 898.665457 266.173199 898.665457c-41.648562 0-75.532277-34.713616-75.532277-77.374228l0-574.913888c0-42.665729 33.883715-77.374228 75.532277-77.374228 17.179264 0 31.099299-13.925151 31.099299-31.099299s-13.920034-31.099299-31.099299-31.099299c-75.946716 0-137.730874 62.61406-137.730874 139.573849l0 574.913888c0 76.959789 61.784158 139.573849 137.730874 139.573849L463.633433 960.866101c17.179264 0 31.099299-13.920034 31.099299-31.099299S480.81372 898.665457 463.633433 898.665457z" fill="#999999" p-id="13638"></path><path d="M756.661256 106.803491c-17.179264 0-31.099299 13.925151-31.099299 31.099299s13.920034 31.099299 31.099299 31.099299c41.648562 0 75.532277 34.7085 75.532277 77.374228L832.193533 551.401382c0 17.175171 13.920034 31.099299 31.099299 31.099299s31.099299-13.925151 31.099299-31.099299L894.39213 246.37734C894.39213 169.417551 832.607972 106.803491 756.661256 106.803491z" fill="#999999" p-id="13639"></path><path d="M840.980663 650.76953 613.392248 885.125069l-110.711498-114.006544c-11.975752-12.325723-31.655977-12.614296-43.976584-0.647753-12.320607 11.966543-12.614296 31.655977-0.647753 43.976584l133.023666 136.981814c0.10847 0.110517 0.23843 0.183172 0.347924 0.291642 0.11154 0.11154 0.186242 0.245593 0.299829 0.356111 0.771573 0.74906 1.644453 1.311879 2.470261 1.963725 0.704035 0.556679 1.363044 1.183965 2.101871 1.671059 1.031493 0.680499 2.131547 1.187035 3.219321 1.731434 0.730641 0.366344 1.426489 0.816598 2.178619 1.121544 1.25048 0.50756 2.548032 0.823761 3.841491 1.162476 0.642636 0.167822 1.26276 0.429789 1.912559 0.555655 1.961678 0.38067 3.949962 0.581238 5.939269 0.581238s3.977591-0.200568 5.939269-0.581238c0.650823-0.12689 1.269923-0.387833 1.912559-0.555655 1.293459-0.338714 2.591011-0.654916 3.841491-1.162476 0.75213-0.304945 1.447978-0.7552 2.178619-1.121544 1.087774-0.544399 2.187829-1.050935 3.219321-1.731434 0.738827-0.487094 1.397836-1.11438 2.101871-1.671059 0.825808-0.651846 1.698688-1.215688 2.470261-1.963725 0.113587-0.110517 0.188288-0.24457 0.299829-0.356111 0.109494-0.10847 0.239454-0.181125 0.347924-0.291642l249.900583-257.331833c11.966543-12.320607 11.672854-32.011064-0.647753-43.976584C872.637664 638.160351 852.956416 638.438691 840.980663 650.76953z" fill="#999999" p-id="13640"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
1
frontend/src/assets/icons/正式检测.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1732515039802" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="21450" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 1024C230.4 1024 0 793.6 0 512S230.4 0 512 0s512 230.4 512 512-230.4 512-512 512z m135.68-320H376.32c-12.8 0-20.48 17.92-20.48 17.92v20.48h314.88v-17.92c0-10.24-10.24-20.48-23.04-20.48zM709.12 281.6H314.88C281.6 281.6 256 307.2 256 335.36v258.56c0 28.16 25.6 53.76 58.88 53.76h135.68v17.92c0 7.68 7.68 17.92 20.48 17.92h76.8c12.8 0 20.48-7.68 20.48-17.92v-17.92h135.68c33.28 0 58.88-25.6 58.88-53.76V335.36C768 307.2 742.4 281.6 709.12 281.6zM314.88 576c-12.8 0-20.48-7.68-20.48-17.92v-222.72c0-7.68 7.68-17.92 20.48-17.92h391.68c12.8 0 20.48 7.68 20.48 17.92v222.72c0 7.68-7.68 17.92-20.48 17.92H314.88z m394.24 56.32c-7.68 0-12.8-5.12-17.92-7.68s-5.12-12.8 0-20.48c5.12-5.12 7.68-7.68 17.92-7.68 7.68 0 12.8 5.12 17.92 7.68 5.12 5.12 5.12 12.8 0 20.48-5.12 5.12-10.24 7.68-17.92 7.68z m-51.2-266.24L588.8 424.96 542.72 384c0-5.12-7.68-5.12-12.8-5.12s-12.8 5.12-12.8 7.68l-58.88 97.28-25.6-53.76c0-7.68-7.68-12.8-12.8-12.8-7.68 0-12.8 0-17.92 5.12l-58.88 53.76c-7.68 7.68-7.68 20.48 0 25.6 7.68 7.68 20.48 7.68 25.6 0l38.4-33.28 30.72 66.56c5.12 7.68 7.68 12.8 17.92 12.8 7.68 0 12.8-5.12 17.92-12.8l64-110.08 43.52 38.4c7.68 7.68 20.48 7.68 30.72 0l76.8-71.68c5.12-5.12 7.68-12.8 5.12-17.92 0-7.68-7.68-12.8-12.8-12.8-15.36-5.12-20.48 0-23.04 5.12z" p-id="21451" fill="#bfbfbf"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1
frontend/src/assets/icons/系数校准.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1732515166779" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="27890" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M418.95 374.32c-38.78 0-63.02 41.98-43.63 75.57l154.61 267.79c12.5-3.1 23.99-10.99 31.32-23.68l184.57-319.68H418.95z" fill="#cdcdcd" p-id="27891"></path><path d="M803.55 299.32H348.43c-13.81 0-25-11.19-25-25s11.19-25 25-25h455.13c13.81 0 25 11.19 25 25s-11.2 25-25.01 25z" fill="#cdcdcd" p-id="27892"></path><path d="M231.68 274.32m-57.35 0a57.35 57.35 0 1 0 114.7 0 57.35 57.35 0 1 0-114.7 0Z" fill="#cdcdcd" p-id="27893"></path><path d="M231.68 332.17c-31.9 0-57.85-25.95-57.85-57.85s25.95-57.85 57.85-57.85c31.9 0 57.85 25.95 57.85 57.85s-25.95 57.85-57.85 57.85z m0-114.7c-31.35 0-56.85 25.5-56.85 56.85s25.5 56.85 56.85 56.85 56.85-25.5 56.85-56.85-25.5-56.85-56.85-56.85z" fill="#cdcdcd" p-id="27894"></path><path d="M196.5 410.67c-2.49 0-5.03-0.38-7.53-1.17-28.53-9.01-53.09-26.5-71.03-50.6-18.32-24.6-28-53.85-28-84.59 0-78.16 63.59-141.74 141.74-141.74s141.74 63.59 141.74 141.74c0 13.81-11.19 25-25 25s-25-11.19-25-25c0-50.59-41.16-91.74-91.74-91.74s-91.74 41.16-91.74 91.74c0 40.24 25.75 75.41 64.08 87.51 13.17 4.16 20.47 18.2 16.31 31.37-3.36 10.67-13.21 17.48-23.83 17.48z" fill="#cdcdcd" p-id="27895"></path><path d="M459.27 690.78c-8.64 0-17.04-4.48-21.67-12.5L210.03 284.12c-6.9-11.96-2.81-27.25 9.15-34.15 11.96-6.9 27.25-2.81 34.15 9.15L480.9 653.27c6.9 11.96 2.81 27.25-9.15 34.15a24.846 24.846 0 0 1-12.48 3.36z" fill="#cdcdcd" p-id="27896"></path><path d="M517.62 766.88m-57.35 0a57.35 57.35 0 1 0 114.7 0 57.35 57.35 0 1 0-114.7 0Z" fill="#cdcdcd" p-id="27897"></path><path d="M517.71 824.75c-5.03 0-10.08-0.66-15.07-2-14.93-4-27.4-13.57-35.13-26.96-7.73-13.38-9.78-28.97-5.78-43.9 4-14.93 13.57-27.4 26.96-35.13 27.63-15.95 63.08-6.45 79.03 21.18 15.95 27.63 6.45 63.08-21.17 79.03-8.92 5.15-18.81 7.78-28.84 7.78z m-0.19-114.71c-9.64 0-19.4 2.45-28.33 7.61-13.15 7.59-22.56 19.85-26.49 34.52-3.93 14.67-1.91 29.99 5.68 43.14s19.85 22.56 34.52 26.49c14.67 3.93 29.99 1.91 43.14-5.68 27.15-15.67 36.48-50.51 20.81-77.66-10.52-18.23-29.67-28.42-49.33-28.42z" fill="#cdcdcd" p-id="27898"></path><path d="M517.85 908.59c-49.03 0-96.77-25.42-122.99-70.84-39.08-67.69-15.8-154.55 51.88-193.63 11.96-6.9 27.25-2.81 34.15 9.15s2.81 27.25-9.15 34.15c-43.81 25.29-58.87 81.51-33.58 125.32 16.97 29.4 47.88 45.85 79.61 45.85 15.56 0 31.31-3.95 45.72-12.27 34.85-20.12 52.43-60 43.74-99.25-2.98-13.48 5.53-26.83 19.01-29.81 13.48-2.98 26.83 5.53 29.81 19.01 6.46 29.21 3.59 59.23-8.3 86.82-12.14 28.16-32.63 51.17-59.26 66.54-22.26 12.85-46.61 18.96-70.64 18.96z" fill="#cdcdcd" p-id="27899"></path><path d="M517.59 791.88c-4.24 0-8.54-1.08-12.48-3.35-11.96-6.9-16.05-22.19-9.15-34.15l227.56-394.15c6.9-11.96 22.19-16.05 34.15-9.15 11.96 6.9 16.05 22.19 9.15 34.15L539.27 779.38c-4.63 8.02-13.04 12.5-21.68 12.5z" fill="#cdcdcd" p-id="27900"></path><path d="M803.55 271.62m-57.35 0a57.35 57.35 0 1 0 114.7 0 57.35 57.35 0 1 0-114.7 0Z" fill="#cdcdcd" p-id="27901"></path><path d="M803.45 329.46a57.47 57.47 0 0 1-28.83-7.74c-27.63-15.95-37.12-51.4-21.17-79.03 15.95-27.62 51.4-37.13 79.03-21.18 13.38 7.73 22.96 20.2 26.96 35.13s1.95 30.52-5.78 43.9c-10.71 18.55-30.2 28.92-50.21 28.92z m0.2-114.68c-19.66 0-38.81 10.2-49.33 28.42-15.67 27.15-6.34 61.99 20.81 77.66 27.15 15.67 61.99 6.34 77.66-20.81 7.59-13.15 9.61-28.47 5.68-43.14s-13.34-26.93-26.49-34.52a56.46 56.46 0 0 0-28.33-7.61z" fill="#cdcdcd" p-id="27902"></path><path d="M803.31 413.34c-24.03 0-48.37-6.11-70.63-18.96-11.96-6.9-16.05-22.19-9.15-34.15 6.9-11.96 22.19-16.05 34.15-9.15 43.81 25.29 100.03 10.23 125.32-33.58 25.29-43.81 10.23-100.03-33.58-125.32-34.85-20.12-78.18-15.4-107.82 11.74-10.18 9.32-26 8.63-35.32-1.56-9.32-10.18-8.63-26 1.56-35.32 22.06-20.2 49.5-32.72 79.34-36.22 30.46-3.56 60.63 2.68 87.25 18.05 67.69 39.08 90.96 125.94 51.88 193.62-26.23 45.42-73.97 70.85-123 70.85z" fill="#cdcdcd" p-id="27903"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
BIN
frontend/src/assets/images/403.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
frontend/src/assets/images/404.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
frontend/src/assets/images/500.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
frontend/src/assets/images/cn_tool_logo.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
33
frontend/src/assets/images/login_bg.svg
Normal file
@@ -0,0 +1,33 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" baseProfile="full" width="100%" height="100%" viewBox="0 0 1400 800">
|
||||
|
||||
<rect x="1300" y="400" rx="40" ry="40" width="150" height="150" stroke="rgb(129, 201, 149)" fill="rgb(129, 201, 149)">
|
||||
<animateTransform attributeType="XML" attributeName="transform" begin="0s" dur="35s" type="rotate" from="0 1450 550" to="360 1450 550" repeatCount="indefinite"/>
|
||||
</rect>
|
||||
|
||||
<path d="M 100 350 A 150 150 0 1 1 400 350 Q400 370 380 370 L 250 370 L 120 370 Q100 370 100 350" fill="#a2b3ff">
|
||||
<animateMotion path="M 800 -200 L 800 -300 L 800 -200" dur="20s" begin="0s" repeatCount="indefinite"/>
|
||||
<animateTransform attributeType="XML" attributeName="transform" begin="0s" dur="30s" type="rotate" values="0 210 530 ; -30 210 530 ; 0 210 530" keyTimes="0 ; 0.5 ; 1" repeatCount="indefinite"/>
|
||||
</path>
|
||||
|
||||
<circle cx="150" cy="150" r="180" stroke="#85FFBD" fill="#85FFBD">
|
||||
<animateMotion path="M 0 0 L 40 20 Z" dur="5s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
|
||||
<!-- 三角形 -->
|
||||
<path d="M 165 580 L 270 580 Q275 578 270 570 L 223 483 Q220 480 217 483 L 165 570 Q160 578 165 580" fill="#a2b3ff">
|
||||
<animateTransform attributeType="XML" attributeName="transform" begin="0s" dur="35s" type="rotate" from="0 210 530" to="360 210 530" repeatCount="indefinite"/>
|
||||
</path>
|
||||
|
||||
<!-- <circle cx="1200" cy="600" r="30" stroke="rgb(241, 243, 244)" fill="rgb(241, 243, 244)">-->
|
||||
<!-- <animateMotion path="M 0 0 L -20 40 Z" dur="9s" repeatCount="indefinite"/>-->
|
||||
<!-- </circle>-->
|
||||
|
||||
<path d="M 100 350 A 40 40 0 1 1 180 350 L 180 430 A 40 40 0 1 1 100 430 Z" fill="#3054EB">
|
||||
<animateMotion path="M 140 390 L 180 360 L 140 390" dur="20s" begin="0s" repeatCount="indefinite"/>
|
||||
<animateTransform attributeType="XML" attributeName="transform" begin="0s" dur="30s" type="rotate" values="0 140 390; -60 140 390; 0 140 390" keyTimes="0 ; 0.5 ; 1" repeatCount="indefinite"/>
|
||||
</path>
|
||||
|
||||
<rect x="400" y="600" rx="40" ry="40" width="100" height="100" stroke="rgb(129, 201, 149)" fill="#3054EB">
|
||||
<animateTransform attributeType="XML" attributeName="transform" begin="0s" dur="35s" type="rotate" from="-30 550 750" to="330 550 750" repeatCount="indefinite"/>
|
||||
</rect>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
BIN
frontend/src/assets/images/login_left.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
1
frontend/src/assets/images/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||
|
After Width: | Height: | Size: 276 B |
BIN
frontend/src/assets/images/msg01.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
frontend/src/assets/images/msg02.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
frontend/src/assets/images/msg03.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
frontend/src/assets/images/msg04.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
frontend/src/assets/images/msg05.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
frontend/src/assets/images/notData.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
39
frontend/src/components/Dialog/index.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<!-- 全局封装dialog组件 -->
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="50%"
|
||||
:before-close="handleClose"
|
||||
:draggable="true"
|
||||
:destroy-on-close="true"
|
||||
:append-to-body="true"
|
||||
>
|
||||
<div class="container">
|
||||
<slot name="container"></slot>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="dialogVisible = false">
|
||||
确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
const dialogVisible = ref<Boolean>(false);
|
||||
const dialogTitle = ref<string>("");
|
||||
const openDialog = (title: string) => {
|
||||
dialogTitle.value = title;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
onMounted(() => {
|
||||
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
20
frontend/src/components/ErrorMessage/403.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="not-container">
|
||||
<img src="@/assets/images/403.png" class="not-img" alt="403" />
|
||||
<div class="not-detail">
|
||||
<h2>403</h2>
|
||||
<h4>抱歉,您无权访问该页面~🙅♂️🙅♀️</h4>
|
||||
<el-button type="primary" @click="router.back">返回上一页</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="403">
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use './index.scss';
|
||||
</style>
|
||||
20
frontend/src/components/ErrorMessage/404.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="not-container">
|
||||
<img src="@/assets/images/404.png" class="not-img" alt="404" />
|
||||
<div class="not-detail">
|
||||
<h2>404</h2>
|
||||
<h4>抱歉,您访问的页面不存在~🤷♂️🤷♀️</h4>
|
||||
<el-button type="primary" @click="router.back">返回上一页</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="404">
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use './index.scss';
|
||||
</style>
|
||||
20
frontend/src/components/ErrorMessage/500.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="not-container">
|
||||
<img src="@/assets/images/500.png" class="not-img" alt="500" />
|
||||
<div class="not-detail">
|
||||
<h2>500</h2>
|
||||
<h4>抱歉,您的网络不见了~🤦♂️🤦♀️</h4>
|
||||
<el-button type="primary" @click="router.back">返回上一页</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="500">
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use './index.scss';
|
||||
</style>
|
||||
32
frontend/src/components/ErrorMessage/index.scss
Normal file
@@ -0,0 +1,32 @@
|
||||
.not-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.not-img {
|
||||
margin-right: 120px;
|
||||
}
|
||||
.not-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
h2,
|
||||
h4 {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
h2 {
|
||||
font-size: 60px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
h4 {
|
||||
margin: 30px 0 20px;
|
||||
font-size: 19px;
|
||||
font-weight: normal;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
.el-button {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
67
frontend/src/components/Grid/components/GridItem.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div v-show='isShow' :style='style'>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang='ts' name='GridItem'>
|
||||
import { BreakPoint, Responsive } from '../interface/index'
|
||||
|
||||
type Props = {
|
||||
offset?: number;
|
||||
span?: number;
|
||||
suffix?: boolean;
|
||||
xs?: Responsive;
|
||||
sm?: Responsive;
|
||||
md?: Responsive;
|
||||
lg?: Responsive;
|
||||
xl?: Responsive;
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
offset: 0,
|
||||
span: 1,
|
||||
suffix: false,
|
||||
xs: undefined,
|
||||
sm: undefined,
|
||||
md: undefined,
|
||||
lg: undefined,
|
||||
xl: undefined,
|
||||
})
|
||||
|
||||
const attrs = useAttrs() as { index: string }
|
||||
const isShow = ref(true)
|
||||
|
||||
// 注入断点
|
||||
const breakPoint = inject<Ref<BreakPoint>>('breakPoint', ref('xl'))
|
||||
const shouldHiddenIndex = inject<Ref<number>>('shouldHiddenIndex', ref(-1))
|
||||
watch(
|
||||
() => [shouldHiddenIndex.value, breakPoint.value],
|
||||
n => {
|
||||
if (!!attrs.index) {
|
||||
isShow.value = !(n[0] !== -1 && parseInt(attrs.index) >= Number(n[0]))
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const gap = inject('gap', 0)
|
||||
const cols = inject('cols', ref(4))
|
||||
const style = computed(() => {
|
||||
let span = props[breakPoint.value]?.span ?? props.span
|
||||
let offset = props[breakPoint.value]?.offset ?? props.offset
|
||||
if (props.suffix) {
|
||||
return {
|
||||
gridColumnStart: cols.value - span - offset + 1,
|
||||
gridColumnEnd: `span ${span + offset}`,
|
||||
marginLeft: offset !== 0 ? `calc(((100% + ${gap}px) / ${span + offset}) * ${offset})` : 'unset',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
gridColumn: `span ${span + offset > cols.value ? cols.value : span + offset}/span ${
|
||||
span + offset > cols.value ? cols.value : span + offset
|
||||
}`,
|
||||
marginLeft: offset !== 0 ? `calc(((100% + ${gap}px) / ${span + offset}) * ${offset})` : 'unset',
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
153
frontend/src/components/Grid/index.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<div :style='style'>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts' name='Grid'>
|
||||
import type { BreakPoint } from './interface/index'
|
||||
|
||||
type Props = {
|
||||
cols?: number | Record<BreakPoint, number>;
|
||||
collapsed?: boolean;
|
||||
collapsedRows?: number;
|
||||
gap?: [number, number] | number;
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
cols: () => ({ xs: 1, sm: 2, md: 2, lg: 3, xl: 4 }),
|
||||
collapsed: false,
|
||||
collapsedRows: 1,
|
||||
gap: 0,
|
||||
})
|
||||
|
||||
onBeforeMount(() => props.collapsed && findIndex())
|
||||
onMounted(() => {
|
||||
resize({ target: { innerWidth: window.innerWidth } } as unknown as UIEvent)
|
||||
window.addEventListener('resize', resize)
|
||||
})
|
||||
onActivated(() => {
|
||||
resize({ target: { innerWidth: window.innerWidth } } as unknown as UIEvent)
|
||||
window.addEventListener('resize', resize)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', resize)
|
||||
})
|
||||
onDeactivated(() => {
|
||||
window.removeEventListener('resize', resize)
|
||||
})
|
||||
|
||||
// 监听屏幕变化
|
||||
const resize = (e: UIEvent) => {
|
||||
let width = (e.target as Window).innerWidth
|
||||
switch (!!width) {
|
||||
case width < 768:
|
||||
breakPoint.value = 'xs'
|
||||
break
|
||||
case width >= 768 && width < 992:
|
||||
breakPoint.value = 'sm'
|
||||
break
|
||||
case width >= 992 && width < 1200:
|
||||
breakPoint.value = 'md'
|
||||
break
|
||||
case width >= 1200 && width < 1920:
|
||||
breakPoint.value = 'lg'
|
||||
break
|
||||
case width >= 1920:
|
||||
breakPoint.value = 'xl'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 注入 gap 间距
|
||||
provide('gap', Array.isArray(props.gap) ? props.gap[0] : props.gap)
|
||||
|
||||
// 注入响应式断点
|
||||
let breakPoint = ref<BreakPoint>('xl')
|
||||
provide('breakPoint', breakPoint)
|
||||
|
||||
// 注入要开始折叠的 index
|
||||
const hiddenIndex = ref(-1)
|
||||
provide('shouldHiddenIndex', hiddenIndex)
|
||||
|
||||
// 注入 cols
|
||||
const gridCols = computed(() => {
|
||||
if (typeof props.cols === 'object') return props.cols[breakPoint.value] ?? props.cols
|
||||
return props.cols
|
||||
})
|
||||
provide('cols', gridCols)
|
||||
|
||||
// 寻找需要开始折叠的字段 index
|
||||
const slots = useSlots().default!()
|
||||
|
||||
const findIndex = () => {
|
||||
let fields: VNodeArrayChildren = []
|
||||
let suffix: VNode | null = null
|
||||
slots.forEach((slot: any) => {
|
||||
// suffix
|
||||
if (typeof slot.type === 'object' && slot.type.name === 'GridItem' && slot.props?.suffix !== undefined) suffix = slot
|
||||
// slot children
|
||||
if (typeof slot.type === 'symbol' && Array.isArray(slot.children)) fields.push(...slot.children)
|
||||
})
|
||||
|
||||
// 计算 suffix 所占用的列
|
||||
let suffixCols = 0
|
||||
if (suffix) {
|
||||
suffixCols =
|
||||
((suffix as VNode).props![breakPoint.value]?.span ?? (suffix as VNode).props?.span ?? 1) +
|
||||
((suffix as VNode).props![breakPoint.value]?.offset ?? (suffix as VNode).props?.offset ?? 0)
|
||||
}
|
||||
try {
|
||||
let find = false
|
||||
fields.reduce((prev = 0, current, index) => {
|
||||
prev +=
|
||||
((current as VNode)!.props![breakPoint.value]?.span ?? (current as VNode)!.props?.span ?? 1) +
|
||||
((current as VNode)!.props![breakPoint.value]?.offset ?? (current as VNode)!.props?.offset ?? 0)
|
||||
if (Number(prev) >= props.collapsedRows * gridCols.value - suffixCols) {
|
||||
hiddenIndex.value = index
|
||||
find = true
|
||||
throw 'find it'
|
||||
}
|
||||
return prev
|
||||
}, 0)
|
||||
if (!find) hiddenIndex.value = -1
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// 断点变化时执行 findIndex
|
||||
watch(
|
||||
() => breakPoint.value,
|
||||
() => {
|
||||
if (props.collapsed) findIndex()
|
||||
},
|
||||
)
|
||||
|
||||
// 监听 collapsed
|
||||
watch(
|
||||
() => props.collapsed,
|
||||
value => {
|
||||
if (value) return findIndex()
|
||||
hiddenIndex.value = -1
|
||||
},
|
||||
)
|
||||
|
||||
// 设置间距
|
||||
const gridGap = computed(() => {
|
||||
if (typeof props.gap === 'number') return `${props.gap}px`
|
||||
if (Array.isArray(props.gap)) return `${props.gap[1]}px ${props.gap[0]}px`
|
||||
return 'unset'
|
||||
})
|
||||
|
||||
// 设置 style
|
||||
const style = computed(() => {
|
||||
return {
|
||||
display: 'grid',
|
||||
gridGap: gridGap.value,
|
||||
gridTemplateColumns: `repeat(${gridCols.value}, minmax(0, 1fr))`,
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({ breakPoint })
|
||||
</script>
|
||||
6
frontend/src/components/Grid/interface/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type BreakPoint = "xs" | "sm" | "md" | "lg" | "xl";
|
||||
|
||||
export type Responsive = {
|
||||
span?: number;
|
||||
offset?: number;
|
||||
};
|
||||
363
frontend/src/components/IpAddress/index.vue
Normal file
@@ -0,0 +1,363 @@
|
||||
<template>
|
||||
<div :class="{ 'disabled': disabled }">
|
||||
<ul class="ipAdress">
|
||||
<li v-for="(item, index) in ipAddress" :key="index">
|
||||
<input :ref="el => getInputRef(el, index)" v-model="item.value" type="text" class="ipInputClass"
|
||||
:disabled="disabled" @input="checkIpVal(item)" @keyup="$event => turnIpPosition(item, index, $event)"
|
||||
@blur="handleBlur" @mouseup="handleMouseUp(index)" />
|
||||
<div></div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="routePage">
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
// 接收来自上层的数据
|
||||
const props = defineProps(['value', 'disabled'])
|
||||
|
||||
// 更新数据
|
||||
const $emits = defineEmits(['update:value', 'blur'])
|
||||
|
||||
// 存储四个ref
|
||||
const ipInputRefs = ref<HTMLInputElement[]>([]);
|
||||
// 存储左右标识位
|
||||
let markFlag = ref([
|
||||
{
|
||||
left: false,
|
||||
right: false
|
||||
},
|
||||
{
|
||||
left: false,
|
||||
right: false
|
||||
},
|
||||
{
|
||||
left: false,
|
||||
right: false
|
||||
},
|
||||
{
|
||||
left: false,
|
||||
right: false
|
||||
}
|
||||
])
|
||||
|
||||
// 更新标识
|
||||
let flag = ref(false)
|
||||
|
||||
// 鼠标点击
|
||||
const handleMouseUp = (index: any) => {
|
||||
let input = ipInputRefs.value[index]
|
||||
// 全为false
|
||||
markFlag.value.forEach(item => {
|
||||
item.left = false
|
||||
item.right = false
|
||||
})
|
||||
// 证明在开始阶段
|
||||
if (input.selectionStart == 0) {
|
||||
|
||||
markFlag.value[index].left = true
|
||||
} else {
|
||||
markFlag.value[index].left = false
|
||||
}
|
||||
// 证明在结束
|
||||
if (input.selectionStart == (input.value || '').length) {
|
||||
markFlag.value[index].right = true
|
||||
} else {
|
||||
markFlag.value[index].right = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取四个input refs
|
||||
const getInputRef = (el: any, index: number) => {
|
||||
if (el) {
|
||||
ipInputRefs.value[index] = el;
|
||||
}
|
||||
};
|
||||
// 声明IP存储类型
|
||||
interface IpType {
|
||||
value: string
|
||||
}
|
||||
// 定义要显示的四个ip
|
||||
let ipAddress = ref<IpType[]>([
|
||||
{
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
value: "",
|
||||
},
|
||||
])
|
||||
// 初始化显示数据
|
||||
const initShowData = () => {
|
||||
// 判断不合理行为
|
||||
if (props.value === '') {
|
||||
ipAddress.value.forEach(item => {
|
||||
item.value = ''
|
||||
})
|
||||
} else {
|
||||
let ipList = props.value.split('.')
|
||||
ipAddress.value.forEach((item: IpType, index: number) => {
|
||||
item.value = ipList[index]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 检查ip输入
|
||||
const checkIpVal = (item: any) => {
|
||||
let val = item.value;
|
||||
|
||||
// 处理非数字
|
||||
val = val.toString().replace(/[^0-9]/g, "");
|
||||
val = parseInt(val, 10);
|
||||
if (isNaN(val)) {
|
||||
val = "";
|
||||
} else {
|
||||
val = val < 0 ? 0 : val;
|
||||
if (val > 255) {
|
||||
// 判断val是几位数
|
||||
let num = (val + '').length
|
||||
if (num > 3) {
|
||||
val = parseInt((val + '').substring(0, 3))
|
||||
} else {
|
||||
val = 255
|
||||
}
|
||||
}
|
||||
}
|
||||
item.value = val;
|
||||
|
||||
}
|
||||
|
||||
// 判断光标左右移动位置
|
||||
const turnIpPosition = (item: IpType, index: number, event: any) => {
|
||||
let e = event || window.event;
|
||||
if (e.keyCode === 37) {
|
||||
// 左箭头向左跳转,左一不做任何措施
|
||||
if (index == 0) {
|
||||
return
|
||||
}
|
||||
if (e.currentTarget.selectionStart === 0) {
|
||||
if (markFlag.value[index].left) {
|
||||
handleFocus(index - 1, 'toLeft')
|
||||
markFlag.value[index].left = false
|
||||
markFlag.value[index].right = false
|
||||
} else {
|
||||
|
||||
markFlag.value[index].left = true
|
||||
}
|
||||
} else {
|
||||
markFlag.value[index].right = false
|
||||
markFlag.value[index].left = false
|
||||
}
|
||||
} else if (e.keyCode == 39) {
|
||||
// 右箭头向右跳转,右一不做任何措施
|
||||
markFlag.value[index].left = false
|
||||
let start = e.currentTarget.selectionStart
|
||||
if (index != 3 && start === item.value.toString().length) {
|
||||
if (markFlag.value[index].right) {
|
||||
handleFocus(index + 1, 'toRight')
|
||||
markFlag.value[index].left = false
|
||||
markFlag.value[index].right = false
|
||||
} else {
|
||||
markFlag.value[index].right = true
|
||||
}
|
||||
} else {
|
||||
markFlag.value[index].right = false
|
||||
}
|
||||
} else if (e.keyCode === 8) {
|
||||
|
||||
// 删除键把当前数据删除完毕后会跳转到前一个input,左一不做任何处理
|
||||
if (index !== 0 && e.currentTarget.selectionStart === 0) {
|
||||
if (markFlag.value[index].left) {
|
||||
ipInputRefs.value[index - 1].focus();
|
||||
markFlag.value[index].left = false
|
||||
} else {
|
||||
|
||||
markFlag.value[index].left = true
|
||||
}
|
||||
}
|
||||
} else if (e.keyCode === 13 || e.keyCode === 32) {
|
||||
|
||||
// 回车键、空格键、冒号均向右跳转,右一不做任何措施
|
||||
if (index !== 3) {
|
||||
ipInputRefs.value[index + 1].focus();
|
||||
}
|
||||
}
|
||||
else if (e.keyCode === 110 || e.keyCode === 190) {
|
||||
// 点 . 向右跳转,右一不做任何措施
|
||||
|
||||
if (item.value == '') {
|
||||
return
|
||||
}
|
||||
if (index !== 3) {
|
||||
ipInputRefs.value[index + 1].select();
|
||||
}
|
||||
}
|
||||
else if (item.value.toString().length === 3) {
|
||||
|
||||
// 满3位,光标自动向下一个文本框.
|
||||
if (index !== 3) {
|
||||
//ipInputRefs.value[index + 1].setSelectionRange(0, 0)
|
||||
ipInputRefs.value[index + 1].focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理聚焦
|
||||
const handleFocus = (index: number, direction: string) => {
|
||||
// 设置当前位置为选中状态 toRight:从左边来的
|
||||
let input = ipInputRefs.value[index]
|
||||
input.focus()
|
||||
let value = input.value
|
||||
// null 左右全部设置为true,可以直接跳转
|
||||
if ((value || '').length == 0) {
|
||||
markFlag.value[index].right = true
|
||||
markFlag.value[index].left = true
|
||||
} else {
|
||||
if (direction == 'toRight') {
|
||||
// 可以直接跳回
|
||||
|
||||
markFlag.value[index].left = true
|
||||
// 设置光标为左边第一个
|
||||
ipInputRefs.value[index].setSelectionRange(0, 0)
|
||||
// 设置上一个的右边标识为false
|
||||
markFlag.value[index - 1] && (markFlag.value[index - 1].right = false)
|
||||
} else {
|
||||
// 直接跳回
|
||||
markFlag.value[index].right = true
|
||||
// 设置后一个侧边为false
|
||||
markFlag.value[index + 1] && (markFlag.value[index + 1].left = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 格式化补零方法
|
||||
const formatter = (val: string) => {
|
||||
let value = val.toString();
|
||||
if (value.length === 2) {
|
||||
value = "0" + value;
|
||||
} else if (value.length === 1) {
|
||||
value = "00" + value;
|
||||
} else if (value.length === 0) {
|
||||
value = "000";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// 监听数据变化,并初始化显示四个数据
|
||||
watch(() => props.value, () => {
|
||||
|
||||
if(flag.value){
|
||||
|
||||
}else{
|
||||
initShowData()
|
||||
}
|
||||
}, {
|
||||
immediate: true
|
||||
})
|
||||
|
||||
// 监听ipAddress数据变化
|
||||
watch(ipAddress, () => {
|
||||
let str = "";
|
||||
for (const i in ipAddress.value) {
|
||||
str += formatter(ipAddress.value[i].value);
|
||||
}
|
||||
if (str === "000000000000") {
|
||||
str = "";
|
||||
} else {
|
||||
str = ipAddress.value.map(item => {
|
||||
if (item.value !== null) {
|
||||
return item.value + ''
|
||||
} else {
|
||||
return '0'
|
||||
}
|
||||
}).join(".")
|
||||
}
|
||||
$emits('update:value', str)
|
||||
flag.value = true
|
||||
setTimeout(() => {
|
||||
flag.value = false
|
||||
}, 100)
|
||||
}, {
|
||||
deep: true
|
||||
})
|
||||
|
||||
const handleBlur = () => {
|
||||
$emits('blur')
|
||||
}
|
||||
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.disabled {
|
||||
cursor: not-allowed;
|
||||
background-color: #f5f7fa;
|
||||
|
||||
.ipAdress {
|
||||
li {
|
||||
.ipInputClass {
|
||||
color: #c3c4cc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ipAdress {
|
||||
display: flex;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
line-height: 40px;
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
padding-inline-start: 0px;
|
||||
padding-left: 10px;
|
||||
padding-right: 20px;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ipAdress li {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.ipInputClass {
|
||||
border: none;
|
||||
width: 50px;
|
||||
height: 23px;
|
||||
text-align: center;
|
||||
color: #606266;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ipAdress li div {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
right: 0;
|
||||
border-radius: 50%;
|
||||
background: #b6b8bc;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
/*只需要3个div*/
|
||||
.ipAdress li:last-child div {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/*取消掉默认的input focus状态*/
|
||||
.ipAdress input:focus {
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
45
frontend/src/components/Loading/fullScreen.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { ElLoading } from "element-plus";
|
||||
|
||||
/* 全局请求 loading */
|
||||
let loadingInstance: ReturnType<typeof ElLoading.service>;
|
||||
|
||||
/**
|
||||
* @description 开启 Loading
|
||||
* */
|
||||
const startLoading = () => {
|
||||
loadingInstance = ElLoading.service({
|
||||
fullscreen: true,
|
||||
lock: true,
|
||||
text: "Loading",
|
||||
background: "rgba(0, 0, 0, 0.7)"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 结束 Loading
|
||||
* */
|
||||
const endLoading = () => {
|
||||
loadingInstance.close();
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 显示全屏加载
|
||||
* */
|
||||
let needLoadingRequestCount = 0;
|
||||
export const showFullScreenLoading = () => {
|
||||
if (needLoadingRequestCount === 0) {
|
||||
startLoading();
|
||||
}
|
||||
needLoadingRequestCount++;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 隐藏全屏加载
|
||||
* */
|
||||
export const tryHideFullScreenLoading = () => {
|
||||
if (needLoadingRequestCount <= 0) return;
|
||||
needLoadingRequestCount--;
|
||||
if (needLoadingRequestCount === 0) {
|
||||
endLoading();
|
||||
}
|
||||
};
|
||||
67
frontend/src/components/Loading/index.scss
Normal file
@@ -0,0 +1,67 @@
|
||||
.loading-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.loading-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 98px;
|
||||
}
|
||||
}
|
||||
.dot {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 32px;
|
||||
transform: rotate(45deg);
|
||||
animation: ant-rotate 1.2s infinite linear;
|
||||
}
|
||||
.dot i {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background-color: var(--el-color-primary);
|
||||
border-radius: 100%;
|
||||
opacity: 0.3;
|
||||
transform: scale(0.75);
|
||||
transform-origin: 50% 50%;
|
||||
animation: ant-spin-move 1s infinite linear alternate;
|
||||
}
|
||||
.dot i:nth-child(1) {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
.dot i:nth-child(2) {
|
||||
top: 0;
|
||||
right: 0;
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
.dot i:nth-child(3) {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
animation-delay: 0.8s;
|
||||
}
|
||||
.dot i:nth-child(4) {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
|
||||
@keyframes ant-rotate {
|
||||
to {
|
||||
transform: rotate(405deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ant-spin-move {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
13
frontend/src/components/Loading/index.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div class="loading-box">
|
||||
<div class="loading-wrap">
|
||||
<span class="dot dot-spin"><i></i><i></i><i></i><i></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="Loading"></script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "./index.scss";;
|
||||
</style>
|
||||
45
frontend/src/components/ProTable/components/ColSetting.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<!-- 列设置 -->
|
||||
<el-drawer v-model="drawerVisible" title="列设置" size="450px">
|
||||
<div class="table-main">
|
||||
<el-table :data="colSetting" :border="true" row-key="prop" default-expand-all :tree-props="{ children: '_children' }">
|
||||
<el-table-column prop="label" align="center" label="列名" />
|
||||
<el-table-column v-slot="scope" prop="isShow" align="center" label="显示">
|
||||
<el-switch v-model="scope.row.isShow"></el-switch>
|
||||
</el-table-column>
|
||||
<el-table-column v-slot="scope" prop="sortable" align="center" label="排序">
|
||||
<el-switch v-model="scope.row.sortable"></el-switch>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<div class="table-empty">
|
||||
<img src="@/assets/images/notData.png" alt="notData" />
|
||||
<div>暂无可配置列</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="ColSetting">
|
||||
import { ref } from "vue";
|
||||
import { ColumnProps } from "@/components/ProTable/interface";
|
||||
|
||||
defineProps<{ colSetting: ColumnProps[] }>();
|
||||
|
||||
const drawerVisible = ref<boolean>(false);
|
||||
|
||||
const openColSetting = () => {
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
openColSetting
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.cursor-move {
|
||||
cursor: move;
|
||||
}
|
||||
</style>
|
||||
39
frontend/src/components/ProTable/components/Pagination.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<!-- 分页组件 -->
|
||||
<el-pagination
|
||||
:background="true"
|
||||
:current-page="pageable.current"
|
||||
:page-size="pageable.size"
|
||||
:page-sizes="[10, 25, 50, 100]"
|
||||
:total="pageable.total"
|
||||
:size="globalStore?.assemblySize ?? 'default'"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
></el-pagination>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="Pagination">
|
||||
import { useGlobalStore } from "@/stores/modules/global";
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
interface Pageable {
|
||||
current: number;
|
||||
size: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface ResPageable {
|
||||
current: number;
|
||||
size: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface PaginationProps {
|
||||
pageable: ResPageable;
|
||||
handleSizeChange: (size: number) => void;
|
||||
handleCurrentChange: (currentPage: number) => void;
|
||||
}
|
||||
|
||||
defineProps<PaginationProps>();
|
||||
</script>
|
||||
57
frontend/src/components/ProTable/components/TableColumn.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<RenderTableColumn v-bind='column' />
|
||||
</template>
|
||||
|
||||
<script setup lang='tsx' name='TableColumn'>
|
||||
import { ColumnProps, RenderScope, HeaderRenderScope } from '@/components/ProTable/interface'
|
||||
import { filterEnum, formatValue, handleProp, handleRowAccordingToProp } from '@/utils'
|
||||
|
||||
defineProps<{ column: ColumnProps }>()
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
const enumMap = inject('enumMap', ref(new Map()))
|
||||
|
||||
// 渲染表格数据
|
||||
const renderCellData = (item: ColumnProps, scope: RenderScope<any>) => {
|
||||
return enumMap.value.get(item.prop) && item.isFilterEnum
|
||||
? filterEnum(handleRowAccordingToProp(scope.row, item.prop!), enumMap.value.get(item.prop)!, item.fieldNames)
|
||||
: formatValue(handleRowAccordingToProp(scope.row, item.prop!))
|
||||
}
|
||||
|
||||
// 获取 tag 类型
|
||||
const getTagType = (item: ColumnProps, scope: RenderScope<any>) => {
|
||||
return (
|
||||
filterEnum(handleRowAccordingToProp(scope.row, item.prop!), enumMap.value.get(item.prop), item.fieldNames, 'tag') || 'primary'
|
||||
)
|
||||
}
|
||||
|
||||
const RenderTableColumn = (item: ColumnProps) => {
|
||||
return (
|
||||
<>
|
||||
{item.isShow && (
|
||||
<el-table-column
|
||||
{...item}
|
||||
align={item.align ?? 'center'}
|
||||
showOverflowTooltip={item.showOverflowTooltip ?? item.prop !== 'operation'}
|
||||
>
|
||||
{{
|
||||
default: (scope: RenderScope<any>) => {
|
||||
if (item._children) return item._children.map(child => RenderTableColumn(child))
|
||||
if (item.render) return item.render(scope)
|
||||
if (item.prop && slots[handleProp(item.prop)]) return slots[handleProp(item.prop)]!(scope)
|
||||
if (item.tag) return <el-tag type={getTagType(item, scope)}>{renderCellData(item, scope)}</el-tag>
|
||||
return renderCellData(item, scope)
|
||||
},
|
||||
header: (scope: HeaderRenderScope<any>) => {
|
||||
if (item.headerRender) return item.headerRender(scope)
|
||||
if (item.prop && slots[`${handleProp(item.prop)}Header`]) return slots[`${handleProp(item.prop)}Header`]!(scope)
|
||||
return item.label
|
||||
},
|
||||
}}
|
||||
</el-table-column>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</script>
|
||||
336
frontend/src/components/ProTable/index.vue
Normal file
@@ -0,0 +1,336 @@
|
||||
<template>
|
||||
<!-- 查询表单 -->
|
||||
<SearchForm
|
||||
v-show='isShowSearch'
|
||||
:search='_search'
|
||||
:reset='_reset'
|
||||
:columns='searchColumns'
|
||||
:search-param='searchParam'
|
||||
:search-col='searchCol'
|
||||
/>
|
||||
<!-- 表格主体 -->
|
||||
<div class='table-main' :class='{ card: showCard }' >
|
||||
<!-- 表格头部 操作按钮 -->
|
||||
<div class='table-header'>
|
||||
<div class='header-button-lf'>
|
||||
<slot name='tableHeader' :selected-list='selectedList' :selected-list-ids='selectedListIds'
|
||||
:is-selected='isSelected' />
|
||||
</div>
|
||||
<div v-if='toolButton' class='header-button-ri'>
|
||||
<slot name='toolButton'>
|
||||
<el-button v-if="showToolButton('refresh')" :icon='Refresh' circle @click='getTableList' />
|
||||
<el-button v-if="showToolButton('setting') && columns.length" :icon='Operation' circle
|
||||
@click='openColSetting' />
|
||||
<el-button
|
||||
v-if="showToolButton('search') && searchColumns?.length"
|
||||
:icon='Search'
|
||||
circle
|
||||
@click='isShowSearch = !isShowSearch'
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 表格主体 -->
|
||||
<el-table
|
||||
ref='tableRef'
|
||||
v-bind='$attrs'
|
||||
:id='uuid'
|
||||
:data='processTableData'
|
||||
:border='border'
|
||||
:row-key='rowKey'
|
||||
@selection-change='selectionChange'
|
||||
>
|
||||
<!-- 默认插槽 -->
|
||||
<slot />
|
||||
|
||||
<template v-for='item in tableColumns' :key='item'>
|
||||
<!-- selection || radio || index || expand || sort -->
|
||||
<el-table-column
|
||||
v-if='item.type && columnTypes.includes(item.type) && item.isShow'
|
||||
v-bind='item'
|
||||
:align="item.align ?? 'center'"
|
||||
:reserve-selection="item.type == 'selection'"
|
||||
>
|
||||
<template #default='scope'>
|
||||
<!-- expand -->
|
||||
<template v-if="item.type == 'expand'">
|
||||
<component :is='item.render' v-bind='scope' v-if='item.render' />
|
||||
<slot v-else :name='item.type' v-bind='scope' />
|
||||
</template>
|
||||
<!-- radio -->
|
||||
<el-radio v-if="item.type == 'radio'" v-model='radio' :label='scope.row[rowKey]'>
|
||||
<i></i>
|
||||
</el-radio>
|
||||
<!-- sort -->
|
||||
<el-tag v-if="item.type == 'sort'" class='move'>
|
||||
<el-icon>
|
||||
<DCaret />
|
||||
</el-icon>
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- other -->
|
||||
<TableColumn v-else :column='item'>
|
||||
<template v-for='slot in Object.keys($slots)' #[slot]='scope'>
|
||||
<slot :name='slot' v-bind='scope' />
|
||||
</template>
|
||||
</TableColumn>
|
||||
</template>
|
||||
<!-- 插入表格最后一行之后的插槽 -->
|
||||
<template #append>
|
||||
<slot name='append' />
|
||||
</template>
|
||||
<!-- 无数据 -->
|
||||
<template #empty>
|
||||
<div class='table-empty'>
|
||||
<slot name='empty'>
|
||||
<img src='@/assets/images/notData.png' alt='notData' />
|
||||
<div>暂无数据</div>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
</el-table>
|
||||
<!-- 分页组件 -->
|
||||
<slot name='pagination'>
|
||||
<Pagination
|
||||
v-if='pagination'
|
||||
:pageable='resPageable'
|
||||
:handle-size-change='handleSizeChange'
|
||||
:handle-current-change='handleCurrentChange'
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
<!-- 列设置 -->
|
||||
<ColSetting v-if='toolButton' ref='colRef' v-model:col-setting='colSetting' />
|
||||
</template>
|
||||
|
||||
<script setup lang='ts' name='ProTable'>
|
||||
import { ElTable } from 'element-plus'
|
||||
import { useTable } from '@/hooks/useTable'
|
||||
import { useSelection } from '@/hooks/useSelection'
|
||||
import { BreakPoint } from '@/components/Grid/interface'
|
||||
import { ColumnProps, TypeProps } from '@/components/ProTable/interface'
|
||||
import { Refresh, Operation, Search } from '@element-plus/icons-vue'
|
||||
import { generateUUID, handleProp } from '@/utils'
|
||||
import SearchForm from '@/components/SearchForm/index.vue'
|
||||
import Pagination from './components/Pagination.vue'
|
||||
import ColSetting from './components/ColSetting.vue'
|
||||
import TableColumn from './components/TableColumn.vue'
|
||||
import Sortable from 'sortablejs'
|
||||
|
||||
export interface ProTableProps {
|
||||
|
||||
columns: ColumnProps[]; // 列配置项 ==> 必传
|
||||
data?: any[]; // 静态 table data 数据,若存在则不会使用 requestApi 返回的 data ==> 非必传
|
||||
requestApi?: (params: any) => Promise<any>; // 请求表格数据的 api ==> 非必传
|
||||
requestAuto?: boolean; // 是否自动执行请求 api ==> 非必传(默认为true)
|
||||
requestError?: (params: any) => void; // 表格 api 请求错误监听 ==> 非必传
|
||||
dataCallback?: (data: any) => any; // 返回数据的回调函数,可以对数据进行处理 ==> 非必传
|
||||
title?: string; // 表格标题 ==> 非必传
|
||||
showCard?: boolean; // 下个是否需要卡片
|
||||
pagination?: boolean; // 是否需要分页组件 ==> 非必传(默认为true)
|
||||
initParam?: any; // 初始化请求参数 ==> 非必传(默认为{})
|
||||
border?: boolean; // 是否带有纵向边框 ==> 非必传(默认为true)
|
||||
toolButton?: ('refresh' | 'setting' | 'search')[] | boolean; // 是否显示表格功能按钮 ==> 非必传(默认为true)
|
||||
rowKey?: string; // 行数据的 Key,用来优化 Table 的渲染,当表格数据多选时,所指定的 id ==> 非必传(默认为 id)
|
||||
searchCol?: number | Record<BreakPoint, number>; // 表格搜索项 每列占比配置 ==> 非必传 { xs: 1, sm: 2, md: 2, lg: 3, xl: 4 }
|
||||
}
|
||||
|
||||
// 接受父组件参数,配置默认值
|
||||
const props = withDefaults(defineProps<ProTableProps>(), {
|
||||
columns: () => [],
|
||||
requestAuto: true,
|
||||
pagination: true,
|
||||
initParam: {},
|
||||
border: true,
|
||||
showCard: true,
|
||||
toolButton: true,
|
||||
rowKey: 'id',
|
||||
searchCol: () => ({ xs: 1, sm: 2, md: 2, lg: 3, xl: 4 }),
|
||||
})
|
||||
|
||||
// table 实例
|
||||
const tableRef = ref<InstanceType<typeof ElTable>>()
|
||||
|
||||
// 生成组件唯一id
|
||||
const uuid = ref('id-' + generateUUID())
|
||||
|
||||
// column 列类型
|
||||
const columnTypes: TypeProps[] = ['selection', 'radio', 'index', 'expand', 'sort']
|
||||
|
||||
// 是否显示搜索模块
|
||||
const isShowSearch = ref(true)
|
||||
|
||||
// 控制 ToolButton 显示
|
||||
const showToolButton = (key: 'refresh' | 'setting' | 'search') => {
|
||||
return Array.isArray(props.toolButton) ? props.toolButton.includes(key) : props.toolButton
|
||||
}
|
||||
|
||||
// 单选值
|
||||
const radio = ref('')
|
||||
|
||||
// 表格多选 Hooks
|
||||
const { selectionChange, selectedList, selectedListIds, isSelected } = useSelection(props.rowKey)
|
||||
|
||||
// 表格操作 Hooks
|
||||
const {
|
||||
tableData,
|
||||
pageable,
|
||||
resPageable,
|
||||
searchParam,
|
||||
searchInitParam,
|
||||
getTableList,
|
||||
search,
|
||||
reset,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
} =
|
||||
useTable(props.requestApi, props.initParam, props.pagination, props.dataCallback, props.requestError)
|
||||
|
||||
// 清空选中数据列表
|
||||
const clearSelection = () => tableRef.value!.clearSelection()
|
||||
|
||||
// 初始化表格数据 && 拖拽排序
|
||||
onMounted(() => {
|
||||
dragSort()
|
||||
props.requestAuto && getTableList()
|
||||
props.data && (resPageable.value.total = props.data.length)
|
||||
})
|
||||
|
||||
// 处理表格数据
|
||||
const processTableData = computed(() => {
|
||||
if (!props.data) return tableData.value
|
||||
if (!props.pagination) return props.data
|
||||
return props.data.slice(
|
||||
(resPageable.value.current - 1) * resPageable.value.size,
|
||||
resPageable.value.size * resPageable.value.current,
|
||||
)
|
||||
})
|
||||
|
||||
// 监听页面 initParam 改化,重新获取表格数据
|
||||
watch(() => props.initParam, getTableList, { deep: true })
|
||||
|
||||
// 接收 columns 并设置为响应式
|
||||
const tableColumns = reactive<ColumnProps[]>(props.columns)
|
||||
|
||||
// 扁平化 columns
|
||||
const flatColumns = computed(() => flatColumnsFunc(tableColumns))
|
||||
|
||||
// 定义 enumMap 存储 enum 值(避免异步请求无法格式化单元格内容 || 无法填充搜索下拉选择)
|
||||
const enumMap = ref(new Map<string, { [key: string]: any }[]>())
|
||||
const setEnumMap = async ({ prop, enum: enumValue }: ColumnProps) => {
|
||||
if (!enumValue) return
|
||||
|
||||
// 如果当前 enumMap 存在相同的值 return
|
||||
if (enumMap.value.has(prop!) && (typeof enumValue === 'function' || enumMap.value.get(prop!) === enumValue)) return
|
||||
|
||||
// 当前 enum 为静态数据,则直接存储到 enumMap
|
||||
if (typeof enumValue !== 'function') return enumMap.value.set(prop!, unref(enumValue!))
|
||||
|
||||
// 为了防止接口执行慢,而存储慢,导致重复请求,所以预先存储为[],接口返回后再二次存储
|
||||
enumMap.value.set(prop!, [])
|
||||
|
||||
// 当前 enum 为后台数据需要请求数据,则调用该请求接口,并存储到 enumMap
|
||||
const { data } = await enumValue()
|
||||
enumMap.value.set(prop!, data)
|
||||
}
|
||||
|
||||
// 注入 enumMap
|
||||
provide('enumMap', enumMap)
|
||||
|
||||
// 扁平化 columns 的方法
|
||||
const flatColumnsFunc = (columns: ColumnProps[], flatArr: ColumnProps[] = []) => {
|
||||
columns.forEach(async col => {
|
||||
if (col._children?.length) flatArr.push(...flatColumnsFunc(col._children))
|
||||
flatArr.push(col)
|
||||
|
||||
// column 添加默认 isShow && isSetting && isFilterEnum 属性值
|
||||
col.isShow = col.isShow ?? true
|
||||
col.isSetting = col.isSetting ?? true
|
||||
col.isFilterEnum = col.isFilterEnum ?? true
|
||||
|
||||
// 设置 enumMap
|
||||
await setEnumMap(col)
|
||||
})
|
||||
return flatArr.filter(item => !item._children?.length)
|
||||
}
|
||||
|
||||
// 过滤需要搜索的配置项 && 排序
|
||||
const searchColumns = computed(() => {
|
||||
return flatColumns.value
|
||||
?.filter(item => item.search?.el || item.search?.render)
|
||||
.sort((a, b) => a.search!.order! - b.search!.order!)
|
||||
})
|
||||
|
||||
// 设置 搜索表单默认排序 && 搜索表单项的默认值
|
||||
searchColumns.value?.forEach((column, index) => {
|
||||
column.search!.order = column.search?.order ?? index + 2
|
||||
const key = column.search?.key ?? handleProp(column.prop!)
|
||||
const defaultValue = column.search?.defaultValue
|
||||
if (defaultValue !== undefined && defaultValue !== null) {
|
||||
searchParam.value[key] = defaultValue
|
||||
searchInitParam.value[key] = defaultValue
|
||||
}
|
||||
})
|
||||
|
||||
// 列设置 ==> 需要过滤掉不需要设置的列
|
||||
const colRef = ref()
|
||||
const colSetting = tableColumns!.filter(item => {
|
||||
const { type, prop, isSetting } = item
|
||||
return !columnTypes.includes(type!) && prop !== 'operation' && isSetting
|
||||
})
|
||||
const openColSetting = () => colRef.value.openColSetting()
|
||||
|
||||
// 定义 emit 事件
|
||||
const emit = defineEmits<{
|
||||
search: [];
|
||||
reset: [];
|
||||
dragSort: [{ newIndex?: number; oldIndex?: number }];
|
||||
}>()
|
||||
|
||||
const _search = () => {
|
||||
search()
|
||||
emit('search')
|
||||
}
|
||||
|
||||
const _reset = () => {
|
||||
reset()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 表格拖拽排序
|
||||
const dragSort = () => {
|
||||
const tbody = document.querySelector(`#${uuid.value} tbody`) as HTMLElement
|
||||
Sortable.create(tbody, {
|
||||
handle: '.move',
|
||||
animation: 300,
|
||||
onEnd({ newIndex, oldIndex }) {
|
||||
const [removedItem] = processTableData.value.splice(oldIndex!, 1)
|
||||
processTableData.value.splice(newIndex!, 0, removedItem)
|
||||
emit('dragSort', { newIndex, oldIndex })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 暴露给父组件的参数和方法 (外部需要什么,都可以从这里暴露出去)
|
||||
defineExpose({
|
||||
element: tableRef,
|
||||
tableData: processTableData,
|
||||
radio,
|
||||
pageable,
|
||||
searchParam,
|
||||
searchInitParam,
|
||||
isSelected,
|
||||
selectedList,
|
||||
selectedListIds,
|
||||
|
||||
// 下面为 function
|
||||
getTableList,
|
||||
search,
|
||||
reset,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
clearSelection,
|
||||
enumMap,
|
||||
})
|
||||
</script>
|
||||
86
frontend/src/components/ProTable/interface/index.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { VNode, ComponentPublicInstance, Ref } from "vue";
|
||||
import { BreakPoint, Responsive } from "@/components/Grid/interface";
|
||||
import { TableColumnCtx } from "element-plus/es/components/table/src/table-column/defaults";
|
||||
import { ProTableProps } from "@/components/ProTable/index.vue";
|
||||
import ProTable from "@/components/ProTable/index.vue";
|
||||
|
||||
export interface EnumProps {
|
||||
label?: string; // 选项框显示的文字
|
||||
value?: string | number | boolean | any[]; // 选项框值
|
||||
disabled?: boolean; // 是否禁用此选项
|
||||
tagType?: string; // 当 tag 为 true 时,此选择会指定 tag 显示类型
|
||||
children?: EnumProps[]; // 为树形选择时,可以通过 children 属性指定子选项
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type TypeProps = "index" | "selection" | "radio" | "expand" | "sort";
|
||||
|
||||
export type SearchType =
|
||||
| "input"
|
||||
| "input-number"
|
||||
| "select"
|
||||
| "select-v2"
|
||||
| "tree-select"
|
||||
| "cascader"
|
||||
| "date-picker"
|
||||
| "time-picker"
|
||||
| "time-select"
|
||||
| "switch"
|
||||
| "slider";
|
||||
|
||||
export type SearchRenderScope = {
|
||||
searchParam: { [key: string]: any };
|
||||
placeholder: string;
|
||||
clearable: boolean;
|
||||
options: EnumProps[];
|
||||
data: EnumProps[];
|
||||
};
|
||||
|
||||
export type SearchProps = {
|
||||
el?: SearchType; // 当前项搜索框的类型
|
||||
label?: string; // 当前项搜索框的 label
|
||||
props?: any; // 搜索项参数,根据 element plus 官方文档来传递,该属性所有值会透传到组件
|
||||
key?: string; // 当搜索项 key 不为 prop 属性时,可通过 key 指定
|
||||
tooltip?: string; // 搜索提示
|
||||
order?: number; // 搜索项排序(从大到小)
|
||||
span?: number; // 搜索项所占用的列数,默认为 1 列
|
||||
offset?: number; // 搜索字段左侧偏移列数
|
||||
defaultValue?: string | number | boolean | any[] | Ref<any>; // 搜索项默认值
|
||||
render?: (scope: SearchRenderScope) => VNode; // 自定义搜索内容渲染(tsx语法)
|
||||
} & Partial<Record<BreakPoint, Responsive>>;
|
||||
|
||||
export type FieldNamesProps = {
|
||||
label: string;
|
||||
value: string;
|
||||
children?: string;
|
||||
};
|
||||
|
||||
export type RenderScope<T> = {
|
||||
row: T;
|
||||
$index: number;
|
||||
column: TableColumnCtx<T>;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type HeaderRenderScope<T> = {
|
||||
$index: number;
|
||||
column: TableColumnCtx<T>;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export interface ColumnProps<T = any>
|
||||
extends Partial<Omit<TableColumnCtx<T>, "type" | "children" | "renderCell" | "renderHeader">> {
|
||||
type?: TypeProps; // 列类型
|
||||
tag?: boolean | Ref<boolean>; // 是否是标签展示
|
||||
isShow?: boolean | Ref<boolean>; // 是否显示在表格当中
|
||||
isSetting?: boolean | Ref<boolean>; // 是否在 ColSetting 中可配置
|
||||
search?: SearchProps | undefined; // 搜索项配置
|
||||
enum?: EnumProps[] | Ref<EnumProps[]> | ((params?: any) => Promise<any>); // 枚举字典
|
||||
isFilterEnum?: boolean | Ref<boolean>; // 当前单元格值是否根据 enum 格式化(示例:enum 只作为搜索项数据)
|
||||
fieldNames?: FieldNamesProps; // 指定 label && value && children 的 key 值
|
||||
headerRender?: (scope: HeaderRenderScope<T>) => VNode; // 自定义表头内容渲染(tsx语法)
|
||||
render?: (scope: RenderScope<T>) => VNode | string; // 自定义单元格内容渲染(tsx语法)
|
||||
_children?: ColumnProps<T>[]; // 多级表头
|
||||
}
|
||||
|
||||
export type ProTableInstance = Omit<InstanceType<typeof ProTable>, keyof ComponentPublicInstance | keyof ProTableProps>;
|
||||
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<component
|
||||
:is="column.search?.render ?? `el-${column.search?.el}`"
|
||||
v-bind="{ ...handleSearchProps, ...placeholder, searchParam: _searchParam, clearable }"
|
||||
v-model.trim="_searchParam[column.search?.key ?? handleProp(column.prop!)]"
|
||||
:data="column.search?.el === 'tree-select' ? columnEnum : []"
|
||||
:options="['cascader', 'select-v2'].includes(column.search?.el!) ? columnEnum : []"
|
||||
>
|
||||
<template v-if="column.search?.el === 'cascader'" #default="{ data }">
|
||||
<span>{{ data[fieldNames.label] }}</span>
|
||||
</template>
|
||||
<template v-if="column.search?.el === 'select'">
|
||||
<component
|
||||
:is="`el-option`"
|
||||
v-for="(col, index) in columnEnum"
|
||||
:key="index"
|
||||
:label="col[fieldNames.label]"
|
||||
:value="col[fieldNames.value]"
|
||||
></component>
|
||||
</template>
|
||||
<slot v-else></slot>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="SearchFormItem">
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { handleProp } from "@/utils";
|
||||
import { ColumnProps } from "@/components/ProTable/interface";
|
||||
|
||||
interface SearchFormItem {
|
||||
column: ColumnProps;
|
||||
searchParam: { [key: string]: any };
|
||||
}
|
||||
const props = defineProps<SearchFormItem>();
|
||||
|
||||
// Re receive SearchParam
|
||||
const _searchParam = computed(() => props.searchParam);
|
||||
|
||||
// 判断 fieldNames 设置 label && value && children 的 key 值
|
||||
const fieldNames = computed(() => {
|
||||
return {
|
||||
label: props.column.fieldNames?.label ?? "label",
|
||||
value: props.column.fieldNames?.value ?? "value",
|
||||
children: props.column.fieldNames?.children ?? "children"
|
||||
};
|
||||
});
|
||||
|
||||
// 接收 enumMap (el 为 select-v2 需单独处理 enumData)
|
||||
const enumMap = inject("enumMap", ref(new Map()));
|
||||
const columnEnum = computed(() => {
|
||||
let enumData = enumMap.value.get(props.column.prop);
|
||||
if (!enumData) return [];
|
||||
if (props.column.search?.el === "select-v2" && props.column.fieldNames) {
|
||||
enumData = enumData.map((item: { [key: string]: any }) => {
|
||||
return { ...item, label: item[fieldNames.value.label], value: item[fieldNames.value.value] };
|
||||
});
|
||||
}
|
||||
return enumData;
|
||||
});
|
||||
|
||||
// 处理透传的 searchProps (el 为 tree-select、cascader 的时候需要给下默认 label && value && children)
|
||||
const handleSearchProps = computed(() => {
|
||||
const label = fieldNames.value.label;
|
||||
const value = fieldNames.value.value;
|
||||
const children = fieldNames.value.children;
|
||||
const searchEl = props.column.search?.el;
|
||||
let searchProps = props.column.search?.props ?? {};
|
||||
if (searchEl === "tree-select") {
|
||||
searchProps = { ...searchProps, props: { ...searchProps, label, children }, nodeKey: value };
|
||||
}
|
||||
if (searchEl === "cascader") {
|
||||
searchProps = { ...searchProps, props: { ...searchProps, label, value, children } };
|
||||
}
|
||||
return searchProps;
|
||||
});
|
||||
|
||||
// 处理默认 placeholder
|
||||
const placeholder = computed(() => {
|
||||
const search = props.column.search;
|
||||
if (["datetimerange", "daterange", "monthrange"].includes(search?.props?.type) || search?.props?.isRange) {
|
||||
return {
|
||||
rangeSeparator: search?.props?.rangeSeparator ?? "至",
|
||||
startPlaceholder: search?.props?.startPlaceholder ?? "开始时间",
|
||||
endPlaceholder: search?.props?.endPlaceholder ?? "结束时间"
|
||||
};
|
||||
}
|
||||
const placeholder = search?.props?.placeholder ?? (search?.el?.includes("input") ? "请输入" : "请选择");
|
||||
return { placeholder };
|
||||
});
|
||||
|
||||
// 是否有清除按钮 (当搜索项有默认值时,清除按钮不显示)
|
||||
const clearable = computed(() => {
|
||||
const search = props.column.search;
|
||||
return search?.props?.clearable ?? (search?.defaultValue == null || search?.defaultValue == undefined);
|
||||
});
|
||||
</script>
|
||||
96
frontend/src/components/SearchForm/index.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div v-if='columns.length' class='card table-search'>
|
||||
<el-form ref='formRef' :model='searchParam'>
|
||||
<Grid ref='gridRef' :collapsed='collapsed' :gap='[20, 0]' :cols='searchCol'>
|
||||
<GridItem v-for='(item, index) in columns' :key='item.prop' v-bind='getResponsive(item)' :index='index'>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<el-space :size='4'>
|
||||
<span>{{ `${item.search?.label ?? item.label}` }}</span>
|
||||
<el-tooltip v-if='item.search?.tooltip' effect='dark' :content='item.search?.tooltip' placement='top'>
|
||||
<i :class="'iconfont icon-yiwen'"></i>
|
||||
</el-tooltip>
|
||||
</el-space>
|
||||
<span> :</span>
|
||||
</template>
|
||||
<SearchFormItem :column='item' :search-param='searchParam' />
|
||||
</el-form-item>
|
||||
</GridItem>
|
||||
<GridItem suffix>
|
||||
<div class='operation'>
|
||||
<el-button type='primary' :icon='Search' @click='search'> 搜索</el-button>
|
||||
<el-button :icon='Delete' @click='reset'> 重置</el-button>
|
||||
<el-button v-if='showCollapse' type='primary' link class='search-isOpen' @click='collapsed = !collapsed'>
|
||||
{{ collapsed ? '展开' : '合并' }}
|
||||
<el-icon class='el-icon--right'>
|
||||
<component :is='collapsed ? ArrowDown : ArrowUp'></component>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang='ts' name='SearchForm'>
|
||||
import { ColumnProps } from '@/components/ProTable/interface'
|
||||
import { BreakPoint } from '@/components/Grid/interface'
|
||||
import { Delete, Search, ArrowDown, ArrowUp } from '@element-plus/icons-vue'
|
||||
import SearchFormItem from './components/SearchFormItem.vue'
|
||||
import Grid from '@/components/Grid/index.vue'
|
||||
import GridItem from '@/components/Grid/components/GridItem.vue'
|
||||
|
||||
|
||||
interface ProTableProps {
|
||||
columns?: ColumnProps[]; // 搜索配置列
|
||||
searchParam?: {
|
||||
[key: string]: any
|
||||
}; // 搜索参数
|
||||
searchCol: number | Record<BreakPoint, number>;
|
||||
search: (params: any) => void; // 搜索方法
|
||||
reset: (params: any) => void; // 重置方法
|
||||
}
|
||||
|
||||
// 默认值
|
||||
const props = withDefaults(defineProps<ProTableProps>(), {
|
||||
columns: () => [],
|
||||
searchParam: () => ({}),
|
||||
})
|
||||
|
||||
// 获取响应式设置
|
||||
const getResponsive = (item: ColumnProps) => {
|
||||
return {
|
||||
span: item.search?.span,
|
||||
offset: item.search?.offset ?? 0,
|
||||
xs: item.search?.xs,
|
||||
sm: item.search?.sm,
|
||||
md: item.search?.md,
|
||||
lg: item.search?.lg,
|
||||
xl: item.search?.xl,
|
||||
}
|
||||
}
|
||||
|
||||
// 是否默认折叠搜索项
|
||||
const collapsed = ref(true)
|
||||
|
||||
// 获取响应式断点
|
||||
const gridRef = ref()
|
||||
const breakPoint = computed<BreakPoint>(() => gridRef.value?.breakPoint)
|
||||
|
||||
// 判断是否显示 展开/合并 按钮
|
||||
const showCollapse = computed(() => {
|
||||
let show = false
|
||||
props.columns.reduce((prev, current) => {
|
||||
prev +=
|
||||
(current.search![breakPoint.value]?.span ?? current.search?.span ?? 1) +
|
||||
(current.search![breakPoint.value]?.offset ?? current.search?.offset ?? 0)
|
||||
if (typeof props.searchCol !== 'number') {
|
||||
if (prev >= props.searchCol[breakPoint.value]) show = true
|
||||
} else {
|
||||
if (prev >= props.searchCol) show = true
|
||||
}
|
||||
return prev
|
||||
}, 0)
|
||||
return show
|
||||
})
|
||||
</script>
|
||||
39
frontend/src/components/SelectIcon/index.scss
Normal file
@@ -0,0 +1,39 @@
|
||||
.icon-box {
|
||||
width: 100%;
|
||||
.el-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 25px 20px 20px;
|
||||
.el-input {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.icon-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 115px);
|
||||
justify-content: space-evenly;
|
||||
max-height: 70vh;
|
||||
.icon-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 42px;
|
||||
padding: 20px 30px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
&:hover {
|
||||
transform: scale(1.3);
|
||||
}
|
||||
span {
|
||||
margin-top: 5px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
90
frontend/src/components/SelectIcon/index.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div class="icon-box">
|
||||
<el-input
|
||||
ref="inputRef"
|
||||
v-model="valueIcon"
|
||||
v-bind="$attrs"
|
||||
:placeholder="placeholder"
|
||||
:clearable="clearable"
|
||||
@clear="clearIcon"
|
||||
@click="openDialog"
|
||||
>
|
||||
<template #append>
|
||||
<el-button :icon="customIcons[iconValue]" />
|
||||
</template>
|
||||
</el-input>
|
||||
<el-dialog v-model="dialogVisible" :title="placeholder" top="5%" width="30%">
|
||||
<el-input v-model="inputValue" placeholder="搜索图标" size="large" :prefix-icon="Icons.Search" />
|
||||
<el-scrollbar v-if="Object.keys(iconsList).length">
|
||||
<div class="icon-list">
|
||||
<div v-for="item in iconsList" :key="item" class="icon-item" @click="selectIcon(item)">
|
||||
<component :is="item"></component>
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<el-empty v-else description="未搜索到您要找的图标~" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="SelectIcon">
|
||||
import * as Icons from '@element-plus/icons-vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
interface SelectIconProps {
|
||||
iconValue: string | undefined
|
||||
title?: string
|
||||
clearable?: boolean
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<SelectIconProps>(), {
|
||||
iconValue: '',
|
||||
title: '请选择图标',
|
||||
clearable: true,
|
||||
placeholder: '请选择图标'
|
||||
})
|
||||
|
||||
// 重新接收一下,防止打包后 clearable 报错
|
||||
const valueIcon = ref(props.iconValue)
|
||||
|
||||
// open Dialog
|
||||
const dialogVisible = ref(false)
|
||||
const openDialog = () => (dialogVisible.value = true)
|
||||
|
||||
// 选择图标(触发更新父组件数据)
|
||||
const emit = defineEmits<{
|
||||
'update:iconValue': [value: string]
|
||||
}>()
|
||||
const selectIcon = (item: any) => {
|
||||
dialogVisible.value = false
|
||||
valueIcon.value = item.name
|
||||
emit('update:iconValue', item.name)
|
||||
setTimeout(() => inputRef.value.blur(), 0)
|
||||
}
|
||||
|
||||
// 清空图标
|
||||
const inputRef = ref()
|
||||
const clearIcon = () => {
|
||||
valueIcon.value = ''
|
||||
emit('update:iconValue', '')
|
||||
setTimeout(() => inputRef.value.blur(), 0)
|
||||
}
|
||||
|
||||
// 监听搜索框值
|
||||
const inputValue = ref('')
|
||||
const customIcons: { [key: string]: any } = Icons
|
||||
const iconsList = computed((): { [key: string]: any } => {
|
||||
if (!inputValue.value) return Icons
|
||||
let result: { [key: string]: any } = {}
|
||||
for (const key in customIcons) {
|
||||
if (key.toLowerCase().indexOf(inputValue.value.toLowerCase()) > -1) result[key] = customIcons[key]
|
||||
}
|
||||
return result
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use './index.scss';
|
||||
</style>
|
||||
2
frontend/src/components/StaticExtend/SvgIcon/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import SvgIcon from './src/SvgIcon.vue'
|
||||
export { SvgIcon }
|
||||
88
frontend/src/components/StaticExtend/SvgIcon/src/SvgIcon.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* @name SvgIcon
|
||||
* @description svg图标组件
|
||||
* 支持定义名称、颜色、大小、旋转
|
||||
* @example <SvgIcon name="icon-name" color="#fff" size="20" spin />
|
||||
*/
|
||||
<template>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
:class="['svg-icon', spin && 'svg-icon-spin']"
|
||||
:style="getStyle"
|
||||
>
|
||||
<use :xlink:href="symbolId" :fill="color" />
|
||||
</svg>
|
||||
</template>
|
||||
<script setup lang='ts'>
|
||||
defineOptions({
|
||||
name: 'SvgIcon',
|
||||
})
|
||||
import type { CSSProperties } from 'vue'
|
||||
// 定义组件对外暴露的props
|
||||
const props = defineProps({
|
||||
prefix: {
|
||||
type: String,
|
||||
default: 'icon',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: 20,
|
||||
},
|
||||
spin: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
//计算属性获取symbolId
|
||||
const symbolId = computed(() => {
|
||||
return `#${props.prefix}-${props.name}`
|
||||
})
|
||||
|
||||
//计算属性获取svg样式
|
||||
const getStyle = computed((): CSSProperties => {
|
||||
const { size } = props
|
||||
let s = `${size}`
|
||||
// 确保size为px单位
|
||||
s = `${s.replace('px', '')}px`
|
||||
return {
|
||||
width: s,
|
||||
height: s,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
.svg-icon {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.svg-icon-spin {
|
||||
animation: loadingCircle 1.2s infinite linear;
|
||||
}
|
||||
|
||||
/* 旋转动画 */
|
||||
@keyframes loadingCircle {
|
||||
0% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
12
frontend/src/components/SwitchDark/index.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<el-switch v-model="globalStore.isDark" inline-prompt :active-icon="Sunny" :inactive-icon="Moon" @change="switchDark" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="SwitchDark">
|
||||
import { useTheme } from "@/hooks/useTheme";
|
||||
import { useGlobalStore } from "@/stores/modules/global";
|
||||
import { Sunny, Moon } from "@element-plus/icons-vue";
|
||||
|
||||
const { switchDark } = useTheme();
|
||||
const globalStore = useGlobalStore();
|
||||
</script>
|
||||
42
frontend/src/components/TimeControl/index.scss
Normal file
@@ -0,0 +1,42 @@
|
||||
/* 添加样式 */
|
||||
.time-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.select {
|
||||
margin-right: 10px; /* 下拉框右侧间距 */
|
||||
width: 90px; /* 下拉框宽度 */
|
||||
}
|
||||
|
||||
.date-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 10px; /* 日期选择器右侧间距 */
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.date-picker {
|
||||
margin-right: 10px; /* 日期选择器之间的间距 */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.triangle-button {
|
||||
margin: 0 2px; /* 设置左右间距 */
|
||||
}
|
||||
|
||||
.left_triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 10px solid transparent; /* 上边透明 */
|
||||
border-bottom: 10px solid transparent; /* 下边透明 */
|
||||
border-right: 15px solid white; /* 左边为白色 */
|
||||
}
|
||||
|
||||
.right_triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 10px solid transparent; /* 上边透明 */
|
||||
border-bottom: 10px solid transparent; /* 下边透明 */
|
||||
border-left: 15px solid white; /* 左边为白色 */
|
||||
}
|
||||
292
frontend/src/components/TimeControl/index.vue
Normal file
@@ -0,0 +1,292 @@
|
||||
<template>
|
||||
<div class="time-control">
|
||||
<el-select class="select" v-model="timeUnit" placeholder="选择时间单位" @change="handleChange">
|
||||
<!-- 采用 v-for 动态渲染 -->
|
||||
<el-option v-for="unit in timeUnits" :key="unit.value" :label="unit.label" :value="unit.value"></el-option>
|
||||
</el-select>
|
||||
|
||||
<!-- 禁用时间选择器 -->
|
||||
<div class="date-display">
|
||||
<el-date-picker
|
||||
class="date-picker"
|
||||
v-model="startDate"
|
||||
type="date"
|
||||
placeholder="起始时间"
|
||||
@change="emitDateChange"
|
||||
:disabled-date="disableStartDate"
|
||||
:readonly="timeUnit != '自定义'"
|
||||
></el-date-picker>
|
||||
<el-text>~</el-text>
|
||||
<el-date-picker
|
||||
class="date-picker"
|
||||
v-model="endDate"
|
||||
type="date"
|
||||
placeholder="结束时间"
|
||||
@change="emitDateChange"
|
||||
:disabled-date="disableEndDate"
|
||||
:readonly="timeUnit !== '自定义'"
|
||||
></el-date-picker>
|
||||
</div>
|
||||
<div class="date-display" v-if="timeUnit !== '自定义'">
|
||||
<el-button
|
||||
style="width: 10px"
|
||||
class="triangle-button"
|
||||
type="primary"
|
||||
@click="prevPeriod"
|
||||
@change="emitDateChange"
|
||||
>
|
||||
<div class="left_triangle"></div>
|
||||
</el-button>
|
||||
<el-button class="triangle-button" type="primary" @click="goToCurrent">当前</el-button>
|
||||
<el-button
|
||||
style="width: 10px"
|
||||
class="triangle-button"
|
||||
type="primary"
|
||||
@click="nextPeriod"
|
||||
:disabled="isNextDisabled"
|
||||
>
|
||||
<div class="right_triangle"></div>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
// 定义时间单位的类型
|
||||
interface TimeUnit {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
// 定义组件的props,包含包括和排除的时间单位
|
||||
const props = defineProps({
|
||||
include: {
|
||||
type: Array as () => string[],
|
||||
default: () => ['日', '周', '月', '季度', '年', '自定义']
|
||||
},
|
||||
exclude: {
|
||||
type: Array as () => string[],
|
||||
default: () => []
|
||||
},
|
||||
default: {
|
||||
type: String,
|
||||
default: '月'
|
||||
}
|
||||
})
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits<{
|
||||
(e: 'update-dates', startDate: string, endDate: string): void
|
||||
}>()
|
||||
const timeUnit = ref<string>(props.default) // 默认选择
|
||||
const startDate = ref<Date>(new Date()) // 起始日期
|
||||
const endDate = ref<Date>(new Date()) // 结束日期
|
||||
const isNextDisabled = ref<boolean>(false) // 控制下一周期按钮的禁用状态
|
||||
const today = ref<Date>(new Date()) // 当前日期
|
||||
// 过滤出可用的时间单位
|
||||
const timeUnits = ref<TimeUnit[]>(
|
||||
props.include
|
||||
.filter(unit => !props.exclude.includes(unit))
|
||||
.map(unit => ({
|
||||
label: unit,
|
||||
value: unit
|
||||
}))
|
||||
)
|
||||
|
||||
// 发出日期变化事件
|
||||
const emitDateChange = () => {
|
||||
emit('update-dates', formatDate(startDate.value), formatDate(endDate.value))
|
||||
}
|
||||
|
||||
// 在组件挂载时更新日期范围
|
||||
onMounted(() => {
|
||||
updateDateRange()
|
||||
})
|
||||
const handleChange = (unit: string) => {
|
||||
// 根据选择的时间单位处理日期变化
|
||||
if (unit !== '自定义') {
|
||||
updateDateRange()
|
||||
} else {
|
||||
// 自定义选项逻辑
|
||||
startDate.value = new Date(new Date().setDate(new Date().getDate() - 1))
|
||||
endDate.value = new Date()
|
||||
}
|
||||
timeUnit.value = unit
|
||||
|
||||
// 确保开始时间和结束时间不为空
|
||||
if (!startDate.value) {
|
||||
startDate.value = new Date()
|
||||
}
|
||||
if (!endDate.value) {
|
||||
endDate.value = new Date()
|
||||
}
|
||||
|
||||
emitDateChange() // 变化时也发出更新事件
|
||||
updateNextButtonStatus()
|
||||
}
|
||||
const updateDateRange = () => {
|
||||
// 根据选择的时间单位计算起始和结束日期
|
||||
if (timeUnit.value === '日') {
|
||||
startDate.value = today.value
|
||||
endDate.value = today.value
|
||||
} else if (timeUnit.value === '周') {
|
||||
startDate.value = getStartOfWeek(today.value)
|
||||
endDate.value = getEndOfWeek(today.value)
|
||||
} else if (timeUnit.value === '月') {
|
||||
// 获取本月的开始和结束日期
|
||||
startDate.value = new Date(today.value.getFullYear(), today.value.getMonth(), 1)
|
||||
endDate.value = new Date(today.value.getFullYear(), today.value.getMonth() + 1, 0)
|
||||
|
||||
// // 确保结束日期不超过今天
|
||||
// if (endDate.value > today.value) {
|
||||
// endDate.value = new Date(today.value);
|
||||
// endDate.value.setHours(23, 59, 59, 999); // 设置结束时间为今天的23:59:59.999
|
||||
// }
|
||||
} else if (timeUnit.value === '季度') {
|
||||
const quarter = Math.floor(today.value.getMonth() / 3)
|
||||
startDate.value = new Date(today.value.getFullYear(), quarter * 3, 1)
|
||||
endDate.value = new Date(today.value.getFullYear(), quarter * 3 + 3, 0)
|
||||
|
||||
// // 确保结束日期不超过今天
|
||||
// if (endDate.value > today.value) {
|
||||
// endDate.value = new Date(today.value);
|
||||
// endDate.value.setHours(23, 59, 59, 999); // 设置结束时间为今天的23:59:59.999
|
||||
// }
|
||||
} else if (timeUnit.value === '年') {
|
||||
startDate.value = new Date(today.value.getFullYear(), 0, 1)
|
||||
endDate.value = new Date(today.value.getFullYear(), 11, 31)
|
||||
|
||||
// // 确保结束日期不超过今天
|
||||
// if (endDate.value > today.value) {
|
||||
|
||||
// endDate.value = new Date(today.value);
|
||||
// endDate.value.setHours(23, 59, 59, 999); // 设置结束时间为今天的23:59:59.999
|
||||
// }
|
||||
}
|
||||
// 确保开始时间和结束时间不为空
|
||||
if (!startDate.value) {
|
||||
startDate.value = new Date()
|
||||
}
|
||||
if (!endDate.value) {
|
||||
endDate.value = new Date()
|
||||
}
|
||||
|
||||
updateNextButtonStatus()
|
||||
}
|
||||
const getStartOfWeek = (date: Date) => {
|
||||
const startOfWeek = new Date(date)
|
||||
const day = startOfWeek.getDay()
|
||||
const diff = day === 0 ? -6 : 1 - day // 星期天的情况
|
||||
startOfWeek.setDate(startOfWeek.getDate() + diff)
|
||||
return startOfWeek
|
||||
}
|
||||
const getEndOfWeek = (date: Date) => {
|
||||
const endOfWeek = new Date(date)
|
||||
const day = endOfWeek.getDay()
|
||||
const diff = day === 0 ? 0 : 7 - day // 星期天的情况
|
||||
endOfWeek.setDate(endOfWeek.getDate() + diff)
|
||||
|
||||
// 获取今天的日期
|
||||
const today = new Date()
|
||||
today.setHours(23, 59, 59, 999) // 设置今天的结束时间(23:59:59.999)
|
||||
|
||||
// 返回不超过今天的结束时间
|
||||
//return endOfWeek > today ? today : endOfWeek;
|
||||
return endOfWeek
|
||||
}
|
||||
const prevPeriod = () => {
|
||||
const prevStartDate = new Date(startDate.value)
|
||||
const prevEndDate = new Date(endDate.value)
|
||||
|
||||
if (timeUnit.value === '日') {
|
||||
prevStartDate.setDate(prevStartDate.getDate() - 1)
|
||||
prevEndDate.setDate(prevEndDate.getDate() - 1)
|
||||
} else if (timeUnit.value === '周') {
|
||||
prevStartDate.setDate(prevStartDate.getDate() - 7)
|
||||
prevEndDate.setDate(prevEndDate.getDate() - 7)
|
||||
} else if (timeUnit.value === '月') {
|
||||
prevStartDate.setMonth(prevStartDate.getMonth() - 1)
|
||||
prevEndDate.setMonth(prevEndDate.getMonth() - 1)
|
||||
} else if (timeUnit.value === '季度') {
|
||||
prevStartDate.setMonth(prevStartDate.getMonth() - 3)
|
||||
prevEndDate.setMonth(prevEndDate.getMonth() - 3)
|
||||
} else if (timeUnit.value === '年') {
|
||||
prevStartDate.setFullYear(prevStartDate.getFullYear() - 1)
|
||||
prevEndDate.setFullYear(prevEndDate.getFullYear() - 1)
|
||||
}
|
||||
|
||||
startDate.value = prevStartDate
|
||||
endDate.value = prevEndDate
|
||||
updateNextButtonStatus()
|
||||
}
|
||||
const goToCurrent = () => {
|
||||
if (timeUnit.value !== '自定义') {
|
||||
updateDateRange() // 更新为当前选择时间单位的时间范围
|
||||
}
|
||||
}
|
||||
const nextPeriod = () => {
|
||||
const nextStartDate = new Date(startDate.value)
|
||||
const nextEndDate = new Date(endDate.value)
|
||||
|
||||
if (timeUnit.value === '日') {
|
||||
nextStartDate.setDate(nextStartDate.getDate() + 1)
|
||||
nextEndDate.setDate(nextEndDate.getDate() + 1)
|
||||
} else if (timeUnit.value === '周') {
|
||||
nextStartDate.setDate(nextStartDate.getDate() + 7)
|
||||
nextEndDate.setDate(nextEndDate.getDate() + 7)
|
||||
} else if (timeUnit.value === '月') {
|
||||
nextStartDate.setMonth(nextStartDate.getMonth() + 1)
|
||||
nextEndDate.setMonth(nextEndDate.getMonth() + 1)
|
||||
} else if (timeUnit.value === '季度') {
|
||||
nextStartDate.setMonth(nextStartDate.getMonth() + 3)
|
||||
nextEndDate.setMonth(nextStartDate.getMonth() + 3)
|
||||
} else if (timeUnit.value === '年') {
|
||||
nextStartDate.setFullYear(nextStartDate.getFullYear() + 1)
|
||||
nextEndDate.setFullYear(nextEndDate.getFullYear() + 1)
|
||||
}
|
||||
|
||||
startDate.value = nextStartDate
|
||||
endDate.value = nextEndDate
|
||||
updateNextButtonStatus()
|
||||
}
|
||||
const updateNextButtonStatus = () => {
|
||||
// 更新下一个按钮的禁用状态
|
||||
const maxDate = new Date() // 假设最新日期为今天
|
||||
// 将 maxDate 设置为当天的开始时间
|
||||
maxDate.setHours(0, 0, 0, 0)
|
||||
// 将 endDate 设置为当天的开始时间并进行比较
|
||||
const endDateAdjusted = new Date(endDate.value)
|
||||
endDateAdjusted.setHours(0, 0, 0, 0)
|
||||
// 仅比较日期部分
|
||||
isNextDisabled.value = endDateAdjusted >= maxDate
|
||||
emitDateChange() // 变化时也发出更新事件
|
||||
}
|
||||
|
||||
// 限制开始日期不能选择超过当前日期
|
||||
const disableStartDate = (date: Date) => {
|
||||
return date > today.value
|
||||
}
|
||||
// 限制结束日期不能超过当前日期且必须大于开始日期
|
||||
const disableEndDate = (date: Date) => {
|
||||
if (timeUnit.value !== '自定义') return false // 如果不是自定义时间单位,则不限制
|
||||
const start = new Date(startDate.value)
|
||||
return date > today.value || (start && date <= start)
|
||||
}
|
||||
|
||||
// 格式化日期yyyy-mm-dd
|
||||
function formatDate(date: Date | null): string {
|
||||
if (!date) {
|
||||
return ''
|
||||
}
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use './index.scss';
|
||||
</style>
|
||||
286
frontend/src/components/echarts/line/index.vue
Normal file
@@ -0,0 +1,286 @@
|
||||
<template>
|
||||
<div class="chart">
|
||||
<div ref="chartRef" class="my-chart" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
// import echarts from './echarts'
|
||||
import * as echarts from 'echarts' // 全引入
|
||||
// import 'echarts-gl'
|
||||
// import 'echarts-liquidfill'
|
||||
// import 'echarts/lib/component/dataZoom'
|
||||
|
||||
const color = [
|
||||
'var(--el-color-primary)',
|
||||
'#07CCCA',
|
||||
'#00BFF5',
|
||||
'#FFBF00',
|
||||
'#77DA63',
|
||||
'#D5FF6B',
|
||||
'#Ff6600',
|
||||
'#FF9100',
|
||||
'#5B6E96',
|
||||
'#66FFCC',
|
||||
'#B3B3B3'
|
||||
]
|
||||
|
||||
const chartRef = ref<HTMLDivElement>()
|
||||
|
||||
const props = defineProps(['options', 'isInterVal', 'pieInterVal'])
|
||||
let chart: echarts.ECharts | any = null
|
||||
const resizeHandler = () => {
|
||||
// 不在视野中的时候不进行resize
|
||||
if (!chartRef.value) return
|
||||
if (chartRef.value.offsetHeight == 0) return
|
||||
chart.getZr().painter.getViewportRoot().style.display = 'none'
|
||||
requestAnimationFrame(() => {
|
||||
chart.resize()
|
||||
chart.getZr().painter.getViewportRoot().style.display = ''
|
||||
})
|
||||
}
|
||||
const initChart = () => {
|
||||
|
||||
if (!props.isInterVal && !props.pieInterVal) {
|
||||
chart?.dispose()
|
||||
}
|
||||
// chart?.dispose()
|
||||
chart = echarts.init(chartRef.value as HTMLDivElement)
|
||||
|
||||
const options = {
|
||||
title: {
|
||||
left: 'center',
|
||||
// textStyle: {
|
||||
color: '#000',
|
||||
fontSize: 18,
|
||||
// },
|
||||
...(props.options?.title || null)
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
label: {
|
||||
color: '#fff',
|
||||
fontSize: 16
|
||||
}
|
||||
},
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontStyle: 'normal',
|
||||
opacity: 0.35,
|
||||
fontSize: 14
|
||||
},
|
||||
backgroundColor: 'rgba(0,0,0,0.55)',
|
||||
borderWidth: 0,
|
||||
confine: true,
|
||||
...(props.options?.tooltip || null)
|
||||
},
|
||||
|
||||
legend: {
|
||||
right: 20,
|
||||
top: 0,
|
||||
itemGap: 10,
|
||||
itemStyle: {},
|
||||
// textStyle: {
|
||||
fontSize: 12,
|
||||
padding: [2, 0, 0, 0], //[上、右、下、左]
|
||||
// },
|
||||
itemWidth: 15,
|
||||
itemHeight: 10,
|
||||
...(props.options?.legend || null)
|
||||
},
|
||||
grid: {
|
||||
top: '60px',
|
||||
left: '30px',
|
||||
right: '70px',
|
||||
bottom: props.options?.options?.dataZoom === null ? '10px' : '40px',
|
||||
containLabel: true,
|
||||
...(props.options?.grid || null)
|
||||
},
|
||||
xAxis: props.options?.xAxis ? handlerXAxis() : null,
|
||||
yAxis: props.options?.yAxis ? handlerYAxis() : null,
|
||||
dataZoom: props.options?.dataZoom || [
|
||||
{
|
||||
type: 'inside',
|
||||
height: 13,
|
||||
start: 0,
|
||||
bottom: '20px',
|
||||
end: 100
|
||||
},
|
||||
{
|
||||
start: 0,
|
||||
height: 13,
|
||||
bottom: '20px',
|
||||
end: 100
|
||||
}
|
||||
],
|
||||
color: props.options?.color || color,
|
||||
series: props.options?.series,
|
||||
...props.options?.options
|
||||
}
|
||||
// console.log(options.series,"获取x轴");
|
||||
handlerBar(options)
|
||||
|
||||
// 处理柱状图
|
||||
chart.setOption(options, true)
|
||||
|
||||
setTimeout(() => {
|
||||
chart.resize()
|
||||
}, 0)
|
||||
}
|
||||
const handlerBar = (options: any) => {
|
||||
if (Array.isArray(options.series)) {
|
||||
options.series.forEach((item: any) => {
|
||||
if (item.type === 'bar') {
|
||||
item.barMinHeight = 10
|
||||
item.barMaxWidth = 20
|
||||
item.itemStyle = Object.assign(
|
||||
{
|
||||
color: (params: any) => {
|
||||
if (params.value == 0 || params.value == 3.14159) {
|
||||
return '#ccc'
|
||||
} else {
|
||||
return props.options?.color
|
||||
? props.options?.color[params.seriesIndex]
|
||||
: color[params.seriesIndex]
|
||||
}
|
||||
}
|
||||
},
|
||||
item.itemStyle
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
const handlerYAxis = () => {
|
||||
let temp = {
|
||||
type: 'value',
|
||||
nameGap: 15,
|
||||
nameTextStyle: {
|
||||
color: '#000'
|
||||
},
|
||||
splitNumber: 5,
|
||||
minInterval: 1,
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#000'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#000',
|
||||
fontSize: 14,
|
||||
formatter: function (value) {
|
||||
return value.toFixed(0) // 格式化显示为一位小数
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
// 使用深浅的间隔色
|
||||
color: ['#ccc'],
|
||||
type: 'dashed',
|
||||
opacity: 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
// props.options?.xAxis 是数组还是对象
|
||||
if (Array.isArray(props.options?.yAxis)) {
|
||||
return props.options?.yAxis.map((item: any) => {
|
||||
return {
|
||||
...temp,
|
||||
...item
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return {
|
||||
...temp,
|
||||
...props.options?.yAxis
|
||||
}
|
||||
}
|
||||
}
|
||||
const handlerXAxis = () => {
|
||||
let temp = {
|
||||
type: 'category',
|
||||
axisTick: { show: false },
|
||||
nameTextStyle: {
|
||||
color: '#000'
|
||||
},
|
||||
axisLine: {
|
||||
// lineStyle: {
|
||||
color: '#000'
|
||||
// }
|
||||
},
|
||||
axisLabel: {
|
||||
// textStyle: {
|
||||
fontFamily: 'dinproRegular',
|
||||
color: '#000',
|
||||
fontSize: '12'
|
||||
// }
|
||||
}
|
||||
// boundaryGap: false,
|
||||
}
|
||||
// props.options?.xAxis 是数组还是对象
|
||||
if (Array.isArray(props.options?.xAxis)) {
|
||||
return props.options?.xAxis.map((item: any) => {
|
||||
return {
|
||||
...temp,
|
||||
...item
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return {
|
||||
...temp,
|
||||
...props.options?.xAxis
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let throttle: ReturnType<typeof setTimeout>
|
||||
// 动态计算table高度
|
||||
const resizeObserver = new ResizeObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
if (throttle) {
|
||||
clearTimeout(throttle)
|
||||
}
|
||||
throttle = setTimeout(() => {
|
||||
resizeHandler()
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
onMounted(() => {
|
||||
initChart()
|
||||
resizeObserver.observe(chartRef.value!)
|
||||
})
|
||||
defineExpose({ initChart })
|
||||
onBeforeUnmount(() => {
|
||||
resizeObserver.unobserve(chartRef.value!)
|
||||
chart?.dispose()
|
||||
})
|
||||
watch(
|
||||
() => props.options,
|
||||
(newVal, oldVal) => {
|
||||
initChart()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.el-button {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: -60px;
|
||||
}
|
||||
|
||||
.my-chart {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
192
frontend/src/components/echarts/pie/default.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<!-- 默认饼图 -->
|
||||
<template>
|
||||
<div class="pie" ref="chartsRef"></div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import * as echarts from "echarts";
|
||||
import { ref } from "vue";
|
||||
const chartsRef = ref();
|
||||
const props = defineProps({
|
||||
//饼图数据
|
||||
chartsData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
//自定义数据
|
||||
customData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
//legend配置
|
||||
legendData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
|
||||
});
|
||||
const customData: any = ref({}),
|
||||
legendData: any = ref({}),
|
||||
chart: any = ref();
|
||||
|
||||
const labelIsShow = ref(true)//引导线台数和鼠标点击饼图是否弹出提示显示
|
||||
const init = () => {
|
||||
|
||||
|
||||
customData.value = {
|
||||
title: "", //标题
|
||||
textAlign: "left", //标题位置可选属性left 可选属性值 left,right,center
|
||||
ratio: true, //是否显示数值占比,默认不显示
|
||||
isRing: false, //是否环形图
|
||||
isRadius: false, //是否圆角
|
||||
isSpace: false, //是否显示间隔
|
||||
isLabelLine: true, //是否显示引导线
|
||||
titleFontSize: '14px', //标题字体大小
|
||||
|
||||
...props.customData,
|
||||
|
||||
};
|
||||
legendData.value = {
|
||||
icon: "roundRect", // 图例项的icon,类型包括 circle(圆形),rect(正方形),//roundRect(圆角正方形),triangle(三角形),diamond(菱形),//pin(大头针行),arrow(箭头形),none(无图例项的icon)
|
||||
orient: "vertical", //图例排列方向
|
||||
left: "right", //可选属性left,right,top,bottom,可选属性值 left,right,top,bottom,px,百分比,数值,
|
||||
itemGap: 10, // 设置图例项之间的间隔为20
|
||||
...props.legendData,
|
||||
};
|
||||
chart.value = chartsRef.value && echarts.init(chartsRef.value);
|
||||
var option = {
|
||||
title: {
|
||||
text: customData.value.title,
|
||||
left: customData.value.textAlign,
|
||||
textStyle: {
|
||||
fontSize: customData.value.titleFontSize, // 使用 titleFontSize 属性
|
||||
},
|
||||
},
|
||||
legend:legendData.value,
|
||||
// legend: {
|
||||
// icon: legendData.value.icon, // 图例项的icon,类型包括 circle(圆形),rect(正方形),//roundRect(圆角正方形),triangle(三角形),diamond(菱形),//pin(大头针行),arrow(箭头形),none(无图例项的icon)
|
||||
// orient: "vertical", //图例排列方向
|
||||
// left: legendData.value.left, //可选属性left,right,top,bottom,可选属性值 left,right,top,bottom,px,百分比,数值,
|
||||
// itemGap: 1, // 设置图例项之间的间隔为20
|
||||
// formatter: function (name) {
|
||||
|
||||
// const item = props.chartsData.filter(item=>item.name==name)
|
||||
// console.log(item)
|
||||
// if(item)
|
||||
// return item[0].value;
|
||||
|
||||
// },
|
||||
// },
|
||||
tooltip: {
|
||||
show: labelIsShow.value,
|
||||
trigger: "item",
|
||||
formatter: customData.value.ratio ? `{b} : {c} ({d}%)` : "{b} :{c} ",
|
||||
borderWidth: 1,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: "pie",
|
||||
radius: customData.value.isRing ? ["55", "75"] : "80%",
|
||||
data: props.chartsData,
|
||||
formatter: function (name: any) {
|
||||
const item = props.chartsData.filter(item=>item.name==name)
|
||||
//console.log(item)
|
||||
if(item)
|
||||
return item[0].value;
|
||||
},
|
||||
center: ["55%", "55%"], // 设置饼图的中心位置
|
||||
// padAngle: 2,
|
||||
minAngle: 15, //最小角度
|
||||
startAngle: 270, //起始角度
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: "rgba(0, 0, 0, 0.5)",
|
||||
},
|
||||
},
|
||||
label: {
|
||||
normal: {
|
||||
show: labelIsShow.value,
|
||||
position: "outside",
|
||||
textStyle: {
|
||||
//color: "#fff",
|
||||
fontSize: 12,
|
||||
},
|
||||
formatter: function (data) {
|
||||
return labelIsShow.value ? data.value + '台' : '';
|
||||
}
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
borderRadius: customData.value.isRadius ? 10 : 0,
|
||||
borderColor: customData.value.isSpace ? "#fff" : "",
|
||||
borderWidth: customData.value.isSpace ? 2 : 0,
|
||||
},
|
||||
labelLine: {
|
||||
show: customData.value.isLabelLine,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
option && chart.value && chart.value.setOption(option);
|
||||
setTimeout(() => {
|
||||
chart.value.resize();
|
||||
}, 0);
|
||||
};
|
||||
const reSize = (widthValue: number,heightValue: number,silentValue: boolean) => {
|
||||
if (chart.value) {
|
||||
chart.value.resize({
|
||||
width: widthValue,
|
||||
height: heightValue,
|
||||
silent: silentValue,
|
||||
});
|
||||
}
|
||||
};
|
||||
const resizeCharts = () => {
|
||||
//console.log(chart.value,111111);
|
||||
|
||||
if (chart.value) {
|
||||
chart.value.resize();
|
||||
}
|
||||
};
|
||||
window.addEventListener("resize", resizeCharts);
|
||||
onUnmounted(() => {
|
||||
if (chart.value) {
|
||||
chart.value.resize();
|
||||
}
|
||||
window.removeEventListener("resize", resizeCharts);
|
||||
if (chart.value != null && chart.value.dispose) {
|
||||
chart.value.dispose(); // 销毁图表
|
||||
}
|
||||
});
|
||||
watch(
|
||||
() => props.chartsData,
|
||||
(val, oldVal) => {
|
||||
if (val) {
|
||||
const item = props.chartsData.find(item => item.value === 0);
|
||||
if(item != undefined){
|
||||
labelIsShow.value = false;
|
||||
}else{
|
||||
labelIsShow.value = true;
|
||||
}
|
||||
init();
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
defineExpose({ init,reSize });
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.pie {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
19
frontend/src/components/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { App, Component } from 'vue'
|
||||
|
||||
// 当组件很多的时候,可以使用
|
||||
import { SvgIcon } from '@/components/StaticExtend/SvgIcon'
|
||||
|
||||
// 这个地方将合并到对象中
|
||||
const Components: {
|
||||
[propName: string]: Component
|
||||
} = { SvgIcon }
|
||||
|
||||
// 批量注册全局组件
|
||||
export default {
|
||||
install: (app: App) => {
|
||||
Object.keys(Components).forEach((key) => {
|
||||
app.component(key, Components[key])
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
21
frontend/src/config/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// ? 全局默认配置项
|
||||
|
||||
// 首页地址(默认)
|
||||
export const HOME_URL: string = "/home";
|
||||
|
||||
// 登录页地址(默认)
|
||||
export const LOGIN_URL: string = "/login";
|
||||
|
||||
// 默认主题颜色
|
||||
export const DEFAULT_PRIMARY: string = "#526ADE";
|
||||
|
||||
// 路由白名单地址(本地存在的路由 staticRouter.ts 中)
|
||||
export const ROUTER_WHITE_LIST: string[] = ["/500"];
|
||||
|
||||
// 高德地图 key
|
||||
export const AMAP_MAP_KEY: string = "";
|
||||
|
||||
// 百度地图 key
|
||||
export const BAIDU_MAP_KEY: string = "";
|
||||
|
||||
|
||||
12
frontend/src/config/nprogress.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import NProgress from "nprogress";
|
||||
import "nprogress/nprogress.css";
|
||||
|
||||
NProgress.configure({
|
||||
easing: "ease", // 动画方式
|
||||
speed: 500, // 递增进度条的速度
|
||||
showSpinner: true, // 是否显示加载ico
|
||||
trickleSpeed: 200, // 自动递增间隔
|
||||
minimum: 0.3 // 初始化时的最小百分比
|
||||
});
|
||||
|
||||
export default NProgress;
|
||||
28
frontend/src/directives/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { App, Directive } from "vue";
|
||||
import auth from "./modules/auth";
|
||||
import copy from "./modules/copy";
|
||||
import waterMarker from "./modules/waterMarker";
|
||||
import draggable from "./modules/draggable";
|
||||
import debounce from "./modules/debounce";
|
||||
import throttle from "./modules/throttle";
|
||||
import longpress from "./modules/longpress";
|
||||
|
||||
const directivesList: { [key: string]: Directive } = {
|
||||
auth,
|
||||
copy,
|
||||
waterMarker,
|
||||
draggable,
|
||||
debounce,
|
||||
throttle,
|
||||
longpress
|
||||
};
|
||||
|
||||
const directives = {
|
||||
install: function (app: App<Element>) {
|
||||
Object.keys(directivesList).forEach(key => {
|
||||
app.directive(key, directivesList[key]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default directives;
|
||||
29
frontend/src/directives/modules/auth.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* v-auth
|
||||
* 按钮权限指令
|
||||
*/
|
||||
import { useAuthStore } from '@/stores/modules/auth'
|
||||
import type { Directive, DirectiveBinding } from 'vue'
|
||||
|
||||
const auth: Directive = {
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||
//console.log('binding',binding)
|
||||
const { value, modifiers } = binding
|
||||
let currentPageRoles = []
|
||||
const authStore = useAuthStore()
|
||||
if (modifiers && Object.keys(modifiers).length) {
|
||||
currentPageRoles = authStore.authButtonListGet[Object.keys(modifiers)[0]] ?? []
|
||||
} else {
|
||||
currentPageRoles = authStore.authButtonListGet[authStore.routeName] ?? []
|
||||
}
|
||||
//console.log('currentPageRoles', currentPageRoles)
|
||||
if (value instanceof Array && value.length) {
|
||||
const hasPermission = value.every(item => currentPageRoles.includes(item))
|
||||
if (!hasPermission) el.remove()
|
||||
} else {
|
||||
if (!currentPageRoles.includes(value)) el.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default auth
|
||||
37
frontend/src/directives/modules/copy.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* v-copy
|
||||
* 复制某个值至剪贴板
|
||||
* 接收参数:string类型/Ref<string>类型/Reactive<string>类型
|
||||
*/
|
||||
import type { Directive, DirectiveBinding } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
interface ElType extends HTMLElement {
|
||||
copyData: string | number;
|
||||
__handleClick__: any;
|
||||
}
|
||||
const copy: Directive = {
|
||||
mounted(el: ElType, binding: DirectiveBinding) {
|
||||
el.copyData = binding.value;
|
||||
el.addEventListener("click", handleClick);
|
||||
},
|
||||
updated(el: ElType, binding: DirectiveBinding) {
|
||||
el.copyData = binding.value;
|
||||
},
|
||||
beforeUnmount(el: ElType) {
|
||||
el.removeEventListener("click", el.__handleClick__);
|
||||
}
|
||||
};
|
||||
|
||||
async function handleClick(this: any) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.copyData);
|
||||
} catch (err) {
|
||||
|
||||
}
|
||||
ElMessage({
|
||||
type: "success",
|
||||
message: "复制成功"
|
||||
});
|
||||
}
|
||||
|
||||
export default copy;
|
||||
31
frontend/src/directives/modules/debounce.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* v-debounce
|
||||
* 按钮防抖指令,可自行扩展至input
|
||||
* 接收参数:function类型
|
||||
*/
|
||||
import type { Directive, DirectiveBinding } from "vue";
|
||||
interface ElType extends HTMLElement {
|
||||
__handleClick__: () => any;
|
||||
}
|
||||
const debounce: Directive = {
|
||||
mounted(el: ElType, binding: DirectiveBinding) {
|
||||
if (typeof binding.value !== "function") {
|
||||
throw "callback must be a function";
|
||||
}
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
el.__handleClick__ = function () {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
timer = setTimeout(() => {
|
||||
binding.value();
|
||||
}, 500);
|
||||
};
|
||||
el.addEventListener("click", el.__handleClick__);
|
||||
},
|
||||
beforeUnmount(el: ElType) {
|
||||
el.removeEventListener("click", el.__handleClick__);
|
||||
}
|
||||
};
|
||||
|
||||
export default debounce;
|
||||
49
frontend/src/directives/modules/draggable.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
需求:实现一个拖拽指令,可在父元素区域任意拖拽元素。
|
||||
|
||||
思路:
|
||||
1、设置需要拖拽的元素为absolute,其父元素为relative。
|
||||
2、鼠标按下(onmousedown)时记录目标元素当前的 left 和 top 值。
|
||||
3、鼠标移动(onmousemove)时计算每次移动的横向距离和纵向距离的变化值,并改变元素的 left 和 top 值
|
||||
4、鼠标松开(onmouseup)时完成一次拖拽
|
||||
|
||||
使用:在 Dom 上加上 v-draggable 即可
|
||||
<div class="dialog-model" v-draggable></div>
|
||||
*/
|
||||
import type { Directive } from "vue";
|
||||
interface ElType extends HTMLElement {
|
||||
parentNode: any;
|
||||
}
|
||||
const draggable: Directive = {
|
||||
mounted: function (el: ElType) {
|
||||
el.style.cursor = "move";
|
||||
el.style.position = "absolute";
|
||||
el.onmousedown = function (e) {
|
||||
let disX = e.pageX - el.offsetLeft;
|
||||
let disY = e.pageY - el.offsetTop;
|
||||
document.onmousemove = function (e) {
|
||||
let x = e.pageX - disX;
|
||||
let y = e.pageY - disY;
|
||||
let maxX = el.parentNode.offsetWidth - el.offsetWidth;
|
||||
let maxY = el.parentNode.offsetHeight - el.offsetHeight;
|
||||
if (x < 0) {
|
||||
x = 0;
|
||||
} else if (x > maxX) {
|
||||
x = maxX;
|
||||
}
|
||||
|
||||
if (y < 0) {
|
||||
y = 0;
|
||||
} else if (y > maxY) {
|
||||
y = maxY;
|
||||
}
|
||||
el.style.left = x + "px";
|
||||
el.style.top = y + "px";
|
||||
};
|
||||
document.onmouseup = function () {
|
||||
document.onmousemove = document.onmouseup = null;
|
||||
};
|
||||
};
|
||||
}
|
||||
};
|
||||
export default draggable;
|
||||
49
frontend/src/directives/modules/longpress.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* v-longpress
|
||||
* 长按指令,长按时触发事件
|
||||
*/
|
||||
import type { Directive, DirectiveBinding } from "vue";
|
||||
|
||||
const directive: Directive = {
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||
if (typeof binding.value !== "function") {
|
||||
throw "callback must be a function";
|
||||
}
|
||||
// 定义变量
|
||||
let pressTimer: any = null;
|
||||
// 创建计时器( 2秒后执行函数 )
|
||||
const start = (e: any) => {
|
||||
if (e.button) {
|
||||
if (e.type === "click" && e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (pressTimer === null) {
|
||||
pressTimer = setTimeout(() => {
|
||||
handler(e);
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
// 取消计时器
|
||||
const cancel = () => {
|
||||
if (pressTimer !== null) {
|
||||
clearTimeout(pressTimer);
|
||||
pressTimer = null;
|
||||
}
|
||||
};
|
||||
// 运行函数
|
||||
const handler = (e: MouseEvent | TouchEvent) => {
|
||||
binding.value(e);
|
||||
};
|
||||
// 添加事件监听器
|
||||
el.addEventListener("mousedown", start);
|
||||
el.addEventListener("touchstart", start);
|
||||
// 取消计时器
|
||||
el.addEventListener("click", cancel);
|
||||
el.addEventListener("mouseout", cancel);
|
||||
el.addEventListener("touchend", cancel);
|
||||
el.addEventListener("touchcancel", cancel);
|
||||
}
|
||||
};
|
||||
|
||||
export default directive;
|
||||
41
frontend/src/directives/modules/throttle.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
需求:防止按钮在短时间内被多次点击,使用节流函数限制规定时间内只能点击一次。
|
||||
|
||||
思路:
|
||||
1、第一次点击,立即调用方法并禁用按钮,等延迟结束再次激活按钮
|
||||
2、将需要触发的方法绑定在指令上
|
||||
|
||||
使用:给 Dom 加上 v-throttle 及回调函数即可
|
||||
<button v-throttle="debounceClick">节流提交</button>
|
||||
*/
|
||||
import type { Directive, DirectiveBinding } from "vue";
|
||||
interface ElType extends HTMLElement {
|
||||
__handleClick__: () => any;
|
||||
disabled: boolean;
|
||||
}
|
||||
const throttle: Directive = {
|
||||
mounted(el: ElType, binding: DirectiveBinding) {
|
||||
if (typeof binding.value !== "function") {
|
||||
throw "callback must be a function";
|
||||
}
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
el.__handleClick__ = function () {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
if (!el.disabled) {
|
||||
el.disabled = true;
|
||||
binding.value();
|
||||
timer = setTimeout(() => {
|
||||
el.disabled = false;
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
el.addEventListener("click", el.__handleClick__);
|
||||
},
|
||||
beforeUnmount(el: ElType) {
|
||||
el.removeEventListener("click", el.__handleClick__);
|
||||
}
|
||||
};
|
||||
|
||||
export default throttle;
|
||||
36
frontend/src/directives/modules/waterMarker.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
需求:给整个页面添加背景水印。
|
||||
|
||||
思路:
|
||||
1、使用 canvas 特性生成 base64 格式的图片文件,设置其字体大小,颜色等。
|
||||
2、将其设置为背景图片,从而实现页面或组件水印效果
|
||||
|
||||
使用:设置水印文案,颜色,字体大小即可
|
||||
<div v-waterMarker="{text:'版权所有',textColor:'rgba(180, 180, 180, 0.4)'}"></div>
|
||||
*/
|
||||
|
||||
import type { Directive, DirectiveBinding } from "vue";
|
||||
const addWaterMarker: Directive = (str: string, parentNode: any, font: any, textColor: string) => {
|
||||
// 水印文字,父元素,字体,文字颜色
|
||||
let can: HTMLCanvasElement = document.createElement("canvas");
|
||||
parentNode.appendChild(can);
|
||||
can.width = 205;
|
||||
can.height = 140;
|
||||
can.style.display = "none";
|
||||
let cans = can.getContext("2d") as CanvasRenderingContext2D;
|
||||
cans.rotate((-20 * Math.PI) / 180);
|
||||
cans.font = font || "16px Microsoft JhengHei";
|
||||
cans.fillStyle = textColor || "rgba(180, 180, 180, 0.3)";
|
||||
cans.textAlign = "left";
|
||||
cans.textBaseline = "Middle" as CanvasTextBaseline;
|
||||
cans.fillText(str, can.width / 10, can.height / 2);
|
||||
parentNode.style.backgroundImage = "url(" + can.toDataURL("image/png") + ")";
|
||||
};
|
||||
|
||||
const waterMarker = {
|
||||
mounted(el: DirectiveBinding, binding: DirectiveBinding) {
|
||||
addWaterMarker(binding.value.text, el, binding.value.font, binding.value.textColor);
|
||||
}
|
||||
};
|
||||
|
||||
export default waterMarker;
|
||||
36
frontend/src/enums/httpEnum.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @description:请求配置
|
||||
*/
|
||||
export enum ResultEnum {
|
||||
SUCCESS = "A0000",
|
||||
ERROR = 500,
|
||||
ACCESSTOKEN_EXPIRED = "A0024",
|
||||
OVERDUE = "A0025",
|
||||
TIMEOUT = 30000,
|
||||
TYPE = "success"
|
||||
}
|
||||
|
||||
/**
|
||||
* @description:请求方法
|
||||
*/
|
||||
export enum RequestEnum {
|
||||
GET = "GET",
|
||||
POST = "POST",
|
||||
PATCH = "PATCH",
|
||||
PUT = "PUT",
|
||||
DELETE = "DELETE"
|
||||
}
|
||||
|
||||
/**
|
||||
* @description:常用的 contentTyp 类型
|
||||
*/
|
||||
export enum ContentTypeEnum {
|
||||
// json
|
||||
JSON = "application/json;charset=UTF-8",
|
||||
// text
|
||||
TEXT = "text/plain;charset=UTF-8",
|
||||
// form-data 一般配合qs
|
||||
FORM_URLENCODED = "application/x-www-form-urlencoded;charset=UTF-8",
|
||||
// form-data 上传
|
||||
FORM_DATA = "multipart/form-data;charset=UTF-8"
|
||||
}
|
||||
40
frontend/src/hooks/interface/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export namespace Table {
|
||||
export interface Pageable {
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ResPageable {
|
||||
current: number;
|
||||
size: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface StateProps {
|
||||
tableData: any[];
|
||||
pageable: Pageable;
|
||||
resPageable: ResPageable;
|
||||
searchParam: {
|
||||
[key: string]: any;
|
||||
};
|
||||
searchInitParam: {
|
||||
[key: string]: any;
|
||||
};
|
||||
totalParam: {
|
||||
[key: string]: any;
|
||||
};
|
||||
icon?: {
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export namespace HandleData {
|
||||
export type MessageType = "" | "success" | "warning" | "info" | "error";
|
||||
}
|
||||
|
||||
export namespace Theme {
|
||||
export type ThemeType = "light" | "inverted" | "dark";
|
||||
export type GreyOrWeakType = "grey" | "weak";
|
||||
}
|
||||