Compare commits

..

1 Commits

Author SHA1 Message Date
Xubx d192c5cadc 初始化工程 2025-01-08 17:13:39 +08:00
199 changed files with 26639 additions and 22007 deletions

2
.env
View File

@ -2,7 +2,7 @@
VITE_PORT = 3100
# 网站标题
VITE_GLOB_APP_TITLE = 哈尔滨师范大学英语四六级综合管理平台
VITE_GLOB_APP_TITLE = JeecgBoot 企业级低代码平台
# 简称,用于配置文件名字 不要出现空格、数字开头等特殊字符
VITE_GLOB_APP_SHORT_NAME = JeecgBootAdmin

View File

@ -5,16 +5,11 @@ VITE_USE_MOCK = true
VITE_PUBLIC_PATH = /
## 跨域代理,您可以配置多个 ,请注意,没有换行符
#VITE_PROXY = [["/jeecgboot","http://62.234.217.137:10000/jeecg-boot"],["/upload","http://localhost:3300/upload"]]
##后台接口全路径地址(必填)
#VITE_GLOB_DOMAIN_URL=http://62.234.217.137:10000/jeecg-boot
# 跨域代理,您可以配置多个 ,请注意,没有换行符
VITE_PROXY = [["/jeecgboot","http://127.0.0.1:8080/jeecg-boot"],["/upload","http://localhost:3300/upload"]]
VITE_PROXY = [["/jeecgboot","http://localhost:8090/jeecg-boot"],["/upload","http://localhost:3300/upload"]]
#后台接口全路径地址(必填)
VITE_GLOB_DOMAIN_URL=http://127.0.0.1:8080/jeecg-boot
VITE_GLOB_DOMAIN_URL=http://localhost:8090/jeecg-boot
#后台接口父地址(必填)
VITE_GLOB_API_URL=/jeecgboot

View File

@ -16,7 +16,7 @@ VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false
VITE_GLOB_API_URL=/jeecgboot
#后台接口全路径地址(必填)
VITE_GLOB_DOMAIN_URL=http://62.234.217.137:10000/jeecg-boot
VITE_GLOB_DOMAIN_URL=http://jeecg-boot-system:8080/jeecg-boot
# 接口父路径前缀
VITE_GLOB_API_URL_PREFIX=

16
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,16 @@
##### 版本号:
##### 问题描述:
##### 截图&代码:
#### 友情提示为了提高issue处理效率
- 未按格式要求发帖,会被直接删掉;
- 请自己初判问题描述是否清楚,是否方便我们调查处理;
- 描述过于简单或模糊,导致无法处理的,会被直接删掉;

38
LICENSE
View File

@ -1 +1,37 @@
黄晖制作
MIT License
Copyright (c) 2020-present, Jeecg
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
开源协议补充
JeecgBoot 是由 北京国炬信息技术有限公司 发行的软件。 总部位于北京地址中国·北京·朝阳区科荟前街1号院奥林佳泰大厦。邮箱jeecgos@163.com
本软件受适用的国家软件著作权法(包括国际条约)和双重保护许可。
1.允许基于本平台软件开展业务系统开发。
2.JeecgBoot底层依赖的非开源功能online lib依赖、仪表盘lib依赖等统一采用LGPL开源协议不二次改造、不拆分出jeecgboot之外使用就不产生侵权
3.不得基于该平台软件的基础修改包装成一个与JeecgBoot平台软件功能类似的产品进行发布、销售或与JeecgBoot参与同类软件产品市场的竞争。
违反此条款属于侵权行为,须赔偿侵权经济损失,同时立即停止著作权侵权行为。
总结在遵循Apache开源协议和开源协议补充条款下允许商用使用不会造成侵权行为
解释权归:
http://www.jeecg.com
http://guojusoft.com

View File

@ -1,11 +1,11 @@
JEECG BOOT 低代码开发平台Vue3前端
===============
当前最新版本: 3.6.2发布时间2024-01-08
当前最新版本: 3.6.3发布时间2024-03-11
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
[![](https://img.shields.io/badge/Author-北京国炬软件-orange.svg)](http://jeecg.com/aboutusIndex)
[![](https://img.shields.io/badge/Blog-官方博客-blue.svg)](https://jeecg.blog.csdn.net)
[![](https://img.shields.io/badge/version-3.6.2-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![](https://img.shields.io/badge/version-3.6.3-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub stars](https://img.shields.io/github/stars/zhangdaiscott/jeecg-boot.svg?style=social&label=Stars)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/zhangdaiscott/jeecg-boot)

View File

@ -5,6 +5,7 @@ import purgeIcons from 'vite-plugin-purge-icons';
import UnoCSS from 'unocss/vite';
import { presetTypography, presetUno } from 'unocss';
// 本地调试https配置方法
import VitePluginCertificate from 'vite-plugin-mkcert';
//[issues/555]开发环境vscode断点调试文件或行数对不上
import vueSetupExtend from 'vite-plugin-vue-setup-extend-plus';

View File

@ -157,10 +157,11 @@
</style>
<div class="app-loading">
<div class="app-loading-wrap">
<img src="/resource/img/logo.png" class="app-loading-logo" alt="Logo" />
<div class="app-loading-dots">
<span class="dot dot-spin"><i></i><i></i><i></i><i></i></span>
</div>
<div class="app-loading-title">正在加载 <%= title %></div>
<div class="app-loading-title"><%= title %></div>
</div>
</div>
</div>

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -6,10 +6,19 @@ const demoList = (keyword, count = 20) => {
list: [] as any[],
};
for (let index = 0; index < count; index++) {
result.list.push({
name: `${keyword ?? ''}选项${index}`,
id: `${index}`,
});
//根据搜索关键词做一下匹配
let name = `选项${index}`;
if(keyword && name.indexOf(keyword)!=-1){
result.list.push({
name: `选项${index}`,
id: `${index}`,
});
}else if(!keyword){
result.list.push({
name: `选项${index}`,
id: `${index}`,
});
}
}
return result;
};

View File

@ -1,6 +1,6 @@
{
"name": "jeecgboot-vue3",
"version": "3.6.2",
"version": "3.6.3",
"author": {
"name": "北京国炬信息技术有限公司",
"email": "jeecgos@163.com",
@ -45,7 +45,11 @@
"intro.js": "^7.2.0",
"lodash-es": "^4.17.21",
"lodash.get": "^4.4.2",
"marked": "^12.0.0",
"markdown-it": "^14.0.0",
"markdown-it-link-attributes": "^4.0.1",
"@traptitech/markdown-it-katex": "^3.6.0",
"event-source-polyfill": "^1.0.31",
"highlight.js": "^11.9.0",
"md5": "^2.3.0",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",

File diff suppressed because it is too large Load Diff

View File

@ -35,6 +35,11 @@
if (newValue === ThemeEnum.DARK) {
appTheme.value.algorithm = theme.darkAlgorithm;
}
// update-begin--author:liaozhiyang---date:20240322---forQQYUN-8570
if (import.meta.env.PROD) {
changeTheme(appStore.getProjectConfig.themeColor);
}
// update-end--author:liaozhiyang---date:20240322---forQQYUN-8570
appTheme.value = {
...appTheme.value,
};

View File

@ -3,7 +3,8 @@ import { getMenuListResultModel } from './model/menuModel';
enum Api {
GetMenuList = '/sys/permission/getUserPermissionByToken',
SwitchVue3Menu = '/sys/switchVue3Menu',
// 【QQYUN-8487】
// SwitchVue3Menu = '/sys/switchVue3Menu',
}
/**
@ -23,11 +24,20 @@ export const getMenuList = () => {
});
};
/**
* @description:
*/
export function getBackMenuAndPerms() {
return defHttp.get({ url: Api.GetMenuList });
}
/**
* vue3菜单
*/
export const switchVue3Menu = () => {
return new Promise((resolve) => {
defHttp.get({ url: Api.SwitchVue3Menu });
});
};
// update-begin--author:liaozhiyang---date:20240313---for【QQYUN-8487】注释掉判断菜单是否vue2版本逻辑代码
// export const switchVue3Menu = () => {
// return new Promise((resolve) => {
// defHttp.get({ url: Api.SwitchVue3Menu });
// });
// };
// update-end--author:liaozhiyang---date:20240313---for【QQYUN-8487】注释掉判断菜单是否vue2版本逻辑代码

View File

@ -126,6 +126,9 @@ export function getCaptcha(params) {
createErrorModal({ title: '错误提示', content: res.message || '未知问题' });
reject();
}
}).catch((res)=>{
createErrorModal({ title: '错误提示', content: res.message || '未知问题' });
reject();
});
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 752 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -34,8 +34,7 @@
flex-basis: 60%;
-webkit-flex-basis: 60%;
background-color: #0198cd;
background-image: url(../icon/jeecg_ad.jpg);
opacity: 0.91;
background-image: url(../icon/jeecg_ad.png);
background-size: cover;
}

View File

@ -4,10 +4,9 @@
-->
<template>
<div class="anticon" :class="getAppLogoClass" @click="goHome">
<img style="width: 100%;" src="../../../assets/images/logo-red.png" />
<div style="display: flex;" class="ml-2 truncate md:opacity-100" :class="getTitleClass" v-show="showTitle">
<!-- {{ title }} -->
<!-- <span>英语四六级综合管理平台</span> -->
<img src="../../../assets/images/logo.png" />
<div class="ml-2 truncate md:opacity-100" :class="getTitleClass" v-show="showTitle">
{{ title }}
</div>
</div>
</template>

View File

@ -12,6 +12,7 @@ export interface SearchResult {
name: string;
path: string;
icon?: string;
internalOrExternal: boolean;
}
// Translate special characters
@ -23,7 +24,7 @@ function transform(c: string) {
function createSearchReg(key: string) {
const keys = [...key].map((item) => transform(item));
const str = ['', ...keys, ''].join('.*');
return new RegExp(str);
return new RegExp(str, 'i');
}
export function useMenuSearch(refs: Ref<HTMLElement[]>, scrollWrap: Ref<ElRef>, emit: EmitType) {
@ -68,12 +69,13 @@ export function useMenuSearch(refs: Ref<HTMLElement[]>, scrollWrap: Ref<ElRef>,
function handlerSearchResult(filterMenu: Menu[], reg: RegExp, parent?: Menu) {
const ret: SearchResult[] = [];
filterMenu.forEach((item) => {
const { name, path, icon, children, hideMenu, meta } = item;
const { name, path, icon, children, hideMenu, meta, internalOrExternal } = item;
if (!hideMenu && reg.test(name) && (!children?.length || meta?.hideChildrenInMenu)) {
ret.push({
name: parent?.name ? `${parent.name} > ${name}` : name,
path,
icon,
internalOrExternal
});
}
if (!meta?.hideChildrenInMenu && Array.isArray(children) && children.length) {
@ -149,7 +151,14 @@ export function useMenuSearch(refs: Ref<HTMLElement[]>, scrollWrap: Ref<ElRef>,
const to = result[index];
handleClose();
await nextTick();
go(to.path);
// update-begin--author:liaozhiyang---date:20230803---for【QQYUN-8369】搜索区分大小写外部链接新页打开
if (to.internalOrExternal) {
window.open(to.path, '_blank');
} else {
go(to.path);
}
// update-end--author:liaozhiyang---date:20230803---for【QQYUN-8369】搜索区分大小写外部链接新页打开
}
// close search modal

View File

@ -59,7 +59,9 @@
instance && emit('register', drawerInstance, instance.uid);
const getMergeProps = computed((): DrawerProps => {
return deepMerge(toRaw(props), unref(propsRef));
// update-begin--author:liaozhiyang---date:20240320---forQQYUN-8389vue3.4footertoRow,propscomputed
return { ...deepMerge(props, unref(propsRef)) };
// update-end--author:liaozhiyang---date:20240320---forQQYUN-8389vue3.4footertoRow,propscomputed
});
const getProps = computed((): DrawerProps => {

View File

@ -1,6 +1,14 @@
/**
*
* JAreaLinkage
* JEditor
* JMarkdownEditor
* JCodeEditor
* JEasyCron
*/
import type { Component } from 'vue';
import type { ComponentType } from './types/index';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
/**
* Component list, register here to setting it in the form
*/
@ -29,7 +37,7 @@ import { StrengthMeter } from '/@/components/StrengthMeter';
import { IconPicker } from '/@/components/Icon';
import { CountdownInput } from '/@/components/CountDown';
//自定义组件
import JAreaLinkage from './jeecg/components/JAreaLinkage.vue';
// import JAreaLinkage from './jeecg/components/JAreaLinkage.vue';
import JSelectUser from './jeecg/components/JSelectUser.vue';
import JSelectPosition from './jeecg/components/JSelectPosition.vue';
import JSelectRole from './jeecg/components/JSelectRole.vue';
@ -37,17 +45,20 @@ import JImageUpload from './jeecg/components/JImageUpload.vue';
import JDictSelectTag from './jeecg/components/JDictSelectTag.vue';
import JSelectDept from './jeecg/components/JSelectDept.vue';
import JAreaSelect from './jeecg/components/JAreaSelect.vue';
import JEditor from './jeecg/components/JEditor.vue';
import JMarkdownEditor from './jeecg/components/JMarkdownEditor.vue';
// import JEditor from './jeecg/components/JEditor.vue';
// import JMarkdownEditor from './jeecg/components/JMarkdownEditor.vue';
import JSelectInput from './jeecg/components/JSelectInput.vue';
import JCodeEditor from './jeecg/components/JCodeEditor.vue';
// import JCodeEditor from './jeecg/components/JCodeEditor.vue';
import JCategorySelect from './jeecg/components/JCategorySelect.vue';
import JSelectMultiple from './jeecg/components/JSelectMultiple.vue';
import JPopup from './jeecg/components/JPopup.vue';
// update-begin--author:liaozhiyang---date:20240130---for【QQYUN-7961】popupDict字典
import JPopupDict from './jeecg/components/JPopupDict.vue';
// update-end--author:liaozhiyang---date:20240130---for【QQYUN-7961】popupDict字典
import JSwitch from './jeecg/components/JSwitch.vue';
import JTreeDict from './jeecg/components/JTreeDict.vue';
import JInputPop from './jeecg/components/JInputPop.vue';
import { JEasyCron } from './jeecg/components/JEasyCron';
// import { JEasyCron } from './jeecg/components/JEasyCron';
import JCheckbox from './jeecg/components/JCheckbox.vue';
import JInput from './jeecg/components/JInput.vue';
import JTreeSelect from './jeecg/components/JTreeSelect.vue';
@ -101,7 +112,10 @@ componentMap.set('Upload', BasicUpload);
componentMap.set('Divider', Divider);
//注册自定义组件
componentMap.set('JAreaLinkage', JAreaLinkage);
componentMap.set(
'JAreaLinkage',
createAsyncComponent(() => import('./jeecg/components/JAreaLinkage.vue'))
);
componentMap.set('JSelectPosition', JSelectPosition);
componentMap.set('JSelectUser', JSelectUser);
componentMap.set('JSelectRole', JSelectRole);
@ -109,17 +123,32 @@ componentMap.set('JImageUpload', JImageUpload);
componentMap.set('JDictSelectTag', JDictSelectTag);
componentMap.set('JSelectDept', JSelectDept);
componentMap.set('JAreaSelect', JAreaSelect);
componentMap.set('JEditor', JEditor);
componentMap.set('JMarkdownEditor', JMarkdownEditor);
componentMap.set(
'JEditor',
createAsyncComponent(() => import('./jeecg/components/JEditor.vue'))
);
componentMap.set(
'JMarkdownEditor',
createAsyncComponent(() => import('./jeecg/components/JMarkdownEditor.vue'))
);
componentMap.set('JSelectInput', JSelectInput);
componentMap.set('JCodeEditor', JCodeEditor);
componentMap.set(
'JCodeEditor',
createAsyncComponent(() => import('./jeecg/components/JCodeEditor.vue'))
);
componentMap.set('JCategorySelect', JCategorySelect);
componentMap.set('JSelectMultiple', JSelectMultiple);
componentMap.set('JPopup', JPopup);
// update-begin--author:liaozhiyang---date:20240130---for【QQYUN-7961】popupDict字典
componentMap.set('JPopupDict', JPopupDict);
// update-end--author:liaozhiyang---date:20240130---for【QQYUN-7961】popupDict字典
componentMap.set('JSwitch', JSwitch);
componentMap.set('JTreeDict', JTreeDict);
componentMap.set('JInputPop', JInputPop);
componentMap.set('JEasyCron', JEasyCron);
componentMap.set(
'JEasyCron',
createAsyncComponent(() => import('./jeecg/components/JEasyCron/EasyCronInput.vue'))
);
componentMap.set('JCheckbox', JCheckbox);
componentMap.set('JInput', JInput);
componentMap.set('JTreeSelect', JTreeSelect);

View File

@ -44,7 +44,9 @@
watch(
() => props.params,
() => {
!unref(isFirstLoaded) && fetch();
//update-begin---author:wangshuai---date:2024-02-28---for:QQYUN-8346 ApiTreeSelect #1054---
unref(isFirstLoaded) && fetch();
//update-end---author:wangshuai---date:2024-02-28---for:QQYUN-8346 ApiTreeSelect #1054---
},
{ deep: true }
);

View File

@ -105,6 +105,22 @@
return disabled;
});
// update-begin--author:liaozhiyang---date:20240308---forQQYUN-8377formSchema props
const getDynamicPropsValue = computed(() => {
const { dynamicPropsVal, dynamicPropskey } = props.schema;
if (dynamicPropskey == null) {
return null;
} else {
const { [dynamicPropskey]: itemValue } = unref(getComponentsProps);
let value = itemValue;
if (isFunction(dynamicPropsVal)) {
value = dynamicPropsVal(unref(getValues));
return value;
}
}
});
// update-end--author:liaozhiyang---date:20240308---forQQYUN-8377formSchema props
function getShow(): { isShow: boolean; isIfShow: boolean } {
const { show, ifShow } = props.schema;
const { showAdvancedButton } = props.formProps;
@ -183,9 +199,12 @@
rule.required = false;
}
if (component) {
if (!Reflect.has(rule, 'type')) {
//update-begin---author:wangshuai---date:2024-02-01---for:QQYUN-8176,,ApiSelect,,使,---
//https://github.com/vbenjs/vue-vben-admin/pull/3082 github
/*if (!Reflect.has(rule, 'type')) {
rule.type = component === 'InputNumber' ? 'number' : 'string';
}
}*/
//update-end---author:wangshuai---date:2024-02-01---for:QQYUN-8176,,ApiSelect,,使,---
rule.message = rule.message || defaultMsg;
@ -273,6 +292,12 @@
...unref(getComponentsProps),
disabled: unref(getDisable),
};
// update-begin--author:liaozhiyang---date:20240308---forQQYUN-8377formSchema props
const dynamicPropskey = props.schema.dynamicPropskey;
if (dynamicPropskey) {
propsData[dynamicPropskey] = unref(getDynamicPropsValue);
}
// update-end--author:liaozhiyang---date:20240308---forQQYUN-8377formSchema props
const isCreatePlaceholder = !propsData.disabled && autoSetPlaceHolder;
// RangePicker place
@ -312,21 +337,19 @@
//update-begin-author:taoyan date:2022-9-7 for: VUEN-2061online4 ..
//label
const { label, helpMessage, helpComponentProps, subLabel, labelLength } = props.schema;
let showLabel:string = (label+'')
if(labelLength && showLabel.length>4){
let showLabel: string = label + '';
if (labelLength && showLabel.length > 4) {
showLabel = showLabel.substr(0, labelLength);
}
const titleObj = {title: label}
const titleObj = { title: label };
const renderLabel = subLabel ? (
<span>
{label} <span class="text-secondary">{subLabel}</span>
</span>
) : labelLength ? (
<label {...titleObj}>{showLabel}</label>
) : (
labelLength ? (
<label {...titleObj}>{showLabel}</label>
) : (
label
)
label
);
//update-end-author:taoyan date:2022-9-7 for: VUEN-2061online4 ..
const getHelpMessage = isFunction(helpMessage) ? helpMessage(unref(getValues)) : helpMessage;
@ -388,14 +411,14 @@
}
const { baseColProps = {} } = props.formProps;
// update-begin--author:liaozhiyang---date:20230803---forissues-641span
// update-begin--author:liaozhiyang---date:20230803---forissues-641span
const { getIsMobile } = useAppInject();
let realColProps;
realColProps = { ...baseColProps, ...colProps };
if (colProps['span'] && !unref(getIsMobile)) {
['xs', 'sm', 'md', 'lg', 'xl', 'xxl'].forEach((name) => delete realColProps[name]);
}
// update-end--author:liaozhiyang---date:20230803---forissues-641span
// update-end--author:liaozhiyang---date:20230803---forissues-641span
const { isIfShow, isShow } = getShow();
const values = unref(getValues);

View File

@ -36,6 +36,12 @@ function genType() {
}
export function setComponentRuleType(rule: ValidationRule, component: ComponentType, valueFormat: string) {
//update-begin---author:wangshuai---date:2024-02-01---for:【QQYUN-8176】编辑表单中,校验必填时,如果组件是ApiSelect,打开编辑页面时,即使该字段有值,也会提示请选择---
//https://github.com/vbenjs/vue-vben-admin/pull/3082 github修复原文
if (Reflect.has(rule, 'type')) {
return;
}
//update-end---author:wangshuai---date:2024-02-01---for:【QQYUN-8176】编辑表单中,校验必填时,如果组件是ApiSelect,打开编辑页面时,即使该字段有值,也会提示请选择---
if (['DatePicker', 'MonthPicker', 'WeekPicker', 'TimePicker'].includes(component)) {
rule.type = valueFormat ? 'string' : 'object';
} else if (['RangePicker', 'Upload', 'CheckboxGroup', 'TimePicker'].includes(component)) {

View File

@ -1,5 +1,9 @@
<template>
<a-checkbox-group v-bind="attrs" v-model:value="checkboxArray" :options="checkOptions" @change="handleChange"></a-checkbox-group>
<a-checkbox-group v-bind="attrs" v-model:value="checkboxArray" :options="checkOptions" @change="handleChange">
<template #label="{label, value}">
<span :class="[useDicColor && getDicColor(value) ? 'colorText' : '']" :style="{ backgroundColor: `${getDicColor(value)}` }">{{ label }}</span>
</template>
</a-checkbox-group>
</template>
<script lang="ts">
@ -13,6 +17,7 @@
props: {
value:propTypes.oneOfType([propTypes.string, propTypes.number]),
dictCode: propTypes.string,
useDicColor: propTypes.bool.def(false),
options: {
type: Array,
default: () => [],
@ -69,6 +74,7 @@
prev.push({
label: next['text'],
value: value,
color: next['color'],
});
}
return prev;
@ -84,8 +90,30 @@
emit('update:value', $event.join(','));
emit('change', $event.join(','));
}
return { checkboxArray, checkOptions, attrs, handleChange };
const getDicColor = (value) => {
if (props.useDicColor) {
const findItem = checkOptions.value.find((item) => item.value == value);
if (findItem) {
return findItem.color;
}
}
return null;
};
return { checkboxArray, checkOptions, attrs, handleChange, getDicColor };
},
});
</script>
<style lang="less" scoped>
// update-begin--author:liaozhiyang---date:20230110---forQQYUN-7799
.colorText {
display: inline-block;
height: 20px;
line-height: 20px;
padding: 0 6px;
border-radius: 8px;
background-color: red;
color: #fff;
font-size: 12px;
}
// update-begin--author:liaozhiyang---date:20230110---forQQYUN-7799
</style>

View File

@ -2,7 +2,9 @@
<a-radio-group v-if="compType === CompTypeEnum.Radio" v-bind="attrs" v-model:value="state" @change="handleChangeRadio">
<template v-for="item in dictOptions" :key="`${item.value}`">
<a-radio :value="item.value">
{{ item.label }}
<span :class="[useDicColor && item.color ? 'colorText' : '']" :style="{ backgroundColor: `${useDicColor && item.color}` }">
{{ item.label }}
</span>
</a-radio>
</template>
</a-radio-group>
@ -41,7 +43,11 @@
<a-select-option v-if="showChooseOption" :value="null">请选择</a-select-option>
<template v-for="item in dictOptions" :key="`${item.value}`">
<a-select-option :value="item.value">
<span style="display: inline-block; width: 100%" :title="item.label">
<span
:class="[useDicColor && item.color ? 'colorText' : '']"
:style="{ backgroundColor: `${useDicColor && item.color}` }"
:title="item.label"
>
{{ item.label }}
</span>
</a-select-option>
@ -69,6 +75,7 @@
type: propTypes.string,
placeholder: propTypes.string,
stringToNumber: propTypes.bool,
useDicColor: propTypes.bool.def(false),
getPopupContainer: {
type: Function,
default: (node) => node?.parentNode,
@ -139,7 +146,8 @@
prev.push({
label: next['text'] || next['label'],
value: stringToNumber ? +value : value,
...omit(next, ['text', 'value']),
color: next['color'],
...omit(next, ['text', 'value', 'color']),
});
}
return prev;
@ -213,3 +221,17 @@
},
});
</script>
<style scoped lang="less">
// update-begin--author:liaozhiyang---date:20230110---forQQYUN-7799
.colorText {
display: inline-block;
height: 20px;
line-height: 20px;
padding: 0 6px;
border-radius: 8px;
background-color: red;
color: #fff;
font-size: 12px;
}
// update-begin--author:liaozhiyang---date:20230110---forQQYUN-7799
</style>

View File

@ -0,0 +1,217 @@
<!--popup组件-->
<template>
<div class="components-input-demo-presuffix">
<!--输入框-->
<a-select v-model:value="showText" v-bind="attrs" :mode="multi ? 'multiple' : ''" @click="handleOpen" readOnly :loading="loading">
<a-select-option v-for="item in options" :value="item.value">{{ item.text }}</a-select-option>
</a-select>
<!--popup弹窗-->
<JPopupOnlReportModal
@register="regModal"
:code="code"
:multi="multi"
:sorter="sorter"
:groupId="''"
:param="param"
@ok="callBack"
:getContainer="getContainer"
/>
</div>
</template>
<script lang="ts">
import JPopupOnlReportModal from './modal/JPopupOnlReportModal.vue';
import { defineComponent, ref, nextTick, watch, reactive, unref } from 'vue';
import { useModal } from '/@/components/Modal';
import { propTypes } from '/@/utils/propTypes';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { defHttp } from '/@/utils/http/axios';
import { useMessage } from '/@/hooks/web/useMessage';
//url
const configUrl = reactive({
getColumns: '/online/cgreport/api/getRpColumns/',
getData: '/online/cgreport/api/getData/',
});
export default defineComponent({
name: 'JPopupDict',
components: {
JPopupOnlReportModal,
},
inheritAttrs: false,
props: {
/**
* 示例demo,name,id
* demo: online报表编码
* name: online报表的字段用户显示的label
* id: online报表的字段用于存储key
*/
dictCode: propTypes.string.def(''),
value: propTypes.string.def(''),
sorter: propTypes.string.def(''),
multi: propTypes.bool.def(false),
param: propTypes.object.def({}),
spliter: propTypes.string.def(','),
getContainer: propTypes.func,
},
emits: ['update:value', 'register', 'change'],
setup(props, { emit }) {
const { createMessage } = useMessage();
const attrs = useAttrs();
const showText = ref<any>(props.multi ? [] : '');
const options = ref<any>([]);
const cgRpConfigId = ref('');
const loading = ref(false);
const code = props.dictCode.split(',')[0];
const labelFiled = props.dictCode.split(',')[1];
const valueFiled = props.dictCode.split(',')[2];
if (!code || !valueFiled || !labelFiled) {
createMessage.error('popupDict参数未正确配置!');
}
//model
const [regModal, { openModal }] = useModal();
/**
* 打开pop弹出框
*/
function handleOpen() {
!props.disabled && openModal(true);
}
/**
* 监听value数值
*/
watch(
() => props.value,
(val) => {
const callBack = () => {
if (props.multi) {
showText.value = val && val.length > 0 ? val.split(props.spliter) : [];
} else {
showText.value = val ?? '';
}
};
if (props.value || props.defaultValue) {
if (cgRpConfigId.value) {
loadData({ callBack });
} else {
loadColumnsInfo({ callBack });
}
} else {
callBack();
}
},
{ immediate: true }
);
watch(
() => showText.value,
(val) => {
let result;
if (props.multi) {
result = val.join(',');
} else {
result = val;
}
nextTick(() => {
emit('change', result);
emit('update:value', result);
});
}
);
/**
* 加载列信息
*/
function loadColumnsInfo({ callBack }) {
loading.value = true;
let url = `${configUrl.getColumns}${code}`;
defHttp
.get({ url }, { isTransformResponse: false, successMessageMode: 'none' })
.then((res) => {
if (res.success) {
cgRpConfigId.value = res.result.cgRpConfigId;
loadData({ callBack });
}
})
.catch((err) => {
loading.value = false;
callBack?.();
});
}
function loadData({ callBack }) {
loading.value = true;
let url = `${configUrl.getData}${unref(cgRpConfigId)}`;
defHttp
.get(
{ url, params: { ['force_' + valueFiled]: props.value || props.defaultValue } },
{ isTransformResponse: false, successMessageMode: 'none' }
)
.then((res) => {
let data = res.result;
if (data.records?.length) {
options.value = data.records.map((item) => {
return { value: item[valueFiled], text: item[labelFiled] };
});
}
})
.finally(() => {
loading.value = false;
callBack?.();
});
}
/**
* 传值回调
*/
function callBack(rows) {
const dataOptions: any = [];
const dataValue: any = [];
let result;
rows.forEach((item) => {
dataOptions.push({ value: item[valueFiled], text: item[labelFiled] });
dataValue.push(item[valueFiled]);
});
options.value = dataOptions;
if (props.multi) {
showText.value = dataValue;
result = dataValue.join(props.spliter);
} else {
showText.value = dataValue[0];
result = dataValue[0];
}
nextTick(() => {
emit('change', result);
emit('update:value', result);
});
}
return {
showText,
attrs,
regModal,
handleOpen,
callBack,
code,
options,
loading,
};
},
});
</script>
<style lang="less" scoped>
.components-input-demo-presuffix {
:deep(.ant-select-dropdown) {
display: none !important;
}
}
.components-input-demo-presuffix .anticon-close-circle {
cursor: pointer;
color: #ccc;
transition: color 0.3s;
font-size: 12px;
}
.components-input-demo-presuffix .anticon-close-circle:hover {
color: #f5222d;
}
.components-input-demo-presuffix .anticon-close-circle:active {
color: #666;
}
</style>

View File

@ -11,7 +11,7 @@
:getPopupContainer="getParentContainer"
>
<a-select-option v-for="(item, index) in dictOptions" :key="index" :getPopupContainer="getParentContainer" :value="item.value">
{{ item.text || item.label }}
<span :class="[useDicColor && item.color ? 'colorText' : '']" :style="{ backgroundColor: `${useDicColor && item.color}` }">{{ item.text || item.label }}</span>
</a-select-option>
</a-select>
</template>
@ -68,6 +68,10 @@
type: Boolean,
default: false,
},
useDicColor: {
type: Boolean,
default: false,
},
},
emits: ['options-change', 'change', 'input', 'update:value'],
setup(props, { emit, refs }) {
@ -135,7 +139,7 @@
//update-end-author:taoyan date:2022-6-21 for:
getDictItems(temp).then((res) => {
if (res) {
dictOptions.value = res.map((item) => ({ value: item.value, label: item.text }));
dictOptions.value = res.map((item) => ({ value: item.value, label: item.text, color:item.color }));
//console.info('res', dictOptions.value);
} else {
console.error('getDictItems error: : ', res);
@ -162,3 +166,15 @@
},
});
</script>
<style scoped lang='less'>
.colorText{
display: inline-block;
height: 20px;
line-height: 20px;
padding: 0 6px;
border-radius: 8px;
background-color: red;
color: #fff;
font-size: 12px;
}
</style>

View File

@ -137,8 +137,8 @@
e.preventDefault();
e.stopPropagation();
}
function records2DataList() {
//update-begin---author:wangshuai---date:2024-02-02---for:QQYUN-8239 2---
/* function records2DataList() {
let arr:any[] = [];
let excludeList = props.excludeUserIdList;
let records = props.dataList;
@ -150,13 +150,14 @@
}
}
return arr;
}
}*/
const showDataList = computed(()=>{
let excludeList = props.excludeUserIdList;
/* let excludeList = props.excludeUserIdList;
if(excludeList && excludeList.length>0){
return records2DataList();
}
}*/
//update-end---author:wangshuai---date:2024-02-02---for:QQYUN-8239 2---
return props.dataList;
});

View File

@ -141,7 +141,7 @@
const url = '/sys/user/selectUserList';
let params = {
pageNo: 1,
pageSize: 10,
pageSize: 99,
};
if (props.searchText) {
params['keyword'] = props.searchText;
@ -149,6 +149,11 @@
if (selectedDepartId.value) {
params['departId'] = selectedDepartId.value;
}
//update-begin---author:wangshuai---date:2024-02-02---for:QQYUN-8239 2---
if(props.excludeUserIdList && props.excludeUserIdList.length>0){
params['excludeUserIdList'] = props.excludeUserIdList.join(",");
}
//update-end---author:wangshuai---date:2024-02-02---for:QQYUN-8239 2---
const data = await defHttp.get({ url, params }, { isTransformResponse: false });
if (data.success) {
const { records } = data.result;

View File

@ -90,7 +90,7 @@
const url = '/sys/user/selectUserList';
let params = {
pageNo: 1,
pageSize: 10,
pageSize: 99,
};
if (props.searchText) {
params['keyword'] = props.searchText;
@ -98,6 +98,11 @@
if (selectedRoleId.value) {
params['roleId'] = selectedRoleId.value;
}
//update-begin---author:wangshuai---date:2024-02-02---for:QQYUN-8239 2---
if(props.excludeUserIdList && props.excludeUserIdList.length>0){
params['excludeUserIdList'] = props.excludeUserIdList.join(",");
}
//update-end---author:wangshuai---date:2024-02-02---for:QQYUN-8239 2---
const data = await defHttp.get({ url, params }, { isTransformResponse: false });
if (data.success) {
const { records } = data.result;

View File

@ -222,13 +222,16 @@
if (selectedDepart.value) {
params['departId'] = selectedDepart.value;
}
//update-begin---author:wangshuai---date:2024-02-02---for:QQYUN-8239 2---
if(unref(excludeUserIdList) && unref(excludeUserIdList).length>0){
params['excludeUserIdList'] = excludeUserIdList.value.join(",");
}
//update-end---author:wangshuai---date:2024-02-02---for:QQYUN-8239 2---
const data = await defHttp.get({ url, params }, { isTransformResponse: false });
if (data.success) {
let { records, total } = data.result;
//id0
if(unref(excludeUserIdList) && unref(excludeUserIdList).length>0){
total = total - unref(excludeUserIdList).length;
}
totalRecord.value = total;
initCurrentUserData(records);
userDataList.value = records;

View File

@ -199,7 +199,7 @@ export function useTreeBiz(treeRef, getList, props, realProps) {
const options = <any[]>[];
optionData.forEach((item) => {
//update-begin-author:taoyan date:2022-7-4 for: issues/I5F3P4 online配置部门选择后编辑查看数据应该显示部门名称不是部门代码
options.push({ label: item[props.titleKey], value: item[props.rowKey] });
options.push({ label: item[props.labelKey], value: item[props.rowKey] });
//update-end-author:taoyan date:2022-7-4 for: issues/I5F3P4 online配置部门选择后编辑查看数据应该显示部门名称不是部门代码
});
selectOptions.value = options;

View File

@ -192,6 +192,11 @@ export interface FormSchema {
dynamicDisabled?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean);
dynamicRules?: (renderCallbackParams: RenderCallbackParams) => Rule[];
// update-begin--author:liaozhiyang---date:20240308---for【QQYUN-8377】formSchema props支持动态修改
// 设置组件props的key
dynamicPropskey?: string;
dynamicPropsVal?: ((renderCallbackParams: RenderCallbackParams) => any);
// update-end--author:liaozhiyang---date:20240308---for【QQYUN-8377】formSchema props支持动态修改
// 这个属性自定义的 用于自定义的业务 比如在表单打开的时候修改表单的禁用状态但是又不能重写componentProps因为他的内容太多了所以使用dynamicDisabled和buss实现
buss?: any;

View File

@ -128,6 +128,7 @@ export type ComponentType =
| 'JCategorySelect'
| 'JSelectMultiple'
| 'JPopup'
| 'JPopupDict'
| 'JSwitch'
| 'JEasyCron'
| 'JTreeDict'

View File

@ -1,6 +1,7 @@
import { registerComponent, registerAsyncComponent } from '/@/components/jeecg/JVxeTable';
import { registerComponent, registerAsyncComponent, registerASyncComponentReal } from '/@/components/jeecg/JVxeTable';
import { JVxeTypes } from '/@/components/jeecg/JVxeTable/types';
import { DictSearchSpanCell, DictSearchInputCell } from './src/components/JVxeSelectDictSearchCell';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
export async function registerJVxeCustom() {
// ----------------- ⚠ 注意事项 ⚠ -----------------
@ -23,5 +24,8 @@ export async function registerJVxeCustom() {
// 注册【部门选择】组件
await registerAsyncComponent(JVxeTypes.departSelect, import('./src/components/JVxeDepartSelectCell.vue'));
// 注册【省市区选择】组件
await registerAsyncComponent(JVxeTypes.pca, import('./src/components/JVxePcaCell.vue'));
registerASyncComponentReal(
JVxeTypes.pca,
createAsyncComponent(() => import('./src/components/JVxePcaCell.vue'))
);
}

View File

@ -130,10 +130,11 @@
if (!props.visible) return;
const wrapperRefDom = unref(wrapperRef);
if (!wrapperRefDom) return;
const bodyDom = wrapperRefDom.$el.parentElement;
// update-begin--author:liaozhiyang---date:20240320---forQQYUN-8573BasicModal
const bodyDom = wrapperRefDom.$el.parentElement?.parentElement?.parentElement;
// update-end--author:liaozhiyang---date:20240320---forBasicModal
if (!bodyDom) return;
bodyDom.style.padding = '0';
// bodyDom.style.padding = '0';
await nextTick();
try {

View File

@ -1,20 +1,6 @@
<template>
<div class="scrollbar">
<div ref="wrap" :class="[wrapClass, 'scrollbar__wrap', native ? '' : 'scrollbar__wrap--hidden-default']"
:style="style" @scroll="handleScroll">
<div style="display: flex; justify-content: center; align-items: center">
<span style="
font-size: 18px;
font-weight: 700;
transition: all 0.5s;
line-height: normal;
padding: 5px;
">
英语四六级综合管理平台
</span>
</div>
<div ref="wrap" :class="[wrapClass, 'scrollbar__wrap', native ? '' : 'scrollbar__wrap--hidden-default']" :style="style" @scroll="handleScroll">
<component :is="tag" ref="resize" :class="['scrollbar__view', viewClass]" :style="viewStyle">
<slot></slot>
</component>
@ -26,182 +12,182 @@
</div>
</template>
<script lang="ts">
import { addResizeListener, removeResizeListener } from '/@/utils/event';
import componentSetting from '/@/settings/componentSetting';
const { scrollbar } = componentSetting;
import { toObject } from './util';
import { defineComponent, ref, onMounted, onBeforeUnmount, nextTick, provide, computed, unref } from 'vue';
import Bar from './bar';
import { addResizeListener, removeResizeListener } from '/@/utils/event';
import componentSetting from '/@/settings/componentSetting';
const { scrollbar } = componentSetting;
import { toObject } from './util';
import { defineComponent, ref, onMounted, onBeforeUnmount, nextTick, provide, computed, unref } from 'vue';
import Bar from './bar';
export default defineComponent({
name: 'Scrollbar',
// inheritAttrs: false,
components: { Bar },
props: {
native: {
type: Boolean,
default: scrollbar?.native ?? false,
export default defineComponent({
name: 'Scrollbar',
// inheritAttrs: false,
components: { Bar },
props: {
native: {
type: Boolean,
default: scrollbar?.native ?? false,
},
wrapStyle: {
type: [String, Array],
default: '',
},
wrapClass: {
type: [String, Array],
default: '',
},
viewClass: {
type: [String, Array],
default: '',
},
viewStyle: {
type: [String, Array],
default: '',
},
noresize: Boolean, // container
tag: {
type: String,
default: 'div',
},
},
wrapStyle: {
type: [String, Array],
default: '',
setup(props) {
const sizeWidth = ref('0');
const sizeHeight = ref('0');
const moveX = ref(0);
const moveY = ref(0);
const wrap = ref();
const resize = ref();
provide('scroll-bar-wrap', wrap);
const style = computed(() => {
if (Array.isArray(props.wrapStyle)) {
return toObject(props.wrapStyle);
}
return props.wrapStyle;
});
const handleScroll = () => {
if (!props.native) {
moveY.value = (unref(wrap).scrollTop * 100) / unref(wrap).clientHeight;
moveX.value = (unref(wrap).scrollLeft * 100) / unref(wrap).clientWidth;
}
};
const update = () => {
if (!unref(wrap)) return;
const heightPercentage = (unref(wrap).clientHeight * 100) / unref(wrap).scrollHeight;
const widthPercentage = (unref(wrap).clientWidth * 100) / unref(wrap).scrollWidth;
sizeHeight.value = heightPercentage < 100 ? heightPercentage + '%' : '';
sizeWidth.value = widthPercentage < 100 ? widthPercentage + '%' : '';
};
onMounted(() => {
if (props.native) return;
nextTick(update);
if (!props.noresize) {
addResizeListener(unref(resize), update);
addResizeListener(unref(wrap), update);
addEventListener('resize', update);
}
});
onBeforeUnmount(() => {
if (props.native) return;
if (!props.noresize) {
removeResizeListener(unref(resize), update);
removeResizeListener(unref(wrap), update);
removeEventListener('resize', update);
}
});
return {
moveX,
moveY,
sizeWidth,
sizeHeight,
style,
wrap,
resize,
update,
handleScroll,
};
},
wrapClass: {
type: [String, Array],
default: '',
},
viewClass: {
type: [String, Array],
default: '',
},
viewStyle: {
type: [String, Array],
default: '',
},
noresize: Boolean, // container
tag: {
type: String,
default: 'div',
},
},
setup(props) {
const sizeWidth = ref('0');
const sizeHeight = ref('0');
const moveX = ref(0);
const moveY = ref(0);
const wrap = ref();
const resize = ref();
provide('scroll-bar-wrap', wrap);
const style = computed(() => {
if (Array.isArray(props.wrapStyle)) {
return toObject(props.wrapStyle);
}
return props.wrapStyle;
});
const handleScroll = () => {
if (!props.native) {
moveY.value = (unref(wrap).scrollTop * 100) / unref(wrap).clientHeight;
moveX.value = (unref(wrap).scrollLeft * 100) / unref(wrap).clientWidth;
}
};
const update = () => {
if (!unref(wrap)) return;
const heightPercentage = (unref(wrap).clientHeight * 100) / unref(wrap).scrollHeight;
const widthPercentage = (unref(wrap).clientWidth * 100) / unref(wrap).scrollWidth;
sizeHeight.value = heightPercentage < 100 ? heightPercentage + '%' : '';
sizeWidth.value = widthPercentage < 100 ? widthPercentage + '%' : '';
};
onMounted(() => {
if (props.native) return;
nextTick(update);
if (!props.noresize) {
addResizeListener(unref(resize), update);
addResizeListener(unref(wrap), update);
addEventListener('resize', update);
}
});
onBeforeUnmount(() => {
if (props.native) return;
if (!props.noresize) {
removeResizeListener(unref(resize), update);
removeResizeListener(unref(wrap), update);
removeEventListener('resize', update);
}
});
return {
moveX,
moveY,
sizeWidth,
sizeHeight,
style,
wrap,
resize,
update,
handleScroll,
};
},
});
});
</script>
<style lang="less">
.scrollbar {
position: relative;
height: 100%;
overflow: hidden;
&__wrap {
height: 100%;
overflow: auto;
&--hidden-default {
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
opacity: 0;
}
}
}
&__thumb {
.scrollbar {
position: relative;
display: block;
width: 0;
height: 0;
cursor: pointer;
background-color: rgba(144, 147, 153, 0.3);
border-radius: inherit;
transition: 0.3s background-color;
height: 100%;
overflow: hidden;
&:hover {
background-color: rgba(144, 147, 153, 0.5);
}
}
&__wrap {
height: 100%;
overflow: auto;
&__bar {
position: absolute;
right: 2px;
bottom: 2px;
z-index: 1;
border-radius: 4px;
opacity: 0;
-webkit-transition: opacity 80ms ease;
transition: opacity 80ms ease;
&--hidden-default {
scrollbar-width: none;
&.is-vertical {
top: 2px;
width: 6px;
&>div {
width: 100%;
&::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
opacity: 0;
}
}
}
&.is-horizontal {
left: 2px;
height: 6px;
&__thumb {
position: relative;
display: block;
width: 0;
height: 0;
cursor: pointer;
background-color: rgba(144, 147, 153, 0.3);
border-radius: inherit;
transition: 0.3s background-color;
&>div {
height: 100%;
&:hover {
background-color: rgba(144, 147, 153, 0.5);
}
}
&__bar {
position: absolute;
right: 2px;
bottom: 2px;
z-index: 1;
border-radius: 4px;
opacity: 0;
-webkit-transition: opacity 80ms ease;
transition: opacity 80ms ease;
&.is-vertical {
top: 2px;
width: 6px;
& > div {
width: 100%;
}
}
&.is-horizontal {
left: 2px;
height: 6px;
& > div {
height: 100%;
}
}
}
}
}
.scrollbar:active>.scrollbar__bar,
.scrollbar:focus>.scrollbar__bar,
.scrollbar:hover>.scrollbar__bar {
opacity: 1;
transition: opacity 340ms ease-out;
}
.scrollbar:active > .scrollbar__bar,
.scrollbar:focus > .scrollbar__bar,
.scrollbar:hover > .scrollbar__bar {
opacity: 1;
transition: opacity 340ms ease-out;
}
</style>

View File

@ -127,6 +127,14 @@
// update-begin--author:sunjianlei---date:20220408---for: VUEN-656#
return;
}
// update-begin--author:liaozhiyang---date:20240227---forQQYUN-6366tab
const findItem = getMatchingMenu(props.items, key);
if (findItem?.internalOrExternal == true) {
window.open(location.origin + key);
return;
}
// update-end--author:liaozhiyang---date:20240227---forQQYUN-6366tab
const { beforeClickFn } = props;
if (beforeClickFn && isFunction(beforeClickFn)) {
const flag = await beforeClickFn(key);
@ -140,6 +148,26 @@
menuState.activeName = key;
}
/**
* 2024-02-27
* liaozhiyang
* 获取菜单中匹配的path所在的项
*/
const getMatchingMenu = (menus, path) => {
for (let i = 0, len = menus.length; i < len; i++) {
const item = menus[i];
if (item.path === path && !item.redirect && !item.paramPath) {
return item;
} else if (item.children?.length) {
const result = getMatchingMenu(item.children, path);
if (result) {
return result;
}
}
}
return '';
}
return {
prefixCls,
getBindValues,

View File

@ -251,6 +251,11 @@
&-popconfirm {
.ant-popconfirm-buttons {
min-width: 120px;
// update-begin--author:liaozhiyang---date:20240124---forissues/1019popConfirm
display: flex;
align-items: center;
justify-content: center;
// update-end--author:liaozhiyang---date:20240124---forissues/1019popConfirm
}
}
}

View File

@ -57,7 +57,9 @@
if (!isFunction(summaryFunc)) {
return [];
}
let dataSource = toRaw(unref(table.getDataSource()));
// update-begin--author:liaozhiyang---date:20230227---forQQYUN-8172
let dataSource = cloneDeep(unref(table.getDataSource()));
// update-end--author:liaozhiyang---date:20230227---forQQYUN-8172
dataSource = summaryFunc(dataSource);
dataSource.forEach((item, i) => {
item[props.rowKey] = `${i}`;

View File

@ -1,5 +1,5 @@
<template>
<Tooltip placement="top" v-bind="getBindProps">
<Tooltip placement="top" v-bind="getBindProps" >
<template #title>
<span>{{ t('component.table.settingColumn') }}</span>
</template>

View File

@ -1,5 +1,6 @@
import { computed, nextTick, unref, watchEffect } from 'vue';
import { router } from '/@/router';
import { useRoute } from 'vue-router';
import { createLocalStorage } from '/@/utils/cache';
import { useTableContext } from './useTableContext';
import { useMessage } from '/@/hooks/web/useMessage';
@ -12,10 +13,13 @@ export function useColumnsCache(opt, setColumns, handleColumnFixed) {
const table = useTableContext();
const $ls = createLocalStorage();
const { createMessage: $message } = useMessage();
const route = useRoute();
// 列表配置缓存key
const cacheKey = computed(() => {
let { fullPath } = router.currentRoute.value;
let key = fullPath.replace(/[\/\\]/g, '_');
// update-begin--author:liaozhiyang---date:20240226---for【QQYUN-8367】online报表配置列展示保存影响到其他页面的table字段的显示隐藏开发环境热更新会有此问题生产环境无问题
const path = route.path;
let key = path.replace(/[\/\\]/g, '_');
// update-end--author:liaozhiyang---date:20240226---for【QQYUN-8367】online报表配置列展示保存影响到其他页面的table字段的显示隐藏开发环境热更新会有此问题生产环境无问题
let cacheKey = table.getBindValues.value.tableSetting?.cacheKey;
if (cacheKey) {
key += ':' + cacheKey;

View File

@ -248,7 +248,14 @@ export function useDataSource(
if (beforeFetch && isFunction(beforeFetch)) {
params = (await beforeFetch(params)) || params;
}
// update-begin--author:liaozhiyang---date:20240227---for【QQYUN-8316】table查询条件,请求剔除空字符串字段
for (let item of Object.entries(params)) {
const [key, val] = item;
if (val === '') {
delete params[key];
};
};
// update-end--author:liaozhiyang---date:20240227---for【QQYUN-8316】table查询条件,请求剔除空字符串字段
const res = await api(params);
rawDataSourceRef.value = res;

View File

@ -118,7 +118,12 @@
const initOptions = computed(() => {
const { height, options, toolbar, plugins, menubar } = props;
const publicPath = import.meta.env.VITE_PUBLIC_PATH || '/';
let publicPath = import.meta.env.VITE_PUBLIC_PATH || '/';
// update-begin--author:liaozhiyang---date:20240320---forQQYUN-8571/
if (!publicPath.endsWith('/')) {
publicPath += '/';
}
// update-end--author:liaozhiyang---date:20240320---forQQYUN-8571/
return {
selector: `#${unref(tinymceId)}`,
height,

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -0,0 +1,445 @@
<template>
<div class="chatWrap">
<div class="content">
<div class="main">
<div id="scrollRef" ref="scrollRef" class="scrollArea">
<template v-if="chatData.length">
<div class="chatContentArea">
<chatMessage
v-for="(item, index) of chatData"
:key="index"
:date-time="item.dateTime"
:text="item.text"
:inversion="item.inversion"
:error="item.error"
:loading="item.loading"
></chatMessage>
</div>
<div v-if="loading" class="stopArea">
<a-button type="primary" danger @click="handleStop" class="stopBtn">
<svg
t="1706148514627"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="5214"
width="18"
height="18"
>
<path
d="M512 967.111111c-250.311111 0-455.111111-204.8-455.111111-455.111111s204.8-455.111111 455.111111-455.111111 455.111111 204.8 455.111111 455.111111-204.8 455.111111-455.111111 455.111111z m0-56.888889c221.866667 0 398.222222-176.355556 398.222222-398.222222s-176.355556-398.222222-398.222222-398.222222-398.222222 176.355556-398.222222 398.222222 176.355556 398.222222 398.222222 398.222222z"
fill="currentColor"
p-id="5215"
></path>
<path d="M341.333333 341.333333h341.333334v341.333334H341.333333z" fill="currentColor" p-id="5216"></path>
</svg>
<span>停止响应</span>
</a-button>
</div>
</template>
<template v-else>
<div class="emptyArea">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="mr-2 text-3xl iconify iconify--ri"
width="1em"
height="1em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M16 16a3 3 0 1 1 0 6a3 3 0 0 1 0-6M6 12a4 4 0 1 1 0 8a4 4 0 0 1 0-8m8.5-10a5.5 5.5 0 1 1 0 11a5.5 5.5 0 0 1 0-11"
></path>
</svg>
<span>新建聊天</span>
</div>
</template>
</div>
</div>
<div class="footer">
<a-button type="text" class="delBtn" @click="handleDelSession">
<svg
t="1706504908534"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="1584"
width="18"
height="18"
>
<path
d="M816.872727 158.254545h-181.527272V139.636364c0-39.563636-30.254545-69.818182-69.818182-69.818182h-107.054546c-39.563636 0-69.818182 30.254545-69.818182 69.818182v18.618181H207.127273c-48.872727 0-90.763636 41.890909-90.763637 93.09091s41.890909 90.763636 90.763637 90.763636h609.745454c51.2 0 90.763636-41.890909 90.763637-90.763636 0-51.2-41.890909-93.090909-90.763637-93.09091zM435.2 139.636364c0-13.963636 9.309091-23.272727 23.272727-23.272728h107.054546c13.963636 0 23.272727 9.309091 23.272727 23.272728v18.618181h-153.6V139.636364z m381.672727 155.927272H207.127273c-25.6 0-44.218182-20.945455-44.218182-44.218181 0-25.6 20.945455-44.218182 44.218182-44.218182h609.745454c25.6 0 44.218182 20.945455 44.218182 44.218182 0 23.272727-20.945455 44.218182-44.218182 44.218181zM835.490909 407.272727h-121.018182c-13.963636 0-23.272727 9.309091-23.272727 23.272728s9.309091 23.272727 23.272727 23.272727h97.745455V837.818182c0 39.563636-30.254545 69.818182-69.818182 69.818182h-37.236364V602.763636c0-13.963636-9.309091-23.272727-23.272727-23.272727s-23.272727 9.309091-23.272727 23.272727V907.636364h-118.690909V602.763636c0-13.963636-9.309091-23.272727-23.272728-23.272727s-23.272727 9.309091-23.272727 23.272727V907.636364H372.363636V602.763636c0-13.963636-9.309091-23.272727-23.272727-23.272727s-23.272727 9.309091-23.272727 23.272727V907.636364h-34.909091c-39.563636 0-69.818182-30.254545-69.818182-69.818182V453.818182H558.545455c13.963636 0 23.272727-9.309091 23.272727-23.272727s-9.309091-23.272727-23.272727-23.272728H197.818182c-13.963636 0-23.272727 9.309091-23.272727 23.272728V837.818182c0 65.163636 51.2 116.363636 116.363636 116.363636h451.490909c65.163636 0 116.363636-51.2 116.363636-116.363636V430.545455c0-13.963636-11.636364-23.272727-23.272727-23.272728z"
fill="currentColor"
p-id="1585"
></path>
</svg>
</a-button>
<a-button type="text" class="contextBtn" :class="[usingContext && 'enabled']" @click="handleUsingContext">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--ri"
width="20"
height="20"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10a9.956 9.956 0 0 1-4.708-1.175L2 22l1.176-5.29A9.956 9.956 0 0 1 2 12C2 6.477 6.477 2 12 2m0 2a8 8 0 0 0-8 8c0 1.335.326 2.618.94 3.766l.35.654l-.656 2.946l2.948-.654l.653.349A7.955 7.955 0 0 0 12 20a8 8 0 1 0 0-16m1 3v5h4v2h-6V7z"
></path>
</svg>
</a-button>
<a-textarea
ref="inputRef"
v-model:value="prompt"
:autoSize="{ minRows: 1, maxRows: 6 }"
:placeholder="placeholder"
@pressEnter="handleEnter"
autofocus
></a-textarea>
<a-button
@click="
() => {
handleSubmit();
}
"
:disabled="loading"
type="primary"
class="sendBtn"
>
<svg
t="1706147858151"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="4237"
width="1em"
height="1em"
>
<path
d="M865.28 202.5472c-17.1008-15.2576-41.0624-19.6608-62.5664-11.5712L177.7664 427.1104c-23.2448 8.8064-38.5024 29.696-39.6288 54.5792-1.1264 24.8832 11.9808 47.104 34.4064 58.0608l97.5872 47.7184c4.5056 2.2528 8.0896 6.0416 9.9328 10.6496l65.4336 161.1776c7.7824 19.1488 24.4736 32.9728 44.7488 37.0688 20.2752 4.096 41.0624-2.1504 55.6032-16.7936l36.352-36.352c6.4512-6.4512 16.5888-7.8848 24.576-3.3792l156.5696 88.8832c9.4208 5.3248 19.8656 8.0896 30.3104 8.0896 8.192 0 16.4864-1.6384 24.2688-5.0176 17.8176-7.68 30.72-22.8352 35.4304-41.6768l130.7648-527.1552c5.5296-22.016-1.7408-45.2608-18.8416-60.416z m-20.8896 50.7904L713.5232 780.4928c-1.536 6.2464-5.8368 11.3664-11.776 13.9264s-12.5952 2.1504-18.2272-1.024L526.9504 704.512c-9.4208-5.3248-19.8656-7.9872-30.208-7.9872-15.9744 0-31.744 6.144-43.52 17.92l-36.352 36.352c-3.8912 3.8912-8.9088 5.9392-14.2336 6.0416l55.6032-152.1664c0.512-1.3312 1.2288-2.56 2.2528-3.6864l240.3328-246.1696c8.2944-8.4992-2.048-21.9136-12.3904-16.0768L301.6704 559.8208c-4.096-3.584-8.704-6.656-13.6192-9.1136L190.464 502.9888c-11.264-5.5296-11.5712-16.1792-11.4688-19.3536 0.1024-3.1744 1.536-13.824 13.2096-18.2272L817.152 229.2736c10.4448-3.9936 18.0224 1.3312 20.8896 3.8912 2.8672 2.4576 9.0112 9.3184 6.3488 20.1728z"
p-id="4238"
fill="currentColor"
></path>
</svg>
</a-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { Ref } from 'vue';
import { computed, ref, createVNode, onUnmounted, onMounted } from 'vue';
import { useScroll } from '../hooks/useScroll';
import { EventSourcePolyfill } from 'event-source-polyfill';
import { ConfigEnum } from '/@/enums/httpEnum';
import { getToken } from '/@/utils/auth';
import { getAppEnvConfig } from '/@/utils/env';
import chatMessage from './chatMessage.vue';
import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons-vue';
import { message, Modal } from 'ant-design-vue';
import '../style/github-markdown.less';
import '../style/highlight.less';
import '../style/style.less';
const props = defineProps(['chatData', 'uuid', 'dataSource']);
const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll();
const prompt = ref<string>('');
const loading = ref<boolean>(false);
const inputRef = ref<Ref | null>(null);
// const chatData = computed(() => {
// return props.chatData;
// });
// ,
const usingContext = ref<any>(true);
const uuid = computed(() => {
return props.uuid;
});
let evtSource: any = null;
const { VITE_GLOB_API_URL } = getAppEnvConfig();
const conversationList = computed(() => props.chatData.filter((item) => !item.inversion && !!item.conversationOptions));
const placeholder = computed(() => {
return '来说点什么吧...Shift + Enter = 换行)';
});
function handleEnter(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
handleSubmit();
}
}
function handleSubmit() {
onConversation();
}
async function onConversation() {
let message = prompt.value;
if (loading.value) return;
if (!message || message.trim() === '') return;
loading.value = true;
prompt.value = '';
if (props.chatData.length == 0) {
const findItem = props.dataSource.history.find((item) => item.uuid === uuid.value);
if (findItem && findItem.title == '新建聊天') {
findItem.title = message;
}
}
addChat(uuid.value, {
dateTime: new Date().toLocaleString(),
text: message,
inversion: true,
error: false,
conversationOptions: null,
requestOptions: { prompt: message, options: null },
});
scrollToBottom();
let options: any = {};
const lastContext = conversationList.value[conversationList.value.length - 1]?.conversationOptions;
if (lastContext && usingContext.value) options = { ...lastContext };
addChat(uuid.value, {
dateTime: new Date().toLocaleString(),
text: '思考中...',
loading: true,
inversion: false,
error: false,
conversationOptions: null,
requestOptions: { prompt: message, options: { ...options } },
});
scrollToBottom();
const initEventSource = () => {
let lastText = '';
if (typeof EventSource !== 'undefined') {
const token = getToken();
evtSource = new EventSourcePolyfill(
`${VITE_GLOB_API_URL}/ai/chat/send?message=${message}${options.parentMessageId ? '&topicId=' + options.parentMessageId : ''}`,
{
withCredentials: true,
headers: {
[ConfigEnum.TOKEN]: token,
},
}
); //
//
evtSource.onopen = function (e) {
console.log(e);
};
//
evtSource.onmessage = function (e) {
const data = e.data;
// console.log(e);
if (data === '[DONE]') {
updateChatSome(uuid, props.chatData.length - 1, { loading: false });
scrollToBottom();
handleStop();
evtSource.close(); //
} else {
try {
const _data = JSON.parse(data);
const content = _data.content;
if (content != undefined) {
lastText += content;
updateChat(uuid.value, props.chatData.length - 1, {
dateTime: new Date().toLocaleString(),
text: lastText,
inversion: false,
error: false,
loading: true,
conversationOptions: e.lastEventId == '[ERR]' ? null : { conversationId: data.conversationId, parentMessageId: e.lastEventId },
requestOptions: { prompt: message, options: { ...options } },
});
scrollToBottom();
} else {
updateChatSome(uuid.value, props.chatData.length - 1, { loading: false });
scrollToBottom();
handleStop();
}
} catch (error) {
updateChatSome(uuid.value, props.chatData.length - 1, { loading: false });
scrollToBottom();
handleStop();
evtSource.close(); //
}
}
};
//
evtSource.onerror = function (e) {
// console.log(e);
if (e.error?.message || e.statusText) {
updateChat(uuid.value, props.chatData.length - 1, {
dateTime: new Date().toLocaleString(),
text: e.error?.message ?? e.statusText,
inversion: false,
error: false,
loading: true,
conversationOptions: null,
requestOptions: { prompt: message, options: { ...options } },
});
scrollToBottom();
}
evtSource.close(); //
updateChatSome(uuid.value, props.chatData.length - 1, { loading: false });
handleStop();
};
} else {
console.log('当前浏览器不支持使用EventSource接收服务器推送事件!');
}
};
initEventSource();
}
onUnmounted(() => {
evtSource?.close();
updateChatSome(uuid.value, props.chatData.length - 1, { loading: false });
});
const addChat = (uuid, data) => {
props.chatData.push({ ...data });
};
const updateChat = (uuid, index, data) => {
props.chatData.splice(index, 1, data);
};
const updateChatSome = (uuid, index, data) => {
props.chatData[index] = { ...props.chatData[index], ...data };
};
//
const handleDelSession = () => {
Modal.confirm({
title: '清空会话',
icon: createVNode(ExclamationCircleOutlined),
content: '是否清空会话?',
closable: true,
okText: '确定',
cancelText: '取消',
async onOk() {
try {
return await new Promise<void>((resolve) => {
props.chatData.length = 0;
resolve();
});
} catch {
return console.log('Oops errors!');
}
},
});
};
//
const handleStop = () => {
if (loading.value) {
loading.value = false;
}
if (evtSource) {
evtSource?.close();
updateChatSome(uuid, props.chatData.length - 1, { loading: false });
}
};
// 使
const handleUsingContext = () => {
usingContext.value = !usingContext.value;
if (usingContext.value) {
message.success('当前模式下, 发送消息会携带之前的聊天记录');
} else {
message.warning('当前模式下, 发送消息不会携带之前的聊天记录');
}
};
onMounted(() => {
scrollToBottom();
});
</script>
<style lang="less" scoped>
.chatWrap {
width: 100%;
height: 100%;
padding: 20px;
.content {
height: 100%;
width: 100%;
background: #fff;
display: flex;
flex-direction: column;
}
}
.main {
flex: 1;
min-height: 0;
.scrollArea {
overflow-y: auto;
height: 100%;
}
.chatContentArea {
padding: 10px;
}
}
.emptyArea {
display: flex;
justify-content: center;
align-items: center;
color: #d4d4d4;
}
.stopArea {
display: flex;
justify-content: center;
padding: 10px 0;
.stopBtn {
display: flex;
justify-content: center;
align-items: center;
svg {
margin-right: 5px;
}
}
}
.footer {
display: flex;
align-items: center;
padding: 6px 16px;
.ant-input {
margin: 0 16px;
}
.ant-input,
.ant-btn {
height: 36px;
}
textarea.ant-input {
padding-top: 6px;
padding-bottom: 6px;
}
.contextBtn,
.delBtn {
padding: 0;
width: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.delBtn {
margin-right: 8px;
}
.contextBtn {
color: #a8071a;
&.enabled {
color: @primary-color;
}
font-size: 18px;
}
.sendBtn {
padding: 0 10px;
font-size: 22px;
display: flex;
align-items: center;
}
}
</style>

View File

@ -0,0 +1,73 @@
<template>
<div class="chat" :class="[inversion ? 'self' : 'chatgpt']">
<div class="avatar">
<img v-if="inversion" :src="avatar()" />
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" aria-hidden="true" width="1em" height="1em">
<path
d="M29.71,13.09A8.09,8.09,0,0,0,20.34,2.68a8.08,8.08,0,0,0-13.7,2.9A8.08,8.08,0,0,0,2.3,18.9,8,8,0,0,0,3,25.45a8.08,8.08,0,0,0,8.69,3.87,8,8,0,0,0,6,2.68,8.09,8.09,0,0,0,7.7-5.61,8,8,0,0,0,5.33-3.86A8.09,8.09,0,0,0,29.71,13.09Zm-12,16.82a6,6,0,0,1-3.84-1.39l.19-.11,6.37-3.68a1,1,0,0,0,.53-.91v-9l2.69,1.56a.08.08,0,0,1,.05.07v7.44A6,6,0,0,1,17.68,29.91ZM4.8,24.41a6,6,0,0,1-.71-4l.19.11,6.37,3.68a1,1,0,0,0,1,0l7.79-4.49V22.8a.09.09,0,0,1,0,.08L13,26.6A6,6,0,0,1,4.8,24.41ZM3.12,10.53A6,6,0,0,1,6.28,7.9v7.57a1,1,0,0,0,.51.9l7.75,4.47L11.85,22.4a.14.14,0,0,1-.09,0L5.32,18.68a6,6,0,0,1-2.2-8.18Zm22.13,5.14-7.78-4.52L20.16,9.6a.08.08,0,0,1,.09,0l6.44,3.72a6,6,0,0,1-.9,10.81V16.56A1.06,1.06,0,0,0,25.25,15.67Zm2.68-4-.19-.12-6.36-3.7a1,1,0,0,0-1.05,0l-7.78,4.49V9.2a.09.09,0,0,1,0-.09L19,5.4a6,6,0,0,1,8.91,6.21ZM11.08,17.15,8.38,15.6a.14.14,0,0,1-.05-.08V8.1a6,6,0,0,1,9.84-4.61L18,3.6,11.61,7.28a1,1,0,0,0-.53.91ZM12.54,14,16,12l3.47,2v4L16,20l-3.47-2Z"
fill="currentColor"
/>
</svg>
</div>
<div class="content">
<p class="date">{{ dateTime }}</p>
<div class="msgArea">
<chatText :text="text" :inversion="inversion" :error="error" :loading="loading"></chatText>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import chatText from './chatText.vue';
import defaultAvatar from '../assets/avatar.jpg';
import { useUserStore } from '/@/store/modules/user';
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading']);
const { userInfo } = useUserStore();
const avatar = () => {
return userInfo?.avatar || defaultAvatar;
};
</script>
<style lang="less" scoped>
.chat {
display: flex;
margin-bottom: 1.5rem;
&.self {
flex-direction: row-reverse;
.avatar {
margin-right: 0;
margin-left: 10px;
}
.msgArea {
flex-direction: row-reverse;
}
.date {
text-align: right;
}
}
}
.avatar {
flex: none;
margin-right: 10px;
img {
width: 34px;
height: 34px;
border-radius: 50%;
overflow: hidden;
}
svg {
font-size: 28px;
}
}
.content {
.date {
color: #b4bbc4;
font-size: 0.75rem;
margin-bottom: 10px;
}
.msgArea {
display: flex;
}
}
</style>

View File

@ -0,0 +1,125 @@
<template>
<div class="textWrap" :class="[inversion ? 'self' : 'chatgpt']" ref="textRef">
<div v-if="!inversion">
<div class="markdown-body" :class="{ 'markdown-body-generate': loading }" v-html="text" />
</div>
<div v-else class="msg" v-text="text" />
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted, onUpdated, ref } from 'vue';
import MarkdownIt from 'markdown-it';
import mdKatex from '@traptitech/markdown-it-katex';
import mila from 'markdown-it-link-attributes';
import hljs from 'highlight.js';
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading']);
const textRef = ref();
const mdi = new MarkdownIt({
html: false,
linkify: true,
highlight(code, language) {
const validLang = !!(language && hljs.getLanguage(language));
if (validLang) {
const lang = language ?? '';
return highlightBlock(hljs.highlight(code, { language: lang }).value, lang);
}
return highlightBlock(hljs.highlightAuto(code).value, '');
},
});
mdi.use(mila, { attrs: { target: '_blank', rel: 'noopener' } });
mdi.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' });
const text = computed(() => {
const value = props.text ?? '';
if (!props.inversion) return mdi.render(value);
return value;
});
function highlightBlock(str: string, lang?: string) {
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">复制代码</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`;
}
function addCopyEvents() {
if (textRef.value) {
const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy');
copyBtn.forEach((btn) => {
btn.addEventListener('click', () => {
const code = btn.parentElement?.nextElementSibling?.textContent;
if (code) {
copyToClip(code).then(() => {
btn.textContent = '复制成功';
setTimeout(() => {
btn.textContent = '复制代码';
}, 1e3);
});
}
});
});
}
}
function removeCopyEvents() {
if (textRef.value) {
const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy');
copyBtn.forEach((btn) => {
btn.removeEventListener('click', () => {});
});
}
}
onMounted(() => {
addCopyEvents();
});
onUpdated(() => {
addCopyEvents();
});
onUnmounted(() => {
removeCopyEvents();
});
function copyToClip(text: string) {
return new Promise((resolve, reject) => {
try {
const input: HTMLTextAreaElement = document.createElement('textarea');
input.setAttribute('readonly', 'readonly');
input.value = text;
document.body.appendChild(input);
input.select();
if (document.execCommand('copy')) document.execCommand('copy');
document.body.removeChild(input);
resolve(text);
} catch (error) {
reject(error);
}
});
}
</script>
<style lang="less" scoped>
.textWrap {
border-radius: 0.375rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
font-size: 0.875rem;
line-height: 1.25rem;
}
.self {
// background-color: #d2f9d1;
background-color: @primary-color;
color: #fff;
overflow-wrap: break-word;
line-height: 1.625;
min-width: 20px;
}
.chatgpt {
background-color: #f4f6f8;
font-size: 0.875rem;
line-height: 1.25rem;
}
</style>

View File

@ -0,0 +1,282 @@
<template>
<div class="slide-wrap">
<div class="createArea">
<a-button type="dashed" @click="handleCreate">新建聊天</a-button>
</div>
<div class="historyArea">
<ul>
<li
v-for="item in dataSource.history"
:key="item.uuid"
class="list"
:class="[item.uuid == dataSource.active ? 'active' : 'normal', dataSource.history.length == 1 ? 'last' : '']"
@click="handleToggleChat(item)"
>
<i class="icon message">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--ri"
width="1em"
height="1em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M2 8.994A5.99 5.99 0 0 1 8 3h8c3.313 0 6 2.695 6 5.994V21H8c-3.313 0-6-2.695-6-5.994zM20 19V8.994A4.004 4.004 0 0 0 16 5H8a3.99 3.99 0 0 0-4 3.994v6.012A4.004 4.004 0 0 0 8 19zm-6-8h2v2h-2zm-6 0h2v2H8z"
></path>
</svg>
</i>
<a-input
class="title"
ref="inputRef"
v-if="item.isEdit"
:defaultValue="item.title"
placeholder="请输入标题"
@change="handleInputChange"
@blur="inputBlur(item)"
/>
<span class="title" v-else>{{ item.title }}</span>
<span class="icon edit" @click="handleEdit(item)" v-if="!item.isEdit">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--ri"
width="1em"
height="1em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M6.414 15.89L16.556 5.748l-1.414-1.414L5 14.476v1.414zm.829 2H3v-4.243L14.435 2.212a1 1 0 0 1 1.414 0l2.829 2.829a1 1 0 0 1 0 1.414zM3 19.89h18v2H3z"
></path>
</svg>
</span>
<span class="icon del" v-if="!item.isEdit">
<a-popconfirm title="确定删除此记录?" placement="bottom" ok-text="确定" cancel-text="取消" @confirm="handleDel(item)">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--ri"
width="1em"
height="1em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M17 6h5v2h-2v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V8H2V6h5V3a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1zm1 2H6v12h12zm-9 3h2v6H9zm4 0h2v6h-2zM9 4v2h6V4z"
></path>
</svg>
</a-popconfirm>
</span>
<span class="icon save" v-if="item.isEdit" @click="handleSave(item)">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--ri"
width="1em"
height="1em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M7 19v-6h10v6h2V7.828L16.172 5H5v14zM4 3h13l4 4v13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1m5 12v4h6v-4z"
></path>
</svg>
</span>
</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
const props = defineProps(['dataSource']);
const inputRef = ref(null);
let inputValue = '';
//
const handleCreate = () => {
const uuid = getUuid();
props.dataSource.history.unshift({ title: '新建聊天', uuid, isEdit: false });
props.dataSource.chat.unshift({ uuid, data: [] });
// ()
if (props.dataSource.history.length == 1) {
props.dataSource.active = uuid;
}
};
//
const handleToggleChat = (item) => {
if (item.uuid != props.dataSource.active) {
props.dataSource.active = item.uuid;
const findItem = props.dataSource.history.find((item) => item.isEdit);
if (findItem) {
handleSave(findItem);
}
}
};
const handleInputChange = (e) => {
inputValue = e.target.value.trim();
};
//
const inputBlur = (item) => {
item.isEdit = false;
item.title = inputValue;
};
//
const handleEdit = (item) => {
item.isEdit = true;
inputValue = item.title;
};
//
const handleSave = (item) => {
item.isEdit = false;
item.title = inputValue;
};
//
const handleDel = (data) => {
const findIndex = props.dataSource.history.findIndex((item) => item.uuid == data.uuid);
if (findIndex != -1) {
props.dataSource.history.splice(findIndex, 1);
props.dataSource.chat.splice(findIndex, 1);
// activeactive
if (props.dataSource.history.length) {
if (props.dataSource.active == data.uuid) {
if (findIndex > 0) {
props.dataSource.active = props.dataSource.history[findIndex - 1].uuid;
} else {
props.dataSource.active = props.dataSource.history[0].uuid;
}
}
} else {
//
props.dataSource.active = null;
}
}
};
watch(
() => inputRef.value,
(newVal: any) => {
if (newVal?.length) {
newVal[0].focus();
}
},
{ deep: true }
);
//
const getUuid = (len = 10, radix = 16) => {
var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
var uuid: any = [],
i;
radix = radix || chars.length;
if (len) {
for (i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * radix)];
} else {
var r;
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';
for (i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | (Math.random() * 16);
uuid[i] = chars[i == 19 ? (r & 0x3) | 0x8 : r];
}
}
}
return uuid.join('');
};
</script>
<style scoped lang="less">
.slide-wrap {
border-right: 1px solid #e5e7eb;
height: 100%;
display: flex;
flex-direction: column;
.historyArea {
padding: 20px;
padding-top: 0;
flex: 1;
min-height: 0;
overflow: auto;
&::-webkit-scrollbar {
width: 8px;
height: 8px;
}
}
.createArea {
padding: 20px;
padding-bottom: 0;
}
.ant-btn {
width: 100%;
margin-bottom: 10px;
}
}
ul {
margin-bottom: 0;
}
.list {
width: 100%;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
border-radius: 0.375rem;
border-width: 1px;
cursor: pointer;
margin-bottom: 10px;
color: #333;
display: flex;
justify-content: flex-start;
align-items: center;
&:hover,
&.active {
border-color: @primary-color;
color: @primary-color;
}
.edit,
.save,
.del {
display: none;
}
&.active {
.edit,
.save,
.del {
display: block;
}
&.last {
.del {
display: none;
}
}
}
.message {
margin-right: 8px;
}
.edit {
margin-right: 8px;
}
.title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.ant-input {
margin-right: 20px;
}
}
svg {
vertical-align: middle;
}
}
</style>

View File

@ -0,0 +1,186 @@
export const localData = {
active: 1002,
usingContext: true,
history: [
{
title: '标题02',
uuid: 1706083575869,
isEdit: false,
},
{
uuid: 1002,
title: '标题01',
isEdit: false,
},
],
chat: [
{
uuid: 1706083575869,
data: [
{
dateTime: '2024/1/24 16:06:27',
text: '',
inversion: true,
error: false,
conversationOptions: null,
requestOptions: {
prompt: '',
options: null,
},
},
{
dateTime: '2024/1/24 16:06:29',
text: 'Hello! How can I assist you today?',
inversion: false,
error: false,
loading: false,
conversationOptions: {
parentMessageId: 'chatcmpl-8kSZA0wju7X8sOdJIyxtpDj0RQVu1',
},
requestOptions: {
prompt: '',
options: {},
},
},
],
},
{
uuid: 1002,
data: [
{
dateTime: '2024/1/24 14:01:52',
text: '1',
inversion: true,
error: false,
conversationOptions: null,
requestOptions: {
prompt: '1',
options: null,
},
},
{
dateTime: '2024/1/24 14:01:54',
text: 'Yes, how can I assist you?',
inversion: false,
error: false,
loading: false,
conversationOptions: {
parentMessageId: 'chatcmpl-8kQcb6mbF04o5hpule4SdHk2jFvNQ',
},
requestOptions: {
prompt: '1',
options: {},
},
},
{
dateTime: '2024/1/24 14:03:45',
text: '',
inversion: true,
error: false,
conversationOptions: null,
requestOptions: {
prompt: '',
options: null,
},
},
{
dateTime: '2024/1/24 14:03:47',
text: "I'm sorry if my previous response was not clear. Please let me know how I can help you or what you would like to discuss.",
inversion: false,
error: false,
loading: false,
conversationOptions: {
parentMessageId: 'chatcmpl-8kQeQ2t8YCXmLeF0ECGkkuOJlk4Pi',
},
requestOptions: {
prompt: '',
options: {
parentMessageId: 'chatcmpl-8kQcb6mbF04o5hpule4SdHk2jFvNQ',
},
},
},
{
dateTime: '2024/1/24 14:10:19',
text: 'js 递归',
inversion: true,
error: false,
conversationOptions: null,
requestOptions: {
prompt: 'js 递归',
options: null,
},
},
{
dateTime: '2024/1/24 14:10:33',
text: 'JavaScript supports recursion, which is the process of a function calling itself. Recursion can be useful for solving problems that can be broken down into smaller, similar sub-problems.\n\nHere\'s an example of a simple recursive function in JavaScript:\n\n```javascript\nfunction countdown(n) {\n if (n <= 0) {\n console.log("Done!");\n } else {\n console.log(n);\n countdown(n - 1); // recursive call\n }\n}\n\ncountdown(5);\n```\n\nIn this example, the `countdown` function takes an argument `n` and logs the value of `n` to the console. If `n` is greater than zero, it then calls itself with `n - 1`. This process continues until `n` becomes less than or equal to zero, at which point it logs "Done!".\n\nRecursion can be helpful in solving problems that involve tree structures, factorial calculations, searching algorithms, and more. However, it\'s important to use recursion properly to avoid infinite loops or excessive stack usage.',
inversion: false,
error: false,
loading: false,
conversationOptions: {
parentMessageId: 'chatcmpl-8kQkmCbnRe4fG1FhWTlY0EyHTpqau',
},
requestOptions: {
prompt: 'js 递归',
options: {
parentMessageId: 'chatcmpl-8kQeQ2t8YCXmLeF0ECGkkuOJlk4Pi',
},
},
},
{
dateTime: '2024/1/24 14:17:15',
text: 'js 递归',
inversion: true,
error: false,
conversationOptions: null,
requestOptions: {
prompt: 'js 递归',
options: null,
},
},
{
dateTime: '2024/1/24 14:23:50',
text: "Certainly! Here's an example of how you can use recursion in JavaScript:\n\n```javascript\nfunction factorial(n) {\n if (n === 0) {\n return 1;\n } else {\n return n * factorial(n - 1);\n }\n}\n\nconsole.log(factorial(5)); // Output: 120\n```\n\nIn this example, the `factorial` function calculates the factorial of a given number `n` using recursion. If `n` is equal to 0, it returns 1, which is the base case. Otherwise, it recursively calls itself with `n - 1`, multiplying the current value of `n` with the result of the recursive call.\n\nWhen calling `factorial(5)`, the function will execute as follows:\n\n- `factorial(5)` calls `factorial(4)`\n- `factorial(4)` calls `factorial(3)`\n- `factorial(3)` calls `factorial(2)`\n- `factorial(2)` calls `factorial(1)`\n- `factorial(1)` calls `factorial(0)`\n- `factorial(0)` returns 1\n- `factorial(1)` returns 1 * 1 = 1\n- `factorial(2)` returns 2 * 1 = 2\n- `factorial(3)` returns 3 * 2 = 6\n- `factorial(4)` returns 4 * 6 = 24\n- `factorial(5)` returns 5 * 24 = 120\n\nThe final result is then printed to the console using `console.log`.",
inversion: false,
error: false,
loading: false,
conversationOptions: {
parentMessageId: 'chatcmpl-8kQwWVoZoWyqjbWuwMJmu6w3hBvXj',
},
requestOptions: {
prompt: 'js 递归',
options: {
parentMessageId: 'chatcmpl-8kQkmCbnRe4fG1FhWTlY0EyHTpqau',
},
},
},
{
dateTime: '2024/1/24 15:05:30',
text: '///',
inversion: true,
error: false,
conversationOptions: null,
requestOptions: {
prompt: '///',
options: null,
},
},
{
dateTime: '2024/1/24 15:05:33',
text: "I apologize if my previous response was not what you were expecting. If you have any specific questions or need further assistance, please let me know and I'll be happy to help.",
inversion: false,
error: false,
loading: false,
conversationOptions: {
parentMessageId: 'chatcmpl-8kRcAggkC4u47d34UcQW3cI0htw0w',
},
requestOptions: {
prompt: '///',
options: {
parentMessageId: 'chatcmpl-8kQwWVoZoWyqjbWuwMJmu6w3hBvXj',
},
},
},
],
},
],
};

View File

@ -0,0 +1,28 @@
import { useChatStore } from '@/store'
export function useChat() {
const chatStore = useChatStore()
const getChatByUuidAndIndex = (uuid: number, index: number) => {
return chatStore.getChatByUuidAndIndex(uuid, index)
}
const addChat = (uuid: number, chat: Chat.Chat) => {
chatStore.addChatByUuid(uuid, chat)
}
const updateChat = (uuid: number, index: number, chat: Chat.Chat) => {
chatStore.updateChatByUuid(uuid, index, chat)
}
const updateChatSome = (uuid: number, index: number, chat: Partial<Chat.Chat>) => {
chatStore.updateChatSomeByUuid(uuid, index, chat)
}
return {
addChat,
updateChat,
updateChatSome,
getChatByUuidAndIndex,
}
}

View File

@ -0,0 +1,44 @@
import type { Ref } from 'vue'
import { nextTick, ref } from 'vue'
type ScrollElement = HTMLDivElement | null
interface ScrollReturn {
scrollRef: Ref<ScrollElement>
scrollToBottom: () => Promise<void>
scrollToTop: () => Promise<void>
scrollToBottomIfAtBottom: () => Promise<void>
}
export function useScroll(): ScrollReturn {
const scrollRef = ref<ScrollElement>(null)
const scrollToBottom = async () => {
await nextTick()
if (scrollRef.value)
scrollRef.value.scrollTop = scrollRef.value.scrollHeight
}
const scrollToTop = async () => {
await nextTick()
if (scrollRef.value)
scrollRef.value.scrollTop = 0
}
const scrollToBottomIfAtBottom = async () => {
await nextTick()
if (scrollRef.value) {
const threshold = 100 // Threshold, indicating the distance threshold to the bottom of the scroll bar.
const distanceToBottom = scrollRef.value.scrollHeight - scrollRef.value.scrollTop - scrollRef.value.clientHeight
if (distanceToBottom <= threshold)
scrollRef.value.scrollTop = scrollRef.value.scrollHeight
}
}
return {
scrollRef,
scrollToBottom,
scrollToTop,
scrollToBottomIfAtBottom,
}
}

View File

@ -0,0 +1,203 @@
<template>
<div ref="chatContainerRef" class="chat-container" :style="chatContainerStyle">
<template v-if="dataSource">
<div class="leftArea" :class="[expand ? 'expand' : 'shrink']">
<div class="content">
<slide v-if="uuid" :dataSource="dataSource"></slide>
</div>
<div class="toggle-btn" @click="handleToggle">
<span class="icon">
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5.64645 3.14645C5.45118 3.34171 5.45118 3.65829 5.64645 3.85355L9.79289 8L5.64645 12.1464C5.45118 12.3417 5.45118 12.6583 5.64645 12.8536C5.84171 13.0488 6.15829 13.0488 6.35355 12.8536L10.8536 8.35355C11.0488 8.15829 11.0488 7.84171 10.8536 7.64645L6.35355 3.14645C6.15829 2.95118 5.84171 2.95118 5.64645 3.14645Z"
fill="currentColor"
></path>
</svg>
</span>
</div>
</div>
<div class="rightArea" :class="[expand ? 'expand' : 'shrink']">
<chat v-if="uuid && chatVisible" :uuid="uuid" :chatData="chatData" :dataSource="dataSource"></chat>
</div>
</template>
<Spin v-else :spinning="true"></Spin>
</div>
</template>
<script setup lang="ts">
import slide from './components/slide.vue';
import chat from './components/chat.vue';
import { Spin } from 'ant-design-vue';
import { ref, watch, nextTick, onUnmounted } from 'vue';
import { useUserStore } from '/@/store/modules/user';
import { JEECG_CHAT_KEY } from '/@/enums/cacheEnum';
import { defHttp } from '/@/utils/http/axios';
const configUrl = {
get: '/ai/chat/history/get',
save: '/ai/chat/history/save',
};
const userId = useUserStore().getUserInfo?.id;
const localKey = JEECG_CHAT_KEY + userId;
let timer: any = null;
let unwatch01: any = null;
let unwatch02: any = null;
const dataSource = ref<any>(null);
const uuid = ref(null);
const chatData = ref([]);
const expand = ref<any>(true);
const chatVisible = ref(true);
const chatContainerRef = ref<any>(null);
const chatContainerStyle = ref({});
const handleToggle = () => {
expand.value = !expand.value;
};
//
const init = () => {
defHttp
.get({ url: configUrl.get })
.then((res) => {
const { content } = res;
if (content) {
dataSource.value = JSON.parse(content);
} else {
dataSource.value = {
active: 1002,
usingContext: true,
history: [{ uuid: 1002, title: '新建聊天', isEdit: false }],
chat: [{ uuid: 1002, data: [] }],
};
}
!unwatch01 && execute();
})
.catch(() => {});
};
const save = (content) => {
defHttp.post({ url: configUrl.save, params: { content: JSON.stringify(content) } }, { isTransformResponse: false });
};
// dataSource
const execute = () => {
unwatch01 = watch(
() => dataSource.value.active,
(value) => {
if (value) {
const findItem = dataSource.value.chat.find((item) => item.uuid === value);
if (findItem) {
uuid.value = findItem.uuid;
chatData.value = findItem.data;
}
chatVisible.value = false;
nextTick(() => {
chatVisible.value = true;
});
}
},
{ immediate: true }
);
unwatch02 = watch(dataSource.value, () => {
clearInterval(timer);
timer = setTimeout(() => {
save(dataSource.value);
}, 2e3);
});
};
onUnmounted(() => {
unwatch01 && unwatch01();
unwatch02 && unwatch02();
});
watch(
() => chatContainerRef.value,
() => {
chatContainerStyle.value = { height: `${chatContainerRef.value.offsetHeight}px` };
}
);
init();
</script>
<style scoped lang="less">
@width: 260px;
.chat-container {
position: relative;
height: 100%;
box-shadow:
0 0 #0000,
0 0 #0000,
0 0 #0000,
0 0 #0000,
0 4px 6px -1px rgb(0 0 0 / 0.1),
0 2px 4px -2px rgb(0 0 0 / 0.1);
border-width: 1px;
border-radius: 0.375rem;
display: flex;
overflow: hidden;
:deep(.ant-spin) {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
.leftArea {
width: @width;
transition: 0.3s left;
position: absolute;
left: 0;
height: 100%;
.content {
width: 100%;
height: 100%;
overflow: hidden;
}
&.shrink {
left: -@width;
.toggle-btn {
.icon {
transform: rotate(0deg);
}
}
}
.toggle-btn {
transition:
color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
right 0.3s cubic-bezier(0.4, 0, 0.2, 1),
left 0.3s cubic-bezier(0.4, 0, 0.2, 1),
border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
width: 24px;
height: 24px;
position: absolute;
top: 50%;
right: 0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: rgb(51, 54, 57);
border: 1px solid rgb(239, 239, 245);
background-color: #fff;
box-shadow: 0 2px 4px 0px rgba(0, 0, 0, 0.06);
transform: translateX(50%) translateY(-50%);
z-index: 1;
}
.icon {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform: rotate(180deg);
font-size: 18px;
height: 18px;
svg {
height: 1em;
width: 1em;
vertical-align: top;
}
}
}
.rightArea {
margin-left: @width;
transition: 0.3s margin-left;
&.shrink {
margin-left: 0;
}
flex: 1;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,206 @@
html.dark {
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em
}
code.hljs {
padding: 3px 5px
}
.hljs {
color: #abb2bf;
background: #282c34
}
.hljs-keyword,
.hljs-operator,
.hljs-pattern-match {
color: #f92672
}
.hljs-function,
.hljs-pattern-match .hljs-constructor {
color: #61aeee
}
.hljs-function .hljs-params {
color: #a6e22e
}
.hljs-function .hljs-params .hljs-typing {
color: #fd971f
}
.hljs-module-access .hljs-module {
color: #7e57c2
}
.hljs-constructor {
color: #e2b93d
}
.hljs-constructor .hljs-string {
color: #9ccc65
}
.hljs-comment,
.hljs-quote {
color: #b18eb1;
font-style: italic
}
.hljs-doctag,
.hljs-formula {
color: #c678dd
}
.hljs-deletion,
.hljs-name,
.hljs-section,
.hljs-selector-tag,
.hljs-subst {
color: #e06c75
}
.hljs-literal {
color: #56b6c2
}
.hljs-addition,
.hljs-attribute,
.hljs-meta .hljs-string,
.hljs-regexp,
.hljs-string {
color: #98c379
}
.hljs-built_in,
.hljs-class .hljs-title,
.hljs-title.class_ {
color: #e6c07b
}
.hljs-attr,
.hljs-number,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-pseudo,
.hljs-template-variable,
.hljs-type,
.hljs-variable {
color: #d19a66
}
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-symbol,
.hljs-title {
color: #61aeee
}
.hljs-emphasis {
font-style: italic
}
.hljs-strong {
font-weight: 700
}
.hljs-link {
text-decoration: underline
}
}
html {
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em
}
code.hljs {
padding: 3px 5px;
&::-webkit-scrollbar {
height: 4px;
}
}
.hljs {
color: #383a42;
background: #fafafa
}
.hljs-comment,
.hljs-quote {
color: #a0a1a7;
font-style: italic
}
.hljs-doctag,
.hljs-formula,
.hljs-keyword {
color: #a626a4
}
.hljs-deletion,
.hljs-name,
.hljs-section,
.hljs-selector-tag,
.hljs-subst {
color: #e45649
}
.hljs-literal {
color: #0184bb
}
.hljs-addition,
.hljs-attribute,
.hljs-meta .hljs-string,
.hljs-regexp,
.hljs-string {
color: #50a14f
}
.hljs-attr,
.hljs-number,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-pseudo,
.hljs-template-variable,
.hljs-type,
.hljs-variable {
color: #986801
}
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-symbol,
.hljs-title {
color: #4078f2
}
.hljs-built_in,
.hljs-class .hljs-title,
.hljs-title.class_ {
color: #c18401
}
.hljs-emphasis {
font-style: italic
}
.hljs-strong {
font-weight: 700
}
.hljs-link {
text-decoration: underline
}
}

View File

@ -0,0 +1,135 @@
.markdown-body {
background-color: transparent;
font-size: 14px;
p {
white-space: pre-wrap;
}
ol {
list-style-type: decimal;
}
ul {
list-style-type: disc;
}
pre code,
pre tt {
line-height: 1.65;
}
.highlight pre,
pre {
background-color: #fff;
}
code.hljs {
padding: 0;
}
.code-block {
&-wrapper {
position: relative;
padding-top: 24px;
}
&-header {
position: absolute;
top: 5px;
right: 0;
width: 100%;
padding: 0 1rem;
display: flex;
justify-content: flex-end;
align-items: center;
color: #b3b3b3;
&__copy {
cursor: pointer;
margin-left: 0.5rem;
user-select: none;
&:hover {
color: #65a665;
}
}
}
}
&.markdown-body-generate>dd:last-child:after,
&.markdown-body-generate>dl:last-child:after,
&.markdown-body-generate>dt:last-child:after,
&.markdown-body-generate>h1:last-child:after,
&.markdown-body-generate>h2:last-child:after,
&.markdown-body-generate>h3:last-child:after,
&.markdown-body-generate>h4:last-child:after,
&.markdown-body-generate>h5:last-child:after,
&.markdown-body-generate>h6:last-child:after,
&.markdown-body-generate>li:last-child:after,
&.markdown-body-generate>ol:last-child li:last-child:after,
&.markdown-body-generate>p:last-child:after,
&.markdown-body-generate>pre:last-child code:after,
&.markdown-body-generate>td:last-child:after,
&.markdown-body-generate>ul:last-child li:last-child:after {
animation: blink 1s steps(5, start) infinite;
color: #000;
content: '_';
font-weight: 700;
margin-left: 3px;
vertical-align: baseline;
}
@keyframes blink {
to {
visibility: hidden;
}
}
}
html.dark {
.markdown-body {
&.markdown-body-generate>dd:last-child:after,
&.markdown-body-generate>dl:last-child:after,
&.markdown-body-generate>dt:last-child:after,
&.markdown-body-generate>h1:last-child:after,
&.markdown-body-generate>h2:last-child:after,
&.markdown-body-generate>h3:last-child:after,
&.markdown-body-generate>h4:last-child:after,
&.markdown-body-generate>h5:last-child:after,
&.markdown-body-generate>h6:last-child:after,
&.markdown-body-generate>li:last-child:after,
&.markdown-body-generate>ol:last-child li:last-child:after,
&.markdown-body-generate>p:last-child:after,
&.markdown-body-generate>pre:last-child code:after,
&.markdown-body-generate>td:last-child:after,
&.markdown-body-generate>ul:last-child li:last-child:after {
color: #65a665;
}
}
.message-reply {
.whitespace-pre-wrap {
white-space: pre-wrap;
color: var(--n-text-color);
}
}
.highlight pre,
pre {
background-color: #282c34;
}
}
@media screen and (max-width: 533px) {
.markdown-body .code-block-wrapper {
padding: unset;
code {
padding: 24px 16px 16px 16px;
}
}
}

View File

@ -1,4 +1,4 @@
export { default as JVxeTable } from './src/JVxeTable';
export { registerJVxeTable } from './src/install';
export { deleteComponent } from './src/componentMap';
export { registerComponent, registerAsyncComponent } from './src/utils/registerUtils';
export { registerComponent, registerAsyncComponent, registerASyncComponentReal } from './src/utils/registerUtils';

View File

@ -30,6 +30,17 @@
target.selectionStart = selectionStart - 1;
target.selectionEnd = selectionStart - 1;
}
} else {
// update-begin--author:liaozhiyang---date:20240227---forQQYUN-83470
// 41.1 -> 41.10, 100.1 -> 100.10 handleChangeCommon
if (value.indexOf('.') != -1) {
const result = value.split('.').pop();
if (result && result.length >= 2 && result.substr(-1) === '0') {
change = false;
innerValue.value = value;
}
}
// update-end--author:liaozhiyang---date:20240227---forQQYUN-83470
}
}
//

View File

@ -17,6 +17,19 @@ export function isRegistered(type: JVxeTypes | string) {
return componentMap.has(type);
}
/**
* 2024-03-08
* liaozhiyang
* vxe自定义组件
* QQYUN-8241
* @param type
* @param promise
*/
export function registerASyncComponentReal(type: JVxeTypes, component) {
addComponent(type, component);
registerOneComponent(type);
}
/**
* vxe自定义组件
*

View File

@ -24,7 +24,7 @@
</div>
<upload-chunk ref="uploadRef" :visible="uploadVisible" @select="selectFirstFile"></upload-chunk>
</div>
<UserSelectModal labelKey="realname" rowKey="username" @register="registerModal" @getSelectResult="setValue" isRadioSelection></UserSelectModal>
<UserSelectModal rowKey="username" @register="registerModal" @selected="setValue" :multi="false"></UserSelectModal>
<a-modal v-model:open="visibleEmoji" :footer="null" wrapClassName="emoji-modal" :closable="false" :width="490">
<template #title>
<span></span>
@ -49,7 +49,7 @@
import { propTypes } from '/@/utils/propTypes';
import { UserAddOutlined, PaperClipOutlined, SmileOutlined } from '@ant-design/icons-vue';
import { Tooltip } from 'ant-design-vue';
import UserSelectModal from '/@/components/Form/src/jeecg/components/modal/UserSelectModal.vue';
import UserSelectModal from '/@/components/Form/src/jeecg/components/userSelect/UserSelectModal.vue';
import { useModal } from '/@/components/Modal';
import UploadChunk from './UploadChunk.vue';
import 'emoji-mart-vue-fast/css/emoji-mart.css';
@ -91,7 +91,7 @@
const uploadVisible = ref(false);
const uploadRef = ref();
//model
const [registerModal, { openModal }] = useModal();
const [registerModal, { openModal, closeModal }] = useModal();
const buttonLoading = ref(false);
const myComment = ref<string>('');
function sendComment() {
@ -149,21 +149,27 @@
function setValue(options) {
console.log('setValue', options);
if (options && options.length > 0) {
const { label, value } = options[0];
if (label && value) {
let str = `${label}[${value}]`;
const { realname, username } = options[0];
if (realname && username) {
let str = `${realname}[${username}]`;
let temp = myComment.value;
if (!temp) {
myComment.value = '@' + str;
} else {
if (temp.endsWith('@')) {
myComment.value = temp + str;
myComment.value = temp + str +' ';
} else {
myComment.value = '@' + str + ' ' + temp;
myComment.value = '@' + str + ' ' + temp + ' ';
}
}
//update-begin---author:wangshuai---date:2024-01-22---for:QQYUN-8002---
showHtml.value = false;
commentRef.value.focus();
commentActive.value = true;
//update-end---author:wangshuai---date:2024-01-22---for:QQYUN-8002---
}
}
closeModal();
}
function handleCommentChange() {

View File

@ -1,8 +1,8 @@
import type { App } from 'vue';
import { Icon } from './Icon';
import AIcon from '/@/components/jeecg/AIcon.vue';
//Tinymce富文本
import Editor from '/@/components/Tinymce/src/Editor.vue'
// //Tinymce富文本
// import Editor from '/@/components/Tinymce/src/Editor.vue'
import { Button, JUploadButton } from './Button';
@ -57,9 +57,11 @@ import {
Skeleton,
Cascader,
Rate,
Progress
} from 'ant-design-vue';
const compList = [AntButton.Group, Icon, AIcon, JUploadButton];
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
export function registerGlobComp(app: App) {
compList.forEach((comp) => {
@ -67,7 +69,13 @@ export function registerGlobComp(app: App) {
});
//仪表盘依赖Tinymce需要提前加载没办法按需加载了
app.component(Editor.name, Editor);
//app.component(Editor.name, Editor);
app.component(
'Tinymce',
createAsyncComponent(() => import('./Tinymce/src/Editor.vue'), {
loading: true,
})
);
app.use(Select)
.use(Alert)
@ -116,5 +124,7 @@ export function registerGlobComp(app: App) {
.use(Popconfirm)
.use(Skeleton)
.use(Cascader)
.use(Rate);
.use(Rate)
.use(Progress);
console.log("---初始化---全局注册Antd等组件--------------")
}

View File

@ -21,13 +21,17 @@
}
&-primary {
color: @white;
background-color: @button-primary-color;
// update-begin--author:liaozhiyang---date:20240223---for【QQYUN-8327】btn样式显示不正确
// color: @white;
// background-color: @button-primary-color;
// update-end--author:liaozhiyang---date:20240223---for【QQYUN-8327】btn样式显示不正确
&:hover,
&:focus {
color: @white;
background-color: @button-primary-hover-color;
// update-begin--author:liaozhiyang---date:20240223---for【QQYUN-8327】btn样式显示不正确
// color: @white;
// background-color: @button-primary-hover-color;
// update-end--author:liaozhiyang---date:20240223---for【QQYUN-8327】btn样式显示不正确
}
//
//&[disabled],
@ -47,16 +51,18 @@
//}
&-default {
color: @button-cancel-color;
background-color: @button-cancel-bg-color;
border-color: @button-cancel-border-color;
// update-begin--author:liaozhiyang---date:20240223---for【QQYUN-8327】btn样式显示不正确
// color: @button-cancel-color;
// background-color: @button-cancel-bg-color;
// border-color: @button-cancel-border-color;
&:hover,
&:focus {
color: @button-cancel-hover-color;
background-color: @button-cancel-hover-bg-color;
border-color: @button-cancel-hover-border-color;
}
// &:hover,
// &:focus {
// color: @button-cancel-hover-color;
// background-color: @button-cancel-hover-bg-color;
// border-color: @button-cancel-hover-border-color;
// }
// update-end--author:liaozhiyang---date:20240223---for【QQYUN-8327】btn样式显示不正确
//
//&[disabled],
//&[disabled]:hover {

View File

@ -1,6 +1,8 @@
@import './pagination.less';
@import './input.less';
//@import './btn.less';
// update-begin--author:liaozhiyang---date:20240130---for【issues/5857】Button color类型颜色失效
@import './btn.less';
// update-end--author:liaozhiyang---date:20240130---for【issues/5857】Button color类型颜色失效
// @import './table.less';
// TODO beta.11 fix
@ -159,4 +161,4 @@ html[data-theme='dark'] .ant-input-affix-wrapper-textarea-with-clear-btn {
margin: 0 4px;
}
}
// update-end--author:liaozhiyang---date:20230108---for【QQYUN-7855】table页码同步3.x页面效果
// update-end--author:liaozhiyang---date:20230108---for【QQYUN-7855】table页码同步3.x页面效果

View File

@ -38,6 +38,12 @@ export const JEECG_CHAT_UID = 'JEECG_CHAT_UID';
// 免登录租户id与系统分开避免重复
export const OAUTH2_THIRD_LOGIN_TENANT_ID = 'THIRD_LOGIN_TENANT_ID';
// ai助手标识退出需要记录一下
export const AIDE_FLAG = 'AIDE_FLAG';
// ai助手标识退出需要记录一下
export const JEECG_CHAT_KEY = 'JEECG-CHAT-KEY';
export enum CacheTypeEnum {
SESSION,
LOCAL,

View File

@ -2,7 +2,7 @@ export enum PageEnum {
// basic login path
BASE_LOGIN = '/login',
// basic home path
BASE_HOME = '/cet/welcomeInterface',
BASE_HOME = '/dashboard/analysis',
// error page path
ERROR_PAGE = '/exception',
// error log page path

View File

@ -281,7 +281,6 @@ export function useListTable(tableProps: TableProps): [
// 发送请求之前调用的方法
function beforeFetch(params) {
// 默认以 createTime 降序排序
console.log('tableProps', tableProps);
return Object.assign({ column: 'createTime', order: 'desc' }, params);
}

View File

@ -21,7 +21,9 @@ export function useMethods() {
* @param url
*/
async function exportXls(name, url, params, isXlsx = false) {
const data = await defHttp.get({ url: url, params: params, responseType: 'blob' }, { isTransformResponse: false });
//update-begin---author:wangshuai---date:2024-01-25---for:【QQYUN-8118】导出超时时间设置长点---
const data = await defHttp.get({ url: url, params: params, responseType: 'blob', timeout: 60000 }, { isTransformResponse: false });
//update-end---author:wangshuai---date:2024-01-25---for:【QQYUN-8118】导出超时时间设置长点---
if (!data) {
createMessage.warning('文件下载失败');
return;

View File

@ -1,4 +1,5 @@
import { watch, unref } from 'vue';
import type {Menu} from "@/router/types";
import { ref, watch, unref } from 'vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { useTitle as usePageTitle } from '@vueuse/core';
import { useGlobSetting } from '/@/hooks/setting';
@ -18,9 +19,11 @@ export function useTitle() {
const pageTitle = usePageTitle();
const menus = ref<Menu[] | null>(null)
watch(
[() => currentRoute.value.path, () => localeStore.getLocale],
() => {
async () => {
const route = unref(currentRoute);
if (route.name === REDIRECT_NAME) {
@ -28,16 +31,17 @@ export function useTitle() {
}
// update-begin--author:liaozhiyang---date:20231110---for【QQYUN-6938】online菜单名字和页面title不一致
if (route.params && Object.keys(route.params).length) {
getMenus().then((menus) => {
const getTitle = getMatchingRouterName(menus, route.fullPath);
let tTitle = '';
if (getTitle) {
tTitle = t(getTitle);
} else {
tTitle = t(route?.meta?.title as string);
};
pageTitle.value = tTitle ? ` ${tTitle} - ${title} ` : `${title}`;
});
if (!menus.value) {
menus.value = await getMenus();
}
const getTitle = getMatchingRouterName(menus.value, route.fullPath);
let tTitle = '';
if (getTitle) {
tTitle = t(getTitle);
} else {
tTitle = t(route?.meta?.title as string);
}
pageTitle.value = tTitle ? ` ${tTitle} - ${title} ` : `${title}`;
} else {
const tTitle = t(route?.meta?.title as string);
pageTitle.value = tTitle ? ` ${tTitle} - ${title} ` : `${title}`;

View File

@ -1,5 +1,5 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" title="修改密码" @ok="handleSubmit" width="600px">
<BasicModal v-bind="$attrs" @register="registerModal" :title="title" @ok="handleSubmit" width="600px">
<BasicForm @register="registerForm" />
</BasicModal>
</template>
@ -11,43 +11,52 @@
import BasicForm from '/@/components/Form/src/BasicForm.vue';
import { useForm } from '/@/components/Form/src/hooks/useForm';
import { useMessage } from '/@/hooks/web/useMessage';
import { useLocaleStore } from '/@/store/modules/locale';
import { useI18n } from '/@/hooks/web/useI18n';
const localeStore = useLocaleStore();
const { t } = useI18n();
// Emits
const emit = defineEmits(['register']);
const $message = useMessage();
const formRef = ref();
const username = ref('');
// update-begin--author:liaozhiyang---date:20240124---forQQYUN-7970
const title = ref(t('layout.changePassword.changePassword'));
//
const [registerForm, { resetFields, validate, clearValidate }] = useForm({
schemas: [
{
label: '旧密码',
label: t('layout.changePassword.oldPassword'),
field: 'oldpassword',
component: 'InputPassword',
required: true,
},
{
label: '新密码',
label: t('layout.changePassword.newPassword'),
field: 'password',
component: 'StrengthMeter',
componentProps: {
placeholder: '请输入新密码',
placeholder: t('layout.changePassword.pleaseEnterNewPassword'),
},
rules: [
{
required: true,
message: '请输入新密码',
message: t('layout.changePassword.pleaseEnterNewPassword'),
},
],
},
{
label: '确认新密码',
label: t('layout.changePassword.confirmNewPassword'),
field: 'confirmpassword',
component: 'InputPassword',
dynamicRules: ({ values }) => rules.confirmPassword(values, true),
},
],
showActionButtonGroup: false,
wrapperCol: null,
labelWidth: localeStore.getLocale == 'zh_CN' ? 100 : 160,
});
// update-end--author:liaozhiyang---date:20240124---forQQYUN-7970
//
const [registerModal, { setModalProps, closeModal }] = useModalInner();
@ -85,6 +94,7 @@
}
defineExpose({
title,
show,
});
</script>

View File

@ -11,7 +11,12 @@
<template #overlay>
<Menu @click="handleMenuClick">
<MenuItem key="doc" :text="t('layout.header.dropdownItemDoc')" icon="ion:document-text-outline" v-if="getShowDoc" />
<MenuDivider v-if="getShowDoc" />
<MenuItem key="account" :text="t('layout.header.dropdownItemSwitchAccount')" icon="ant-design:setting-outlined" />
<MenuItem key="password" :text="t('layout.header.dropdownItemSwitchPassword')" icon="ant-design:edit-outlined" />
<MenuItem key="depart" :text="t('layout.header.dropdownItemSwitchDepart')" icon="ant-design:cluster-outlined" />
<MenuItem key="cache" :text="t('layout.header.dropdownItemRefreshCache')" icon="ion:sync-outline" />
<!-- <MenuItem
v-if="getUseLockPage"
key="lock"
@ -121,9 +126,13 @@
const res = await queryAllDictItems();
removeAuthCache(DB_DICT_DATA_KEY);
setAuthCache(DB_DICT_DATA_KEY, res.result);
createMessage.success('刷新缓存完成!');
// update-begin--author:liaozhiyang---date:20240124---forQQYUN-7970
createMessage.success(t('layout.header.refreshCacheComplete'));
// update-end--author:liaozhiyang---date:20240124---forQQYUN-7970
} else {
createMessage.error('刷新缓存失败!');
// update-begin--author:liaozhiyang---date:20240124---forQQYUN-7970
createMessage.error(t('layout.header.refreshCacheFailure'));
// update-end--author:liaozhiyang---date:20240124---forQQYUN-7970
}
}
//

View File

@ -105,7 +105,7 @@
&-action {
display: flex;
min-width: 150px;
min-width: 180px;
// padding-right: 12px;
align-items: center;

View File

@ -3,11 +3,15 @@
<!-- left start -->
<div :class="`${prefixCls}-left`">
<!-- logo -->
<AppLogo v-if="getShowHeaderLogo || getIsMobile" :class="`${prefixCls}-logo`" :theme="getHeaderTheme"
:style="getLogoWidth" />
<LayoutTrigger v-if="(getShowContent && getShowHeaderTrigger && !getSplit && !getIsMixSidebar) || getIsMobile"
:theme="getHeaderTheme" :sider="false" />
<AppLogo v-if="getShowHeaderLogo || getIsMobile" :class="`${prefixCls}-logo`" :theme="getHeaderTheme" :style="getLogoWidth" />
<LayoutTrigger
v-if="(getShowContent && getShowHeaderTrigger && !getSplit && !getIsMixSidebar) || getIsMobile"
:theme="getHeaderTheme"
:sider="false"
/>
<LayoutBreadcrumb v-if="getShowContent && getShowBread" :theme="getHeaderTheme" />
<!-- 欢迎语 -->
<span v-if="getShowContent && getShowBreadTitle && !getIsMobile" :class="[prefixCls, `${prefixCls}--${getHeaderTheme}`,'headerIntroductionClass']"> {{t('layout.header.welcomeIn')}} {{ title }} </span>
</div>
<!-- left end -->
@ -19,226 +23,236 @@
<!-- action -->
<div :class="`${prefixCls}-action`">
<AppSearch :class="`${prefixCls}-action__item `" v-if="getShowSearch" />
<ErrorAction v-if="getUseErrorHandle" :class="`${prefixCls}-action__item error-action`" />
<Notify v-if="getShowNotice" :class="`${prefixCls}-action__item notify-item`" />
<FullScreen v-if="getShowFullScreen" :class="`${prefixCls}-action__item fullscreen-item`" />
<LockScreen v-if="getUseLockPage" />
<AppLocalePicker v-if="getShowLocalePicker" :reload="true" :showText="false" :class="`${prefixCls}-action__item`" />
<UserDropDown :theme="getHeaderTheme" />
<SettingDrawer v-if="getShowSetting" :class="`${prefixCls}-action__item`" />
<SettingDrawer v-if="getShowSetting" :class="`${prefixCls}-action__item`" />
<!-- ai助手 -->
<Aide></Aide>
</div>
</Header>
<LoginSelect ref="loginSelectRef" @success="loginSelectOk"></LoginSelect>
</template>
<script lang="ts">
import { defineComponent, unref, computed, ref, onMounted, toRaw } from 'vue';
import { useGlobSetting } from '/@/hooks/setting';
import { propTypes } from '/@/utils/propTypes';
import { defineComponent, unref, computed, ref, onMounted, toRaw } from 'vue';
import { useGlobSetting } from '/@/hooks/setting';
import { propTypes } from '/@/utils/propTypes';
import { Layout } from 'ant-design-vue';
import { AppLogo } from '/@/components/Application';
import LayoutMenu from '../menu/index.vue';
import LayoutTrigger from '../trigger/index.vue';
import { Layout } from 'ant-design-vue';
import { AppLogo } from '/@/components/Application';
import LayoutMenu from '../menu/index.vue';
import LayoutTrigger from '../trigger/index.vue';
import { AppSearch } from '/@/components/Application';
import { AppSearch } from '/@/components/Application';
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
import { SettingButtonPositionEnum } from '/@/enums/appEnum';
import { AppLocalePicker } from '/@/components/Application';
import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
import { SettingButtonPositionEnum } from '/@/enums/appEnum';
import { AppLocalePicker } from '/@/components/Application';
import { UserDropDown, LayoutBreadcrumb, FullScreen, Notify, ErrorAction, LockScreen } from './components';
import { useAppInject } from '/@/hooks/web/useAppInject';
import { useDesign } from '/@/hooks/web/useDesign';
import { UserDropDown, LayoutBreadcrumb, FullScreen, Notify, ErrorAction, LockScreen } from './components';
import { useAppInject } from '/@/hooks/web/useAppInject';
import { useDesign } from '/@/hooks/web/useDesign';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import { useLocale } from '/@/locales/useLocale';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import { useLocale } from '/@/locales/useLocale';
import LoginSelect from '/@/views/sys/login/LoginSelect.vue';
import { useUserStore } from '/@/store/modules/user';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
import LoginSelect from '/@/views/sys/login/LoginSelect.vue';
import { useUserStore } from '/@/store/modules/user';
import { useI18n } from '/@/hooks/web/useI18n';
import Aide from "@/views/dashboard/ai/components/aide/index.vue"
const { t } = useI18n();
export default defineComponent({
name: 'LayoutHeader',
components: {
Header: Layout.Header,
AppLogo,
LayoutTrigger,
LayoutBreadcrumb,
LayoutMenu,
UserDropDown,
AppLocalePicker,
FullScreen,
Notify,
AppSearch,
ErrorAction,
LockScreen,
LoginSelect,
SettingDrawer: createAsyncComponent(() => import('/@/layouts/default/setting/index.vue'), {
loading: true,
}),
},
props: {
fixed: propTypes.bool,
},
setup(props) {
const { prefixCls } = useDesign('layout-header');
const userStore = useUserStore();
const { getShowTopMenu, getShowHeaderTrigger, getSplit, getIsMixMode, getMenuWidth, getIsMixSidebar } = useMenuSetting();
const { getUseErrorHandle, getShowSettingButton, getSettingButtonPosition } = useRootSetting();
const { title } = useGlobSetting();
export default defineComponent({
name: 'LayoutHeader',
components: {
Header: Layout.Header,
AppLogo,
LayoutTrigger,
LayoutBreadcrumb,
LayoutMenu,
UserDropDown,
AppLocalePicker,
FullScreen,
Notify,
AppSearch,
ErrorAction,
LockScreen,
LoginSelect,
SettingDrawer: createAsyncComponent(() => import('/@/layouts/default/setting/index.vue'), {
loading: true,
}),
Aide
},
props: {
fixed: propTypes.bool,
},
setup(props) {
const { prefixCls } = useDesign('layout-header');
const userStore = useUserStore();
const { getShowTopMenu, getShowHeaderTrigger, getSplit, getIsMixMode, getMenuWidth, getIsMixSidebar } = useMenuSetting();
const { getUseErrorHandle, getShowSettingButton, getSettingButtonPosition } = useRootSetting();
const { title } = useGlobSetting();
const {
getHeaderTheme,
getShowFullScreen,
getShowNotice,
getShowContent,
getShowBread,
getShowHeaderLogo,
getShowHeader,
getShowSearch,
getUseLockPage,
getShowBreadTitle,
} = useHeaderSetting();
const {
getHeaderTheme,
getShowFullScreen,
getShowNotice,
getShowContent,
getShowBread,
getShowHeaderLogo,
getShowHeader,
getShowSearch,
getUseLockPage,
getShowBreadTitle,
} = useHeaderSetting();
const { getShowLocalePicker } = useLocale();
const { getShowLocalePicker } = useLocale();
const { getIsMobile } = useAppInject();
const { getIsMobile } = useAppInject();
const getHeaderClass = computed(() => {
const theme = unref(getHeaderTheme);
return [
const getHeaderClass = computed(() => {
const theme = unref(getHeaderTheme);
return [
prefixCls,
{
[`${prefixCls}--fixed`]: props.fixed,
[`${prefixCls}--mobile`]: unref(getIsMobile),
[`${prefixCls}--${theme}`]: theme,
},
];
});
const getShowSetting = computed(() => {
if (!unref(getShowSettingButton)) {
return false;
}
const settingButtonPosition = unref(getSettingButtonPosition);
if (settingButtonPosition === SettingButtonPositionEnum.AUTO) {
return unref(getShowHeader);
}
return settingButtonPosition === SettingButtonPositionEnum.HEADER;
});
const getLogoWidth = computed(() => {
if (!unref(getIsMixMode) || unref(getIsMobile)) {
return {};
}
const width = unref(getMenuWidth) < 180 ? 180 : unref(getMenuWidth);
return { width: `${width}px` };
});
const getSplitType = computed(() => {
return unref(getSplit) ? MenuSplitTyeEnum.TOP : MenuSplitTyeEnum.NONE;
});
const getMenuMode = computed(() => {
return unref(getSplit) ? MenuModeEnum.HORIZONTAL : null;
});
/**
* 首页多租户部门弹窗逻辑
*/
const loginSelectRef = ref();
function showLoginSelect() {
//update-begin---author:liusq Date:20220101 for----
//
const loginInfo = toRaw(userStore.getLoginInfo) || {};
if (!!loginInfo.isLogin) {
loginSelectRef.value.show(loginInfo);
}
//update-end---author:liusq Date:20220101 for----
}
function loginSelectOk() {
console.log('成功。。。。。');
}
onMounted(() => {
showLoginSelect();
});
return {
prefixCls,
{
[`${prefixCls}--fixed`]: props.fixed,
[`${prefixCls}--mobile`]: unref(getIsMobile),
[`${prefixCls}--${theme}`]: theme,
},
];
});
const getShowSetting = computed(() => {
if (!unref(getShowSettingButton)) {
return false;
}
const settingButtonPosition = unref(getSettingButtonPosition);
if (settingButtonPosition === SettingButtonPositionEnum.AUTO) {
return unref(getShowHeader);
}
return settingButtonPosition === SettingButtonPositionEnum.HEADER;
});
const getLogoWidth = computed(() => {
if (!unref(getIsMixMode) || unref(getIsMobile)) {
return {};
}
const width = unref(getMenuWidth) < 180 ? 180 : unref(getMenuWidth);
return { width: `${width}px` };
});
const getSplitType = computed(() => {
return unref(getSplit) ? MenuSplitTyeEnum.TOP : MenuSplitTyeEnum.NONE;
});
const getMenuMode = computed(() => {
return unref(getSplit) ? MenuModeEnum.HORIZONTAL : null;
});
/**
* 首页多租户部门弹窗逻辑
*/
const loginSelectRef = ref();
function showLoginSelect() {
//update-begin---author:liusq Date:20220101 for----
//
const loginInfo = toRaw(userStore.getLoginInfo) || {};
if (!!loginInfo.isLogin) {
loginSelectRef.value.show(loginInfo);
}
//update-end---author:liusq Date:20220101 for----
}
function loginSelectOk() {
console.log('成功。。。。。');
}
onMounted(() => {
showLoginSelect();
});
return {
prefixCls,
getHeaderClass,
getShowHeaderLogo,
getHeaderTheme,
getShowHeaderTrigger,
getIsMobile,
getShowBreadTitle,
getShowBread,
getShowContent,
getSplitType,
getSplit,
getMenuMode,
getShowTopMenu,
getShowLocalePicker,
getShowFullScreen,
getShowNotice,
getUseErrorHandle,
getLogoWidth,
getIsMixSidebar,
getShowSettingButton,
getShowSetting,
getShowSearch,
getUseLockPage,
loginSelectOk,
loginSelectRef,
title,
t
};
},
});
getHeaderClass,
getShowHeaderLogo,
getHeaderTheme,
getShowHeaderTrigger,
getIsMobile,
getShowBreadTitle,
getShowBread,
getShowContent,
getSplitType,
getSplit,
getMenuMode,
getShowTopMenu,
getShowLocalePicker,
getShowFullScreen,
getShowNotice,
getUseErrorHandle,
getLogoWidth,
getIsMixSidebar,
getShowSettingButton,
getShowSetting,
getShowSearch,
getUseLockPage,
loginSelectOk,
loginSelectRef,
title,
t
};
},
});
</script>
<style lang="less">
@import './index.less';
//update-begin---author:scott ---date:2022-09-30 for-----------
//
@prefix-cls: ~'@{namespace}-layout-header';
.ant-layout .@{prefix-cls} {
display: flex;
padding: 0 8px;
height: 48px;
align-items: center;
.headerIntroductionClass {
margin-right: 4px;
margin-bottom: 2px;
border-bottom: 0px;
border-left: 0px;
}
&--light {
@import './index.less';
//update-begin---author:scott ---date:2022-09-30 for-----------
//
@prefix-cls: ~'@{namespace}-layout-header';
.ant-layout .@{prefix-cls} {
display: flex;
padding: 0 8px;
height: 48px;
align-items: center;
.headerIntroductionClass {
color: @breadcrumb-item-normal-color;
margin-right: 4px;
margin-bottom: 2px;
border-bottom: 0px;
border-left: 0px;
}
&--light {
.headerIntroductionClass {
color: @breadcrumb-item-normal-color;
}
}
&--dark {
.headerIntroductionClass {
color: rgba(255, 255, 255, 0.6);
}
.anticon, .truncate {
color: rgba(255, 255, 255, 0.8);
}
}
//update-end---author:scott ---date::2022-09-30 for--------------
}
&--dark {
.headerIntroductionClass {
color: rgba(255, 255, 255, 0.6);
}
.anticon,
.truncate {
color: rgba(255, 255, 255, 0.8);
}
}
//update-end---author:scott ---date::2022-09-30 for--------------
}
</style>

View File

@ -49,7 +49,8 @@
},
]"
>
<span class="text" style="margin-left: 15px;"> {{ title }}</span>
<span class="text"> {{ title }}</span>
<Icon :size="16" :icon="getMixSideFixed ? 'ri:pushpin-2-fill' : 'ri:pushpin-2-line'" class="pushpin" @click="handleFixedMenu" />
</div>
<ScrollContainer :class="`${prefixCls}-menu-list__content`">
<SimpleMenu :items="childrenMenus" :theme="getMenuTheme" mixSider @menuClick="handleMenuClick" />

View File

@ -17,11 +17,14 @@ export default {
tooltipExitFull: 'Exit Full Screen',
// lock
lockScreenPassword: 'Lock screen password',
lockScreenPassword: 'Password',
lockScreen: 'Lock screen',
lockScreenBtn: 'Locking',
home: 'Home',
welcomeIn: 'Welcome in',
refreshCacheComplete: 'Refresh cache complete',
refreshCacheFailure: 'Refresh cache failure',
},
multipleTab: {
reload: 'Refresh current',
@ -119,4 +122,11 @@ export default {
mixSidebarFixed: 'Fixed expanded menu',
},
changePassword: {
changePassword: 'Change password',
oldPassword: 'Old password',
newPassword: 'New password',
confirmNewPassword: 'Confirm new password',
pleaseEnterNewPassword: 'Please enter new password',
},
};

View File

@ -23,6 +23,9 @@ export default {
lockScreenBtn: '锁定',
home: '首页',
welcomeIn:"欢迎进入",
refreshCacheComplete: "刷新缓存完成!",
refreshCacheFailure: "刷新缓存失败!",
},
multipleTab: {
reload: '重新加载',
@ -120,4 +123,11 @@ export default {
mixSidebarFixed: '固定展开菜单',
},
changePassword: {
changePassword: '修改密码',
oldPassword: '旧密码',
newPassword: '新密码',
confirmNewPassword: '确认新密码',
pleaseEnterNewPassword: '请输入新密码',
},
};

View File

@ -1,10 +1,19 @@
import { getThemeColors, generateColors } from '../../../build/config/themeConfig';
import { replaceStyleVariables } from '@rys-fe/vite-plugin-theme/es/client';
import {
replaceStyleVariables,
loadDarkThemeCss,
replaceCssColors,
darkCssIsReady,
linkID,
styleTagId,
appendCssToDom,
getStyleDom,
} from '@rys-fe/vite-plugin-theme/es/client';
import { mixLighten, mixDarken, tinycolor } from '@rys-fe/vite-plugin-theme/es/colorUtils';
import { useAppStore } from '/@/store/modules/app';
import { defHttp } from '/@/utils/http/axios';
let cssText = '';
export async function changeTheme(color: string) {
// update-begin--author:liaozhiyang---date:20231218---for【QQYUN-6366】升级到antd4.x
const appStore = useAppStore();
@ -16,17 +25,32 @@ export async function changeTheme(color: string) {
tinycolor,
color,
});
let res = await replaceStyleVariables({
colorVariables: [...getThemeColors(color), ...colors],
});
fixDark();
return res;
// update-begin--author:liaozhiyang---date:20240322---for【QQYUN-8570】生产环境暗黑模式下主题色不生效
if (import.meta.env.PROD && appStore.getDarkMode === 'dark') {
if (!darkCssIsReady && !cssText) {
await loadDarkThemeCss();
}
const el: HTMLLinkElement = document.getElementById(linkID) as HTMLLinkElement;
if (el?.href) {
// cssText = await fetchCss(el.href) as string;
!cssText && (cssText = await defHttp.get({ url: el.href }, { isTransformResponse: false }));
const colorVariables = [...getThemeColors(color), ...colors];
const processCss = await replaceCssColors(cssText, colorVariables);
appendCssToDom(getStyleDom(styleTagId) as HTMLStyleElement, processCss);
}
} else {
await replaceStyleVariables({
colorVariables: [...getThemeColors(color), ...colors],
});
fixDark();
}
// update-end--author:liaozhiyang---date:20240322---for【QQYUN-8570】生产环境暗黑模式下主题色不生效
}
// 【LOWCOD-2262】修复黑暗模式下切换皮肤无效的问题
async function fixDark() {
let el = document.getElementById('__VITE_PLUGIN_THEME__');
// update-begin--author:liaozhiyang---date:20240322---for【QQYUN-8570】生产环境暗黑模式下主题色不生效
const el = document.getElementById(styleTagId);
// update-end--author:liaozhiyang---date:20240322---for【QQYUN-8570】生产环境暗黑模式下主题色不生效
if (el) {
el.innerHTML = el.innerHTML.replace(/\\["']dark\\["']/g, `'dark'`);
}

View File

@ -141,6 +141,7 @@ export function createPermissionGuard(router: Router) {
// update-begin-author:sunjianlei date:20230306 for: 修复登录成功后,没有正确重定向的问题
redirect: to.fullPath,
// update-end-author:sunjianlei date:20230306 for: 修复登录成功后,没有正确重定向的问题
};
}
next(redirectData);
@ -161,40 +162,44 @@ export function createPermissionGuard(router: Router) {
return;
}
// get userinfo while last fetch time is empty
//update-begin---author:scott ---date:2024-02-21 for【QQYUN-8326】刷新首页不需要重新获取用户信息---
// // get userinfo while last fetch time is empty
// if (userStore.getLastUpdateTime === 0) {
// try {
// console.log("--LastUpdateTime---getUserInfoAction-----")
// await userStore.getUserInfoAction();
// } catch (err) {
// console.info(err);
// next();
// }
// }
//update-end---author:scott ---date::2024-02-21 for【QQYUN-8326】刷新首页不需要重新获获取用户信息---
// update-begin--author:liaozhiyang---date:20240321---for【QQYUN-8572】表格行选择卡顿问题customRender中字典引起的
if (userStore.getLastUpdateTime === 0) {
try {
await userStore.getUserInfoAction();
} catch (err) {
console.info(err);
next();
}
userStore.setAllDictItemsByLocal();
}
// update-end--author:liaozhiyang---date:20240321---for【QQYUN-8572】表格行选择卡顿问题customRender中字典引起的
if (permissionStore.getIsDynamicAddedRoute) {
next();
return;
}
// 构建后台菜单路由
const routes = await permissionStore.buildRoutesAction();
routes.forEach((route) => {
router.addRoute(route as unknown as RouteRecordRaw);
});
router.addRoute(PAGE_NOT_FOUND_ROUTE as unknown as RouteRecordRaw);
permissionStore.setDynamicAddedRoute(true);
if (to.name === PAGE_NOT_FOUND_ROUTE.name) {
// 动态添加路由后此处应当重定向到fullPath否则会加载404页面内容
//next({ path: to.fullPath, replace: true, query: to.query });
next({ path: PageEnum.BASE_HOME, replace: true, query: to.query });
next({ path: to.fullPath, replace: true, query: to.query });
} else {
const redirectPath = (from.query.redirect || to.path) as string;
const redirect = decodeURIComponent(redirectPath);
//const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect };
const nextData = PageEnum.BASE_HOME;
const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect };
next(nextData);
}
});

View File

@ -15,7 +15,7 @@ export const PAGE_NOT_FOUND_ROUTE: AppRouteRecordRaw = {
children: [
{
path: '/:path(.*)*',
name: 'ChildPageNotFound',
name: PAGE_NOT_FOUND_NAME,
component: EXCEPTION_COMPONENT,
meta: {
title: 'ErrorPage',

View File

@ -87,7 +87,7 @@ const setting: ProjectConfig = {
// 菜单配置
menuSetting: {
// 背景色
bgColor: SIDE_BAR_BG_COLOR_LIST[3],
bgColor: SIDE_BAR_BG_COLOR_LIST[0],
// 是否固定住左侧菜单
fixed: true,
// 菜单折叠
@ -106,15 +106,15 @@ const setting: ProjectConfig = {
// 菜单模式
mode: MenuModeEnum.INLINE,
// 菜单类型
type: MenuTypeEnum.MIX,
type: MenuTypeEnum.SIDEBAR,
// 菜单主题
theme: ThemeEnum.LIGHT,
theme: ThemeEnum.DARK,
// 分割菜单
split: false,
// 顶部菜单布局
topMenuAlign: 'center',
// 折叠触发器的位置
trigger: TriggerEnum.NONE,
trigger: TriggerEnum.HEADER,
// 手风琴模式,只展示一个菜单
accordion: true,
// 在路由切换的时候关闭左侧混合菜单展开菜单
@ -130,7 +130,7 @@ const setting: ProjectConfig = {
// 刷新后是否保留已经打开的标签页
cache: false,
// 开启
show: false,
show: true,
// 是否可以拖拽
canDrag: true,
// 开启快速操作

View File

@ -2,12 +2,13 @@ import type { App } from 'vue';
import { registerJVxeTable } from '/@/components/jeecg/JVxeTable';
import { registerJVxeCustom } from '/@/components/JVxeCustom';
// 注册全局聊天表情包
import { Picker } from 'emoji-mart-vue-fast/src';
// // 注册全局聊天表情包
// import { Picker } from 'emoji-mart-vue-fast/src';
// 注册全局dayjs
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
export async function registerThirdComp(app: App) {
//---------------------------------------------------------------------
@ -17,8 +18,22 @@ export async function registerThirdComp(app: App) {
await registerJVxeCustom();
//---------------------------------------------------------------------
// 注册全局聊天表情包
app.component('Picker', Picker);
//app.component('Picker', Picker);
app.component(
'Picker',
createAsyncComponent(() => {
return new Promise((resolve, rejected) => {
import('emoji-mart-vue-fast/src')
.then((res) => {
const { Picker } = res;
resolve(Picker);
})
.catch((err) => {
rejected(err);
});
});
})
);
//---------------------------------------------------------------------
// 注册全局dayjs
dayjs.locale('zh-cn');

View File

@ -0,0 +1,75 @@
import {store} from '/@/store';
import {defineStore} from 'pinia';
import {defHttp} from "@/utils/http/axios";
interface DefIndexState {
// 首页url
url: string,
// 首页组件
component: string
}
export const useDefIndexStore = defineStore({
id: 'defIndex',
state: (): DefIndexState => ({
url: '',
component: '',
}),
getters: {},
actions: {
/**
*
*/
async query() {
const config = await defIndexApi.query();
this.url = config.url;
this.component = config.component;
},
/**
*
* @param url url
* @param component
* @param isRoute
*/
async update(url: string, component: string, isRoute: boolean) {
await defIndexApi.update(url, component, isRoute);
await this.query()
},
check(url: string) {
return url === this.url;
}
}
});
// Need to be used outside the setup
export function useDefIndexStoreWithOut() {
return useDefIndexStore(store);
}
/**
* API
*/
export const defIndexApi = {
/**
*
*/
async query() {
const url = '/sys/sysRoleIndex/queryDefIndex'
return await defHttp.get({url});
},
/**
*
* @param url url
* @param component
* @param isRoute
*/
async update(url: string, component: string, isRoute: boolean) {
let apiUrl = '/sys/sysRoleIndex/updateDefIndex'
apiUrl += '?url=' + url
apiUrl += '&component=' + component
apiUrl += '&isRoute=' + isRoute
return await defHttp.put({url: apiUrl});
},
}

View File

@ -18,8 +18,7 @@ import { ERROR_LOG_ROUTE, PAGE_NOT_FOUND_ROUTE } from '/@/router/routes/basic';
import { filter } from '/@/utils/helper/treeHelper';
import { getMenuList,switchVue3Menu } from '/@/api/sys/menu';
import { getPermCode } from '/@/api/sys/user';
import { getBackMenuAndPerms } from '/@/api/sys/menu';
import { useMessage } from '/@/hooks/web/useMessage';
import { PageEnum } from '/@/enums/pageEnum';
@ -124,10 +123,14 @@ export const usePermissionStore = defineStore({
this.lastBuildMenuTime = 0;
},
async changePermissionCode() {
const systemPermission = await getPermCode();
const systemPermission = await getBackMenuAndPerms();
const codeList = systemPermission.codeList;
this.setPermCodeList(codeList);
this.setAuthData(systemPermission);
//菜单路由
const routeList = systemPermission.menu;
return routeList;
},
async buildRoutesAction(): Promise<AppRouteRecordRaw[]> {
const { t } = useI18n();
@ -207,6 +210,7 @@ export const usePermissionStore = defineStore({
// 后台菜单构建
case PermissionModeEnum.BACK:
const { createMessage, createWarningModal } = useMessage();
console.log(" --- 构建后台路由菜单 --- ")
// 菜单加载提示
// createMessage.loading({
// content: t('sys.app.menuLoading'),
@ -217,42 +221,42 @@ export const usePermissionStore = defineStore({
// 这个函数可能只需要执行一次,并且实际的项目可以在正确的时间被放置
let routeList: AppRouteRecordRaw[] = [];
try {
this.changePermissionCode();
routeList = (await getMenuList()) as AppRouteRecordRaw[];
routeList = await this.changePermissionCode();
//routeList = (await getMenuList()) as AppRouteRecordRaw[];
// update-begin----author:sunjianlei---date:20220315------for: 判断是否是 vue3 版本的菜单 ---
let hasIndex: boolean = false;
let hasIcon: boolean = false;
for (let menuItem of routeList) {
// 条件1判断组件是否是 layouts/default/index
if (!hasIndex) {
hasIndex = menuItem.component === 'layouts/default/index';
}
// 条件2判断图标是否带有 冒号
if (!hasIcon) {
hasIcon = !!menuItem.meta?.icon?.includes(':');
}
// 满足任何一个条件都直接跳出循环
if (hasIcon || hasIndex) {
break;
}
}
// 两个条件都不满足,就弹出提示框
if (!hasIcon && !hasIndex) {
// 延迟1.5秒之后再出现提示,否则提示框出不来
setTimeout(
() =>
createWarningModal({
title: '检测提示',
content:
'当前菜单表是 <b>Vue2版本</b>,导致菜单加载异常!<br>点击确认切换到Vue3版菜单',
onOk:function () {
switchVue3Menu();
location.reload();
}
}),
100
);
}
// let hasIndex: boolean = false;
// let hasIcon: boolean = false;
// for (let menuItem of routeList) {
// // 条件1判断组件是否是 layouts/default/index
// if (!hasIndex) {
// hasIndex = menuItem.component === 'layouts/default/index';
// }
// // 条件2判断图标是否带有 冒号
// if (!hasIcon) {
// hasIcon = !!menuItem.meta?.icon?.includes(':');
// }
// // 满足任何一个条件都直接跳出循环
// if (hasIcon || hasIndex) {
// break;
// }
// }
// // 两个条件都不满足,就弹出提示框
// if (!hasIcon && !hasIndex) {
// // 延迟1.5秒之后再出现提示,否则提示框出不来
// setTimeout(
// () =>
// createWarningModal({
// title: '检测提示',
// content:
// '当前菜单表是 <b>Vue2版本</b>,导致菜单加载异常!<br>点击确认切换到Vue3版菜单',
// onOk:function () {
// switchVue3Menu();
// location.reload();
// }
// }),
// 100
// );
// }
// update-end----author:sunjianlei---date:20220315------for: 判断是否是 vue3 版本的菜单 ---
} catch (error) {
console.error(error);

View File

@ -19,11 +19,14 @@ import { useGlobSetting } from '/@/hooks/setting';
import { JDragConfigEnum } from '/@/enums/jeecgEnum';
import { useSso } from '/@/hooks/web/useSso';
import { isOAuth2AppEnv } from "/@/views/sys/login/useLogin";
interface dictType {
[key: string]: any;
}
interface UserState {
userInfo: Nullable<UserInfo>;
token?: string;
roleList: RoleEnum[];
dictItems?: [];
dictItems?: dictType | null;
sessionTimeout?: boolean;
lastUpdateTime: number;
tenantid?: string | number;
@ -41,7 +44,7 @@ export const useUserStore = defineStore({
// 角色列表
roleList: [],
// 字典
dictItems: [],
dictItems: null,
// session过期时间
sessionTimeout: false,
// Last fetch time
@ -56,6 +59,9 @@ export const useUserStore = defineStore({
}),
getters: {
getUserInfo(): UserInfo {
if(this.userInfo == null){
this.userInfo = getAuthCache<UserInfo>(USER_INFO_KEY)!=null ? getAuthCache<UserInfo>(USER_INFO_KEY) : null;
}
return this.userInfo || getAuthCache<UserInfo>(USER_INFO_KEY) || {};
},
getLoginInfo(): LoginInfo {
@ -106,6 +112,16 @@ export const useUserStore = defineStore({
this.dictItems = dictItems;
setAuthCache(DB_DICT_DATA_KEY, dictItems);
},
setAllDictItemsByLocal() {
// update-begin--author:liaozhiyang---date:20240321---for【QQYUN-8572】表格行选择卡顿问题customRender中字典引起的
if (!this.dictItems) {
const allDictItems = getAuthCache(DB_DICT_DATA_KEY);
if (allDictItems) {
this.dictItems = allDictItems;
}
}
// update-end--author:liaozhiyang---date:20240321---for【QQYUN-8572】表格行选择卡顿问题customRender中字典引起的
},
setTenant(id) {
this.tenantid = id;
setAuthCache(TENANT_ID, id);
@ -118,7 +134,7 @@ export const useUserStore = defineStore({
},
resetState() {
this.userInfo = null;
this.dictItems = [];
this.dictItems = null;
this.token = '';
this.roleList = [];
this.sessionTimeout = false;
@ -168,15 +184,19 @@ export const useUserStore = defineStore({
if (sessionTimeout) {
this.setSessionTimeout(false);
} else {
const permissionStore = usePermissionStore();
if (!permissionStore.isDynamicAddedRoute) {
const routes = await permissionStore.buildRoutesAction();
routes.forEach((route) => {
router.addRoute(route as unknown as RouteRecordRaw);
});
router.addRoute(PAGE_NOT_FOUND_ROUTE as unknown as RouteRecordRaw);
permissionStore.setDynamicAddedRoute(true);
}
//update-begin---author:scott ---date::2024-02-21 for【QQYUN-8326】登录不需要构建路由进入首页有构建---
// // 构建后台菜单路由
// const permissionStore = usePermissionStore();
// if (!permissionStore.isDynamicAddedRoute) {
// const routes = await permissionStore.buildRoutesAction();
// routes.forEach((route) => {
// router.addRoute(route as unknown as RouteRecordRaw);
// });
// router.addRoute(PAGE_NOT_FOUND_ROUTE as unknown as RouteRecordRaw);
// permissionStore.setDynamicAddedRoute(true);
// }
//update-end---author:scott ---date::2024-02-21 for【QQYUN-8326】登录不需要构建路由进入首页有构建---
await this.setLoginInfo({ ...data, isLogin: true });
//update-begin-author:liusq date:2022-5-5 for:登录成功后缓存拖拽模块的接口前缀
localStorage.setItem(JDragConfigEnum.DRAG_BASE_URL, useGlobSetting().domainUrl);
@ -279,6 +299,7 @@ export const useUserStore = defineStore({
this.setUserInfo(null);
this.setLoginInfo(null);
this.setTenant(null);
this.setAllDictItems(null);
//update-begin-author:liusq date:2022-5-5 for:退出登录后清除拖拽模块的接口前缀
localStorage.removeItem(JDragConfigEnum.DRAG_BASE_URL);
//update-end-author:liusq date:2022-5-5 for: 退出登录后清除拖拽模块的接口前缀

View File

@ -52,7 +52,8 @@ const render = {
if (obj.length > 0) {
text = obj[0].text;
}
return isEmpty(text) || !renderTag ? h('span', text) : h(Tag, text);
//【jeecgboot-vue3/issues/903】render.renderDict使用tag渲染报警告问题 #903
return isEmpty(text) || !renderTag ? h('span', text) : h(Tag, () => text);
},
/**
*

View File

@ -1,70 +0,0 @@
function myBrowser() {
var userAgent = navigator.userAgent; // 取得浏览器的userAgent字符串
var isOpera = userAgent.indexOf('Opera') > -1;
if (isOpera) {
return 'Opera';
} // 判断是否Opera浏览器
if (userAgent.indexOf('Firefox') > -1) {
return 'FF';
} // 判断是否Firefox浏览器
if (userAgent.indexOf('Chrome') > -1) {
return 'Chrome';
}
if (userAgent.indexOf('Safari') > -1) {
return 'Safari';
} // 判断是否Safari浏览器
if (userAgent.indexOf('compatible') > -1 && userAgent.indexOf('MSIE') > -1 && !isOpera) {
return 'IE';
} // 判断是否IE浏览器
if (userAgent.indexOf('Trident') > -1) {
return 'Edge';
} // 判断是否Edge浏览器
}
function SaveAs5(imgURL) {
var oPop = window.open(imgURL, '', 'width=1, height=1, top=5000, left=5000');
for (; oPop.document.readyState !== 'complete'; ) {
if (oPop.document.readyState === 'complete') break;
}
oPop.document.execCommand('SaveAs');
oPop.close();
}
function download(src, fileName) {
// 创建隐藏的可下载链接
var eleLink = document.createElement('a');
if (fileName) {
eleLink.setAttribute('download', fileName);
} else {
eleLink.download = src;
}
eleLink.style.display = 'none';
// // 字符内容转变成blob地址
eleLink.href = src;
// // 触发点击
document.body.appendChild(eleLink);
eleLink.click();
// // 然后移除
document.body.removeChild(eleLink);
}
function downLoadFile(url, fileName = '') {
if (myBrowser() === 'IE' || myBrowser() === 'Edge') {
SaveAs5(url);
} else {
download(url, fileName);
}
}
function conversionFileDownload(res) {
const a = document.createElement('a');
const blob = new Blob([res.data], { type: 'application/vnd.ms-excel' });
const temp = res.headers['content-disposition'].split(';')[1].split('filename=')[1];
// 对文件名乱码转义--【Node.js】使用iconv-lite解决中文乱码
const iconv = require('iconv-lite');
iconv.skipDecodeWarning = true; // 忽略警告
const fileName = iconv.decode(temp, 'utf-8');
console.log(fileName);
a.setAttribute('download', fileName);
const objectUrl = URL.createObjectURL(blob);
a.setAttribute('href', objectUrl);
a.click();
}
export { downLoadFile, conversionFileDownload };

View File

@ -91,13 +91,22 @@ const transform: AxiosTransform = {
beforeRequestHook: (config, options) => {
const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true, urlPrefix } = options;
if (joinPrefix) {
//update-begin---author:scott ---date:2024-02-20 for以http开头的请求url不拼加前缀--
// http开头的请求url不加前缀
let isStartWithHttp = false;
const requestUrl = config.url;
if(requestUrl!=null && (requestUrl.startsWith("http:") || requestUrl.startsWith("https:"))){
isStartWithHttp = true;
}
if (!isStartWithHttp && joinPrefix) {
config.url = `${urlPrefix}${config.url}`;
}
if (apiUrl && isString(apiUrl)) {
if (!isStartWithHttp && apiUrl && isString(apiUrl)) {
config.url = `${apiUrl}${config.url}`;
}
//update-end---author:scott ---date::2024-02-20 for以http开头的请求url不拼加前缀--
const params = config.params || {};
const data = config.data || false;
formatDate && data && !isString(data) && formatRequestDate(data);
@ -246,7 +255,7 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) {
// authenticationScheme: 'Bearer',
authenticationScheme: '',
//接口超时设置
timeout: 15 * 1000,
timeout: 10 * 1000,
// 基础接口地址
// baseURL: globSetting.apiUrl,
headers: { 'Content-Type': ContentTypeEnum.JSON },

View File

@ -104,7 +104,7 @@ export function cloneObject(obj) {
}
export const withInstall = <T>(component: T, alias?: string) => {
console.log("---初始化---", component)
//console.log("---初始化---", component)
const comp = component as any;
comp.install = (app: App) => {

View File

@ -1,24 +0,0 @@
<template>
<div>
<div>
<h1>二期开发</h1>
</div>
</div>
</template>
<script>
export default {
}
</script>
<style lang="less" scoped>
h1{
color: red;
font-size: 50px;
text-align: center;
margin-top: 100px;
}
</style>

View File

@ -0,0 +1,13 @@
<template>
<div>
<h1>学生信息</h1>
</div>
</template>
<script>
export default {
}
</script>
<style></style>

View File

@ -1,511 +0,0 @@
<template>
<div style="background: #ececec; padding: 15px">
<a-card title="四级成绩分析" :loading="loading" :bordered="false">
<template #extra>
<a-cascader v-model:value="collegeMajor" :options="collegeMajorOptions" change-on-select />
<a-button style="margin-left: 10px" type="primary" @click="query">查询</a-button>
<a-button style="margin-left: 10px" :loading="aiLoading" v-show="resultData != null" type="primary"
@click="getAI">分析</a-button>
</template>
<a-row :gutter="12">
<a-col :xl="12">
<div style="width: 100%; height: 215px" class="resChart" id="resChart"> </div>
</a-col>
<a-col :xl="12">
<div style="width: 100%; height: 215px" class="piechart" id="pieChart"> </div>
</a-col>
</a-row>
<a-card style="margin-bottom: 10px" class="tip">
<div style="display: flex">
<div v-html="aiMessage"></div>
</div>
</a-card>
</a-card>
</div>
</template>
<script>
import { defHttp } from '/@/utils/http/axios';
import * as echarts from 'echarts';
import axios from 'axios';
import { marked } from 'marked';
export default {
data() {
return {
loading: false,
dataSourceCet4: [],
columns: [
{
title: '学年',
dataIndex: 'grade',
key: 'grade',
align: 'center',
customCell: (_, index) => ({
rowSpan: index % 2 === 0 ? 2 : 0, //graderowSpan
}),
},
{
title: '考试批次',
dataIndex: 'batch',
key: 'batch',
align: 'center',
},
{
title: '参加人数',
dataIndex: 'attendNumber',
key: 'attendNumber',
align: 'center',
},
{
title: '通过人数',
dataIndex: 'passNumber',
key: 'passNumber',
align: 'center',
},
{
title: '本批次通过率',
dataIndex: 'batchpassrate',
key: 'batchpassrate',
align: 'center',
},
{
slots: { title: 'passRateSlot' },
dataIndex: 'gradepassrate',
key: 'gradepassrate',
align: 'center',
customCell: (_, index) => ({
rowSpan: index % 2 === 0 ? 2 : 0,
}),
},
],
Url: {
getEntrydate: '/cet/getEntrydate',
getCollege: '/cet/getCollege',
getData: '/cet/getData',
getCollegeMajor: '/cet/getCollegeMajor',
getAnalyze: '/cet_4/analyze',
},
total: 0,
totalName: '',
passNumberBottom: 0,
passRateBottom: 0,
passRatePie: [],
lineXData: [],
resultData: [],
aiMessage: '',
lineYData: [],
collegeOptions: [],
collegeMajorOptions: [],
entrydateOptions: [],
college: null,
aiLoading: false,
entrydate: 2017, //
//
collegeMajor: ['全校'], //
topCollege: null, //
topMajor: null, //
topEntrydate: null, //
};
},
mounted() {
this.getEntrydateAndCollegeData();
this.getCollegeMajorData();
this.query();
//this.map2Chart();
},
methods: {
//
async getEntrydateAndCollegeData() {
const getEntrydate = await defHttp.get({ url: this.Url.getEntrydate });
const getCollege = await defHttp.get({ url: this.Url.getCollege });
this.collegeOptions = getCollege.colleges;
//
this.collegeOptions.unshift({ value: '全校', label: '全校' });
this.college = this.collegeOptions[0].value;
this.entrydateOptions = getEntrydate.entrydates;
// this.entrydate = this.entrydateOptions[0].value;
},
//
async getCollegeMajorData() {
const res = await defHttp.get({ url: this.Url.getCollegeMajor });
//map
this.collegeMajorOptions = res.collegeMajor.map((item) => {
return {
value: item.college,
label: item.college,
children: item.major.map((major) => {
return {
value: major,
label: major,
};
}),
};
});
//
this.collegeMajorOptions.unshift({ value: '专升本', label: '专升本' });
//
this.collegeMajorOptions.unshift({ value: '全校', label: '全校' });
// this.collegeMajor = [''];
console.log(this.collegeMajorOptions);
},
//
async query() {
try {
//this.collegeMajor[1]null
let major = this.collegeMajor.length > 1 ? this.collegeMajor[1] : '';
this.topCollege = this.collegeMajor[0];
this.topMajor = major;
this.topEntrydate = this.entrydate;
if (this.college) this.loading = true;
let params = {
college: this.collegeMajor[0],
major: major,
};
console.log('params', params);
const result = await defHttp.post({ url: this.Url.getAnalyze, params });
this.resultData = result.data;
//
console.log(result, 'res');
const resdata = [];
//
for (const [range, rate] of Object.entries(result.data[0].rateByBatch)) {
//
resdata.push({
value: parseFloat(rate), //
name: `${range}分人数占比`,
});
}
//
resdata.sort((a, b) => {
const aMin = parseInt(a.name.split('-')[0]); //
const bMin = parseInt(b.name.split('-')[0]); //
return aMin - bMin; //
});
console.log(resdata, 'resdata');
//
resdata[0].name = '400分以下人数占比';
//
resdata[4].name = '475分以上人数占比';
const piedata = [];
piedata.push({ value: result.data[0].scoreByBatch.read, name: '阅读' });
piedata.push({ value: result.data[0].scoreByBatch.listen, name: '听力' });
piedata.push({ value: result.data[0].scoreByBatch.write, name: '写作' });
setTimeout(() => {
this.drawResChart(resdata);
this.drawPieChart(piedata);
this.map2Chart();
}, 100);
// this.drawPieChart();
} finally {
this.loading = false;
}
},
test() {
defHttp.get({ url: '/cet/getTest' });
},
map2Chart() {
var myChart = echarts.init(document.getElementById('map2'));
var option = {
title: {
text: '各批次通过率分析',
left: 'left',
top: '0%',
textStyle: {
fontSize: 16,
},
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
formatter: function (params) {
return (
params[0].name +
'<br/>' +
'<table>' +
'<tr><td>' +
params[0].marker +
'</td><td style="font-weight: bold;">' +
'&nbsp;&nbsp;&nbsp;&nbsp;' +
params[0].value +
'%' +
'</td></tr>' +
'<tr>' +
'</table>'
);
},
},
grid: {
top: '50px',
left: '50px',
right: '50px',
bottom: '30px',
},
xAxis: {
type: 'category',
data: this.lineXData,
},
yAxis: {
type: 'value',
},
series: [
{
data: this.lineYData,
type: 'line',
itemStyle: {
normal: {
label: {
show: true, //
position: 'top', //
textStyle: {
//
color: 'black',
fontSize: 12,
},
formatter: '{c}%',
},
},
},
},
],
};
option && myChart.setOption(option);
},
async getAI() {
this.aiLoading = true;
this.aiMessage = ''; //
// AI
const SYSTEM_PROMPT = `
#Role:
我是某大学英语教研组的成员致力于通过科学分析和有效建议帮助学生全面提升四级成绩
#Background:
作为英语教学与考试研究的专业团队我们关注学生英语能力的各项提升尤其重视数据驱动的教学改进我们分析学生的四级考试成绩提供针对性的建议和指导
数据来源本校最近三个批次的四级考试数据batch1-3,三个批次之间不存在直接关系
分析维度各分数段人数占比rateByBatch模块平均分scoreByBatch阅读/写作/听力
#Goal:
通过对四级成绩数据的深入分析识别学生在各个模块阅读写作听力中的优势和不足提出切实可行的提升建议帮助学生在未来的考试中取得更好的成绩
#Tone:
以专业客观和建设性的态度进行分析确保建议具有实用性和可操作性
#Format:
总体分析
模块诊断阅读/写作/听力分别说明
教学建议
#Constraints:
1. 分析应基于提供的四级成绩数据确保准确性和相关性
2. 不需要重复说明数据内容直接进行分析
下面是我们学校的四级成绩数据batch为批次rateByBatch为每个分数段的人数占比scoreByBatch为四级每个模块的平均分分别为阅读写作听力
` + JSON.stringify(this.resultData) + `
`;
let validMessages = [
{
role: 'user',
content: SYSTEM_PROMPT
}
];
try {
const response = await fetch('https://api.deepseek.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer sk-1d3dd761c40045228547efb38ce59754',
},
body: JSON.stringify({
model: 'deepseek-chat',
messages: validMessages,
stream: true, //
}),
});
if (!response.ok) {
throw new Error(`API请求失败: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullMessage = '(数据分析由deepseek生成仅供参考数据来源为学校近三个批次的四级考试数据)\n\n';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') continue;
try {
const parsed = JSON.parse(data);
if (parsed.choices[0].delta?.content) {
fullMessage += parsed.choices[0].delta.content;
//
this.aiMessage = marked(fullMessage);
}
} catch (e) {
console.error('解析响应数据时出错:', e);
}
}
}
}
} catch (error) {
console.error('获取AI分析失败:', error);
this.aiMessage = marked('抱歉,分析过程中发生了错误,请稍后重试。');
} finally {
this.aiLoading = false;
}
},
drawResChart(piedata) {
console.log(this.passRatePie);
console.log('piedata', piedata);
let xData = piedata.map((item) => item.name);
let Data = piedata.map((item) => item.value);
console.log('xData', xData);
console.log('Data', Data);
let myChart = echarts.init(document.getElementById('resChart'));
let option = {
grid: {
left: '10%', //
right: '10%', //
bottom: '30%', // X
},
tooltip: {
trigger: 'axis',
},
title: {
text: '四级分数占比',
left: 'left',
top: '0%',
textStyle: {
fontSize: 16,
},
},
xAxis: {
type: 'category',
data: xData,
axisLabel: {
interval: 0, //
//rotate: 10, // 45
margin: 15, // 线
formatter: function (value) {
// ''
return value.replace(/分/g, '分\n');
},
},
axisLine: {
show: true,
},
boundaryGap: true, //
},
yAxis: {
type: 'value',
},
series: [
{
data: Data,
type: 'bar',
label: {
show: true, //
position: 'top', //
formatter: '{c}%', // 使 {c}
fontSize: 12, //
color: '#000', //
},
},
],
};
myChart.setOption(option);
},
drawPieChart(piedata) {
console.log(this.passRatePie);
console.log('piedata', piedata);
let xData = piedata.map((item) => item.name);
let data = piedata.map((item) => item.value);
let myChart = echarts.init(document.getElementById('pieChart'));
let option = {
grid: {
left: '10%', //
right: '10%', //
bottom: '30%', // X
},
tooltip: {
trigger: 'axis',
},
title: {
text: '四级分数分析',
left: 'left',
top: '0%',
textStyle: {
fontSize: 16,
},
},
xAxis: {
type: 'category',
data: xData,
axisLabel: {
interval: 0, //
//rotate: 10, // 45
margin: 15, // 线
},
axisLine: {
show: true,
},
boundaryGap: true, //
},
yAxis: {
type: 'value',
},
series: [
{
data: data,
type: 'bar',
label: {
show: true, //
position: 'top', //
fontSize: 12, //
color: '#000', //
},
barWidth: '25%',
},
],
};
myChart.setOption(option);
},
},
};
</script>
<style lang="less" scoped>
.container {
display: flex;
background-color: #fff;
padding: 15px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
}
.title {
font-size: 34px;
color: rgb(8, 8, 8);
font-weight: bold;
}
</style>

View File

@ -1,866 +0,0 @@
<template>
<div style="background: #ececec; padding: 15px">
<a-card :bordered="false">
<div>
<div class="query">
<span style="font-size: 15px; margin-right: 10px; display: flex; justify-content: center; align-items: center; font-weight: bold"
>年级:
</span>
<a-select v-model:value="oneentrydate" style="width: 200px" :options="entrydateOptions" />
<a-button style="margin-left: 10px" type="primary" @click="allQuery">查询</a-button>
</div>
<a-card :bordered="true">
<a-row :gutter="12">
<a-col :xl="16">
<a-card :loading="map1loading">
<div id="span">
<div style="display: flex; justify-content: center; align-items: center">
<span style="font-size: 16px">点击柱状图可查看该学院&nbsp; </span>
<span style="font-size: 16px; color: red">各年级四级通过率</span>
</div>
<!-- <span style=" font-size: 18px;">&nbsp 变化</span> -->
</div>
<div id="map1" style="width: 100%; height: 500px"></div>
</a-card>
</a-col>
<a-col :xl="8">
<a-card :loading="map2loading" :style="{ marginBottom: '12px' }">
<div id="map2" style="width: 100%; height: 230px"></div>
</a-card>
<a-card :loading="map3loading">
<div id="map3" style="width: 100%; height: 230px"></div>
</a-card>
</a-col>
</a-row>
</a-card>
</div>
</a-card>
</div>
</template>
<script>
import { defHttp } from '/@/utils/http/axios';
import * as echarts from 'echarts';
import { message } from 'ant-design-vue';
export default {
data() {
return {
activeKey: '1',
allCollege: [],
showBox: false,
showGroup: false,
oneentrydate: null,
collegeentrydate: [],
majorentrydate: [],
lastMajorEntrydate: [],
checkedOptions: [],
collegetab2: [],
collegeMajorOptions: [],
collegeMajor: [],
Url: {
getBatch: '/cet/getBatch',
getEntrydate: '/cet/getEntrydate',
getCollege: '/cet/getCollege',
getCollegeRate: '/cet/getRateByCollege',
getAllRate: '/cet/getAllRate',
getCollegeMajor: '/cet/getCollegeMajor',
getMajor: '/cet/getMajorByCollege',
getMajorRate: '/cet/getRateByMajor',
getRateByMajor: '/cet/getRateByMajor',
},
map1loading: false,
map2loading: false,
map3loading: false,
collegeOptions: [],
batchOptions: [],
levelOptions: [
{ value: 'cet4', label: '英语四级' },
{ value: 'cet6', label: '英语六级' },
],
visible: false,
level: null,
college: [],
batch: null,
entrydateOptions: [],
entrydate: [],
majorCheckOn: [],
//
lastCollegeMajor: [],
majorLength: 0,
majorOptions: [],
//
lastCollege: [],
//
lastCollegeEntrydate: [],
};
},
mounted() {
this.getCollegeOptions();
// this.getBatch();
this.getEntrydate();
this.allQuery();
this.getCollegeMajorData();
},
methods: {
allQuery() {
this.map1loading = true;
this.map2loading = true; //
this.map3loading = true; //
let college = ['全校'];
let query = 'tab1';
if (this.oneentrydate == null) {
this.oneentrydate = '2017';
}
this.query(query, college, [this.oneentrydate]);
this.majorPassRate();
this.gradePassRate();
},
async majorQuery() {
if (this.majorentrydate.length == 0) {
message.error('请选择年级');
return;
}
if (this.collegeMajor.length == 0 || this.collegeMajor == null) {
message.error('请选择学院');
return;
}
if (this.lastCollegeMajor.length == 0 || this.lastCollegeMajor == null) {
message.error('请选择专业');
return;
}
this.tab3loading = true;
let res = null;
try {
let params = {
college: this.lastCollegeMajor,
entrydate: this.majorentrydate,
};
res = await defHttp.post({ url: this.Url.getRateByMajor, params });
} finally {
console.log(res, 'res');
this.tab3loading = false;
this.$nextTick(() => {
this.drawChart(res.data, 'tab3');
});
}
},
//
async getCollegeMajorData() {
const res = await defHttp.get({ url: this.Url.getCollegeMajor });
//map
this.collegeMajorOptions = res.collegeMajor.map((item) => {
return {
value: item.college,
label: item.college,
children: item.major.map((major) => {
return {
value: major,
label: major,
};
}),
};
});
// this.collegeMajor = [''];
console.log(this.collegeMajor, 'collegeMajor');
console.log(this.collegeMajorOptions, 'collegeMajorOptions1');
},
getCollegeOptions() {
defHttp.get({ url: this.Url.getCollege }).then((res) => {
this.collegeOptions = res.colleges;
//
// this.collegeOptions.unshift({ value: '', label: '' });
// this.collegeOptions.forEach(option => {
// if (option.value !== '') {
// option.disabled = true;
//}
// });
// this.college = [''];
// this.collegetab2 = ['', ''];
console.log(this.collegeOptions, 'collegeOptions');
});
},
//
async gradePassRate() {
this.map2loading = true;
const getEntrydate = await defHttp.get({ url: this.Url.getEntrydate });
this.entrydateOptions = getEntrydate.entrydates;
let entrydate = this.entrydateOptions.map((item) => item.value);
const name = ['西语学院'];
let queryParams = {
college: name,
entrydate: entrydate,
level: 'cet4',
};
let url = this.Url.getCollegeRate;
let result = await defHttp.post({ url: url, data: queryParams });
if (!result) {
return;
}
result = result.data[name];
let collegeOption = {
title: {
text: name + '四级通过率',
textStyle: {
fontSize: 14,
},
},
xAxis: {
data: result.map((item) => item.entrydate),
},
//
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
formatter: function (params) {
//tooltip,
let result = params[0].name + '<br>';
for (let i = 0; i < params.length; i++) {
result += params[i].marker + name + ' : ' + params[i].value + '%' + '<br>';
}
return result;
},
},
series: {
type: 'line',
data: result.map((item) => item.passRate),
label: {
show: true, //
position: 'top', //
formatter: function (params) {
//
return params.value + '%';
},
//
},
universalTransition: {
// enabled: true,
// divideShape: 'clone',
},
},
yAxis: {},
animationDurationUpdate: 500,
};
this.map2loading = false;
this.$nextTick(() => {
var myChart = echarts.init(document.getElementById('map2'));
myChart.setOption(collegeOption);
});
},
//
async majorPassRate() {
this.map3loading = true;
const name = '西语学院';
let queryParams = {
college: [[name]],
entrydate: [this.oneentrydate],
level: 'cet4',
};
let url = this.Url.getMajorRate;
console.log(queryParams, 'queryParams');
let result = await defHttp.post({ url: url, data: queryParams });
if (!result) {
return;
}
//result = result.data[name];
// result.data
const majors = Object.keys(result.data); //
const passRates = majors.map((major) => {
const entries = result.data[major];
return entries.length > 0 ? parseFloat(entries[0].passRate) : 0; //
});
let xData = majors.map((label) => label.split('').join('\n')); //x
var majorOption = {
tooltip: {
trigger: 'item',
conginee: false,
//
itemStytle: {
fontSize: 100,
},
formatter: function (params) {
// %
return `${params.marker}${params.name} ${params.value}%`;
},
},
title: {
text: name + '各专业四级通过率',
textStyle: {
fontSize: 14,
},
},
xAxis: {
type: 'category',
data: xData,
axisLabel: {
interval: 0, //x
//rotate: -10, //30
},
//data: result.map((item) => item.major),
//data: ['', '', '', '', '西'],
},
yAxis: {
type: 'value',
},
series: [
{
label: {
show: true,
fontSize: 14,
overflow: 'truncate',
position: 'top',
formatter: function (params) {
//
return params.value + '%';
},
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
//
label: {
show: false,
fontSize: 14,
fontWeight: 'bold',
},
},
data: passRates,
//data: result.map((item) => item.passRate),
//data: [200, 150, 80, 70, 110, 130],
type: 'bar',
},
],
animationDurationUpdate: 500,
};
this.map3loading = false;
this.$nextTick(() => {
var myChart = echarts.init(document.getElementById('map3'));
myChart.setOption(majorOption);
});
},
dataChart(data, tab) {
let seriesData = [];
let xData = [];
let colors = [
'#5370c5',
'#ffffff',
'#fac858',
'#ee6666',
'#73c0de',
'#FF6A6A',
'#FFA500',
'#EE2C2C',
'#90EE90',
'#008B8B',
'#FFC0CB',
'#FFDAB9',
'#FFDEAD',
'#FFE4B5',
'#FFE4C4',
'#FFE4E1',
'#FFEBCD',
'#FFEFD5',
'#FFFAF0',
'#FFFAFA',
'#FFFFE0',
'#FFFFF0',
'#FFFFFF',
'#F0F8FF',
'#FAEBD7',
'#FAF0E6',
'#FAFAD2',
'#F5FFFA',
'#F8F8FF',
'#F0FFF0',
'#F0FFFF',
'#F0E68C',
'#F0F8FF',
'#F0FFF0',
'#F0FFFF',
'#F4A460',
'#F5DEB3',
'#F5F5DC',
'#F5F5F5',
'#F5FFFA',
'#F8F8FF',
'#F9EBEA',
'#FAD7A0',
'#FAF0E6',
'#FAFAD2',
'#FAF0E6 ',
];
let j = 0;
for (let i in data) {
xData = [];
let yData = [];
for (let key in data[i]) {
xData.push(data[i][key].college);
// %
yData.push(data[i][key].passRate);
}
xData = xData.map((label) => label.split('').join('\n')); //x
seriesData.push({
name: i + '级累计总通过率',
type: 'bar',
//
barWidth: 1,
data: yData,
//
barGap: '30%',
//
itemStyle: {
normal: {
label: {
show: true, //
position: 'top', //
formatter: '{c}%',
textStyle: {
//
color: 'black',
fontSize: 13,
},
},
color: colors[j++],
},
},
});
if (j == colors.length) {
j = 0;
}
let rankData = yData
.slice()
.sort((a, b) => b - a)
.map((value) => yData.indexOf(value) + 1);
//
seriesData.push({
name: i + '级累计总通过率排名',
type: 'line',
yAxisIndex: 1,
data: rankData,
bar: {},
show: false,
itemStyle: {
normal: {
label: {
show: false, //
position: 'top', //
formatter: '{c}',
textStyle: {
//
color: 'black',
fontSize: 8,
},
},
color: colors[j++],
},
},
});
if (j == colors.length) {
j = 0;
}
}
let myChart = null;
myChart = echarts.init(document.getElementById('map1'));
//
// const college1 = this.college;
let option = {
title: {
text: '本年级学院四级通过率排名',
textStyle: {
fontSize: 16,
},
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
formatter: function (params) {
//tooltip,
let result = params[0].name + '<br>';
for (let i = 0; i < params.length; i++) {
result += params[i].marker + params[i].seriesName + ' : ' + params[i].value + '%' + '<br>';
}
if (tab == 'tab1') {
result =
params[0].name +
'<br/>' +
'<table>' +
'<tr><td>' +
params[0].marker +
params[0].seriesName +
'</td><td style="font-weight: bold;">' +
'&nbsp;&nbsp;&nbsp;&nbsp;' +
params[0].value +
'%' +
'</td></tr>' +
'<tr><td>' +
params[1].marker +
params[1].seriesName +
'</td><td style="font-weight: bold;">' +
'&nbsp;&nbsp;&nbsp;&nbsp;' +
params[1].value +
'</td></tr>' +
'</table>';
}
return result;
},
},
toolbox: {
// show: true,
// feature: {
// magicType: { show: true, type: ['line', 'bar'] },
// restore: { show: true },
// saveAsImage: { show: true },
// },
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: [
{
type: 'category',
data: xData,
axisLabel: {
interval: 0, //x
rotate: 0, //0
},
},
],
yAxis: [
{
type: 'value',
name: '通过率',
axisLabel: {
formatter: '{value} %',
},
},
{
type: 'value',
name: '排名',
show: false,
},
],
series: seriesData,
};
option.series = [
{
name: '累计总通过率',
type: 'bar',
data: seriesData[0].data,
//
barWidth: 16,
// barWidth: '60%',
itemStyle: {
normal: {
label: {
show: true, //
position: 'top', //
formatter: '{c}%',
textStyle: {
//
color: 'black',
fontSize: 10,
},
},
color: colors[0],
},
},
},
{
name: '排名',
type: 'bar',
data: seriesData[1].data,
label: {
show: false,
position: 'inside',
formatter: '{c}',
},
yAxisIndex: 1,
color: colors[1],
},
];
myChart.setOption(option, true);
// 使
myChart.on('click', async (params) => {
let name = params.name;
//name
name = name.replace(/\n/g, '');
console.log(
this.collegeOptions.map((item) => item.value),
'collegeOptions'
);
console.log(this.collegeOptions.map((item) => item.value).includes(name), 'bool');
if (params != null && params != undefined && params.name != '' && this.collegeOptions.map((item) => item.value).includes(name)) {
console.log(params, 'params');
let entrydate = this.entrydateOptions.map((item) => item.value);
let queryParams = {
college: [name],
entrydate: entrydate,
level: 'cet4',
};
let url = this.Url.getCollegeRate;
this.map2loading = true;
this.map3loading = true;
let result = await defHttp.post({ url: url, data: queryParams });
if (!result) {
return;
}
result = result.data[name];
let collegeOption = {
title: {
text: name + '四级通过率',
textStyle: {
fontSize: 14,
},
},
xAxis: {
data: result.map((item) => item.entrydate),
},
//
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
formatter: function (params) {
//tooltip,
let result = params[0].name + '<br>';
for (let i = 0; i < params.length; i++) {
result += params[i].marker + name + ' : ' + params[i].value + '%' + '<br>';
}
return result;
},
},
series: {
type: 'line',
data: result.map((item) => item.passRate),
label: {
show: true, //
position: 'top', //
formatter: function (params) {
//
return params.value + '%';
},
//
},
universalTransition: {
// enabled: true,
// divideShape: 'clone',
},
},
yAxis: {},
animationDurationUpdate: 500,
};
//
let querMajorParam = {
college: [[name]],
entrydate: [this.oneentrydate],
level: 'cet4',
};
let urlMajor = this.Url.getMajorRate;
let resultMajor = await defHttp.post({ url: urlMajor, data: querMajorParam });
if (!resultMajor) {
return;
}
//result = result.data[name];
// result.data
const majors = Object.keys(resultMajor.data); //
const passRates = majors.map((major) => {
const entries = resultMajor.data[major];
return entries.length > 0 ? parseFloat(entries[0].passRate) : 0; //
});
xData = majors.map((label) => label.split('').join('\n')); //x
var majorOption = {
tooltip: {
trigger: 'item',
conginee: false,
//
itemStytle: {
fontSize: 100,
},
},
title: {
text: name + '各专业四级通过率',
textStyle: {
fontSize: 14,
},
},
xAxis: {
type: 'category',
data: xData,
axisLabel: {
interval: 0, //x
//rotate: -10, //30
},
//data: result.map((item) => item.major),
//data: ['', '', '', '', '西'],
},
yAxis: {
type: 'value',
},
series: [
{
//
label: {
show: true,
fontSize: 14,
overflow: 'truncate',
position: 'top',
formatter: function (params) {
//
return params.value + '%';
},
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
//
label: {
show: false,
fontSize: 14,
fontWeight: 'bold',
},
},
data: passRates,
//data: result.map((item) => item.passRate),
//data: [200, 150, 80, 70, 110, 130],
type: 'bar',
},
],
animationDurationUpdate: 500,
};
this.map3loading = false;
this.map2loading = false;
this.$nextTick(() => {
var map2Chart = echarts.init(document.getElementById('map2'));
map2Chart.setOption(collegeOption, true);
var map3Chart = echarts.init(document.getElementById('map3'));
map3Chart.setOption(majorOption, true);
});
}
});
},
//
async getBatch() {
const getBatch = await defHttp.get({ url: this.Url.getBatch });
this.batchOptions = getBatch.batches;
this.batch = this.batchOptions[0].value;
this.level = this.levelOptions[0].value;
},
//
async getEntrydate() {
const getEntrydate = await defHttp.get({ url: this.Url.getEntrydate });
this.entrydateOptions = getEntrydate.entrydates;
this.oneentrydate = this.entrydateOptions[0].value;
// this.collegeentrydate = [this.entrydateOptions[0].value];
// this.majorentrydate = [this.entrydateOptions[0].value];
// this.entrydate.push (this.entrydateOptions[0].value);
},
//
async query(query, college, entrydate) {
this.visible = false;
let result = null;
// console.log(this.college, this.entrydate);
// //this.collegethis.batchnull
// if (this.college === null || this.college.length === 0) {
// this.college = [''];
// }
// if (!this.entrydate || this.entrydate.length === 0) {
// this.entrydate = ['2017'];
// }
try {
// console.log(this.college, this.entrydate)
let params = {
college: college,
entrydate: entrydate,
level: 'cet4',
};
let url = query == 'tab1' ? this.Url.getAllRate : this.Url.getCollegeRate;
console.log(params.college, 'college');
result = await defHttp.post({ url: url, params });
//使passRate
// result.data.sort((a, b) => {
// return b.passRate - a.passRate;
// });
console.log('data', result.data);
} finally {
this.map1loading = false;
this.$nextTick(() => {
this.dataChart(result.data, query);
});
}
},
},
};
</script>
<style lang="less" scoped>
.container {
// display: flex;
background-color: #fff;
padding: 15px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
}
.query {
//
display: flex;
justify-content: flex-end;
align-items: center;
margin-left: 24px;
padding-bottom: 10px;
}
.tab3 {
justify-content: flex-start;
align-items: center;
margin-left: 24px;
}
.title {
font-size: 34px;
color: rgb(8, 8, 8);
font-weight: bold;
}
</style>

View File

@ -1,425 +0,0 @@
<template>
<div style="background: #ececec; padding: 15px">
<a-card title="按批次对比分析" :loading="loading" :bordered="false">
<template #extra>
<!-- <a-select v-model:value="college" style="width: 300px" :options="collegeOptions"></a-select> -->
<a-cascader v-model:value="collegeMajor" :options="collegeMajorOptions" change-on-select />
<a-button style="margin-left: 10px" type="primary" @click="query">查询</a-button>
</template>
<a-row :gutter="12">
<a-col :xl="16" :style="{ marginBottom: '24px' }">
<!-- 根据需求 删除 更改-->
<a-card class="tip">
<div style="display: flex">
<div v-if="topCollege != null">
<span style="font-size: 15px; padding-left: 100px">该数据为 </span>
<span style="color: red; font-size: 18px">{{ topCollege }}</span>
<span v-if="topMajor != null && topMajor != ''" style="color: red; font-size: 18px"> / {{ topMajor }}</span>
<span style="font-size: 15px"> 的四级通过率变化趋势 </span>
</div>
</div>
</a-card>
<a-card>
<div>
<div id="map1" style="width: 100%; height: 400px"></div>
</div>
</a-card>
</a-col>
<a-col :xl="8">
<a-card :loading="participate_Piechartloading" style="margin-bottom: 10px">
<div style="width: 100%; height: 210px; padding: 10px" class="participate_Piechart" id="participate_Piechart"></div>
</a-card>
<a-card :loading="pass_Piechartloading">
<div style="width: 100%; height: 210px; padding: 10px" class="pass_Piechart" id="pass_Piechart"></div>
</a-card>
</a-col>
</a-row>
</a-card>
</div>
</template>
<script>
import { defHttp } from '/@/utils/http/axios';
import * as echarts from 'echarts';
export default {
data() {
return {
Url: {
getBatch: '/cet/getBatch',
getCollege: '/cet/getCollege',
getRate: '/cet/getRateByAllBatch',
getCollegeMajor: '/cet/getCollegeMajor',
getRateByMajor: '/cet/getgetRateByMajor',
getRateByMajorAndLastestBatch: '/cet/getRateByMajorAndLastestBatch',
getRateByEntryDate: '/cet/getRateByEntryDate',
},
loading: false,
participate_Piechartloading: false,
pass_Piechartloading: false,
collegeOptions: [],
collegeMajorOptions: [],
batchOptions: [],
levelOptions: [
{ value: 'cet4', label: '英语四级' },
{ value: 'cet6', label: '英语六级' },
],
level: null,
college: null,
batch: null,
collegeMajor: null,
topCollege: null, //
topMajor: null, //
};
},
mounted() {
this.getCollegeMajorData();
this.getBatch();
this.query();
},
methods: {
dataChart(data) {
let xData = [];
let yData = [];
for (let key in data) {
xData.push(key);
// %
yData.push(data[key].toFixed(1));
}
let myChart = echarts.init(document.getElementById('map1'));
//
let option = {
title: {
// text: ' / ',
},
xAxis: {
type: 'category',
data: xData,
axisLabel: {
interval: 1, //x
rotate: -10, //30
},
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
formatter: function (params) {
return (
params[0].name +
'<br/>' +
'<table>' +
'<tr><td>' +
params[0].marker +
'</td><td style="font-weight: bold;">' +
'&nbsp;&nbsp;&nbsp;&nbsp;' +
params[0].value +
'%' +
'</td></tr>' +
'<tr>' +
'</table>'
);
},
},
yAxis: {
type: 'value',
name: '通过率',
axisLabel: {
formatter: '{value} %',
},
},
series: [
{
data: yData,
type: 'line',
itemStyle: {
normal: {
label: {
show: true, //
position: 'top', //
textStyle: {
//
color: 'black',
fontSize: 12,
},
formatter: '{c}%',
},
},
},
},
],
};
// 使
myChart.setOption(option);
},
//--
async draPieChart_Participate() {
console.log(this.collegeMajor, 'collegeMajor');
let college = this.collegeMajor[0];
let major = this.collegeMajor[1];
console.log(college, major, 'college major');
let queryParams = {
college: college,
major: major,
level: 'cet4',
};
let url = this.Url.getRateByMajorAndLastestBatch;
this.participate_Piechartloading = true;
console.log(queryParams, 'queryParams');
let result = await defHttp.post({ url: url, data: queryParams });
if (!result) {
return;
}
console.log(result.data, 'result');
let titleText = major ? `${college} / ${major}专业 最新批次通过人数` : `${college} 最新批次通过人数`;
// 使 API
let chartData = [];
for (let key in result.data) {
chartData.push({
name: key + '级',
value: result.data[key].passRate,
});
}
console.log(chartData, '123');
let xData = chartData.map((item) => item.name); //
let yData = chartData.map((item) => item.value); //
let option = {
grid: {
left: '20%', //
right: '5%', //
top: '20%', //
bottom: '10%', //
},
title: {
text: '最新批次每一年级的通过率',
left: 'top',
top: '0%',
textStyle: {
fontSize: 14,
},
},
tooltip: {
trigger: 'item',
},
xAxis: {
type: 'category',
data: xData,
//data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
},
yAxis: {
type: 'value',
axisLabel: {
formatter: '{value}%',
},
},
series: [
{
name: titleText,
data: yData,
//data: [150, 230, 224, 218, 135, 147, 260],
type: 'line',
itemStyle: {
normal: {
label: {
show: true, //
position: 'top', //
textStyle: {
//
color: 'black',
fontSize: 12,
},
formatter: '{c}%',
},
},
},
},
],
animationDurationUpdate: 500,
};
this.participate_Piechartloading = false;
this.$nextTick(() => {
var myChart = echarts.init(document.getElementById('participate_Piechart'));
myChart.setOption(option);
});
},
//--/
async draPieChart_Pass() {
//console.log(this.passRatePie);
//console.log('piedata', piedata);
let college = this.collegeMajor[0];
let major = this.collegeMajor[1];
let queryParams = {
college: college,
major: major,
level: 'cet4',
};
let url = this.Url.getRateByEntryDate;
this.pass_Piechartloading = true;
console.log(queryParams, 'queryParams');
let result = await defHttp.post({ url: url, data: queryParams });
if (!result) {
return;
}
console.log(result, 'result');
// result
let xData = result.data.map((item) => item.entryDate); // X
let values = result.data.map((item) => item.passRate); //
console.log(xData, values, 'xData values');
let titleText = major ? `${college} / ${major}专业 每个年级的通过率柱状图` : `${college} 每个年级的通过率柱状图`;
let option = {
grid: {
left: '15%', //
top: '20%', //
right: '10%', //
bottom: '10%', //
},
tooltip: {
trigger: 'item',
formatter: function (params) {
// %
return `${params.marker}${params.name} ${params.value}%`;
},
},
title: {
text: titleText,
left: 'left',
top: '0%',
textStyle: {
fontSize: 14,
},
},
xAxis: {
type: 'category',
data: xData,
},
yAxis: {
type: 'value',
axisLabel: {
formatter: '{value}%', // Y
},
},
series: [
{
data: values,
type: 'bar',
showBackground: true,
backgroundStyle: {
color: 'rgba(180, 180, 180, 0.2)',
},
label: {
show: true, //
position: 'top', //
formatter: '{c}%', //
fontSize: '10px',
},
},
],
animationDurationUpdate: 500,
};
this.pass_Piechartloading = false;
this.$nextTick(() => {
var myChart = echarts.init(document.getElementById('pass_Piechart'));
myChart.setOption(option);
});
},
//
async getBatch() {
const getBatch = await defHttp.get({ url: this.Url.getBatch });
// const getCollege = await defHttp.get({ url: this.Url.getCollege });
// this.collegeOptions = getCollege.colleges;
// this.college = this.collegeOptions[0].value;
this.level = this.levelOptions[0].value;
// this.query();
},
//
async getCollegeMajorData() {
const res = await defHttp.get({ url: this.Url.getCollegeMajor });
//map
this.collegeMajorOptions = res.collegeMajor.map((item) => {
return {
value: item.college,
label: item.college,
children: item.major.map((major) => {
return {
value: major,
label: major,
};
}),
};
});
this.collegeMajor = ['东语学院', '日语'];
},
//
async query() {
let data = null;
console.log(this.collegeMajor, 'collegeMajor');
try {
//this.collegeMajornull
if (!this.collegeMajor) {
this.collegeMajor = ['东语学院', '日语'];
}
let major = null;
let college = null;
//this.collegeMajor[1]null
college = this.collegeMajor[0];
major = this.collegeMajor.length > 1 ? this.collegeMajor[1] : '';
this.topCollege = college;
this.topMajor = major;
this.loading = true;
console.log(college, major, 'college,major');
let params = {
college: college,
major: major,
level: 'cet4',
};
data = await defHttp.get({ url: this.Url.getRate, params });
console.log(data, 'data');
} finally {
this.loading = false;
this.$nextTick(() => {
this.dataChart(data);
this.draPieChart_Pass();
this.draPieChart_Participate();
});
}
},
},
};
</script>
<style lang="less" scoped>
.container {
// display: flex;
background-color: #fff;
padding: 15px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
}
.title {
font-size: 20px;
color: rgb(8, 8, 8);
font-weight: bold;
}
.tip {
margin-bottom: 10px;
background-color: #d0e5fe;
opacity: 0.9;
}
</style>

View File

@ -1,477 +0,0 @@
<template>
<div style="background: #ececec; padding: 15px">
<a-card title="按单批次对比分析" :loading="loading" :bordered="false">
<template #extra>
<a-select v-model:value="collegeMajor" style="width: 300px" :options="collegeMajorOptions" />
<a-select v-model:value="batch" :options="batchOptions" change-on-select style="margin-left: 10px; width: 120px" />
<a-button style="margin-left: 10px" type="primary" @click="query">查询</a-button>
</template>
<a-row :grutter="24">
<a-col :xl="16" :style="{ marginBottom: '24px' }">
<!-- 根据需求 删除 更改-->
<a-card style="margin-bottom: 10px" class="tip">
<div style="display: flex">
<div v-if="topCollege != null">
<span style="color: red; font-size: 18px; padding-left: 100px"> {{ topCollege }}</span>
<span style="font-size: 15px"> </span>
<span style="color: red; font-size: 18px"> {{ topBath }} </span>
<span style="font-size: 15px"> 批次的四级通过率为</span>
<span style="color: red; font-size: 18px"> {{ sumRate }}%</span>
</div>
</div>
</a-card>
<a-card>
<div>
<div id="map1" style="width: 100%; height: 400px"></div>
</div>
</a-card>
</a-col>
<a-col :xl="8" style="padding-left: 10px">
<a-card :loading="participate_Piechartloading" style="margin-bottom: 10px">
<div style="width: 100%; height: 210px; padding: 10px" class="participate_Piechart" id="participate_Piechart"></div>
</a-card>
<a-card :loading="tableloading" style="height: 268px">
<div>
<a-table
:dataSource="dataSourceCet4"
:columns="columns"
:pagination="false"
bordered
class="custom-table"
:style="{ fontSize: '12px', color: '#333', marginTop: '-11px' }"
/>
</div>
</a-card>
</a-col>
</a-row>
</a-card>
</div>
</template>
<script>
//import { pagination } from 'mock/_util';
import { red } from '@ant-design/colors';
import { defHttp } from '/@/utils/http/axios';
import * as echarts from 'echarts';
export default {
data() {
return {
dataSourceCet4: [],
columns: [
{
title: '年级',
dataIndex: 'grade',
key: 'grade',
align: 'center',
},
{
title: '参加人数',
dataIndex: 'attendNumber',
key: 'attendNumber',
align: 'center',
},
{
title: '通过人数',
dataIndex: 'passNumber',
key: 'passNumber',
align: 'center',
},
{
title: '通过率',
dataIndex: 'passrate',
key: 'passrate',
align: 'center',
},
],
Url: {
getBatch: '/cet/getBatch',
getCollege: '/cet/getCollege',
getRate: '/cet/getRateByBatch',
getCollegeMajor: '/cet/getCollegeMajor',
},
loading: false,
participate_Piechartloading: false,
tableloading: false,
collegeOptions: [],
collegeMajorOptions: [],
batchOptions: [],
levelOptions: [
{ value: 'cet4', label: '英语四级' },
{ value: 'cet6', label: '英语六级' },
],
level: null,
college: null,
batch: null,
collegeMajor: null,
sumRate: null, //
topCollege: null, //
topBath: null, //
};
},
mounted() {
this.getCollegeMajorData();
this.getBatch();
this.query();
},
methods: {
dataChart(data) {
let xData = [];
let yData = [];
for (let key in data) {
xData.push(key);
// %
yData.push(data[key].toFixed(1));
}
xData = xData.map((label) => label.split('').join('\n')); //x
let myChart = echarts.init(document.getElementById('map1'));
//
let option = {
title: {
// text: " / ",
},
xAxis: {
type: 'category',
data: xData,
axisLabel: {
interval: 0, //x
rotate: 0, //30
},
},
//
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
formatter: function (params) {
return (
params[0].name +
'<br/>' +
'<table>' +
'<tr><td>' +
params[0].marker +
'</td><td style="font-weight: bold;">' +
'&nbsp;&nbsp;&nbsp;&nbsp;' +
params[0].value +
'%' +
'</td></tr>' +
'<tr>' +
'</table>'
);
},
},
toolbox: {
// show: true,
// feature: {
// magicType: { show: true, type: ['line', 'bar'] },
// restore: { show: true },
// saveAsImage: { show: true },
// },
},
//
grid: {
left: '3%',
right: '4%',
bottom: '1%', //3%
containLabel: true,
},
yAxis: {
type: 'value',
name: '通过率',
axisLabel: {
formatter: '{value} %',
},
},
series: [
{
barMaxWidth: 50, //
data: yData,
type: 'bar',
itemStyle: {
normal: {
label: {
show: true, //
position: 'top', //
textStyle: {
//
color: 'black',
fontSize: 12,
},
formatter: '{c}%',
},
},
},
},
],
};
// 使
myChart.setOption(option);
},
//
async getBatch() {
const getBatch = await defHttp.get({ url: this.Url.getBatch });
console.log('1111', getBatch);
this.batchOptions = getBatch.batches.map((item) => {
return {
value: item.value,
label: item.label,
};
});
console.log('2222', this.batchOptions);
this.batchOptions.forEach((item) => {
const year = item.label.substring(0, 4);
if (item.label.includes('12-01')) {
item.label = `${year}冬季`;
} else if (item.label.includes('06-01')) {
item.label = `${year}夏季`;
} else if (item.label.includes('09-01')) {
item.label = `${year}夏季`;
} else if (item.label.includes('03-01')) {
item.label = `${year}春季`;
}
});
// const getCollege = await defHttp.get({ url: this.Url.getCollege });
// this.collegeOptions = getCollege.colleges;
// this.college = this.collegeOptions[0].value;
this.level = this.levelOptions[0].value;
// this.query();
},
//
async getCollegeMajorData() {
const res = await defHttp.get({ url: this.Url.getCollegeMajor });
//map
this.collegeMajorOptions = res.collegeMajor.map((item) => {
return {
value: item.college,
label: item.college,
children: item.major.map((major) => {
return {
value: major,
label: major,
};
}),
};
});
this.collegeMajorOptions.unshift({ value: '全校', label: '全校' });
this.collegeMajor = this.collegeMajorOptions[0].value;
console.log(this.collegeMajor);
console.log(this.collegeMajorOptions);
},
//
async query() {
this.getBatch();
let resultData = null;
try {
//this.batchnull
if (!this.batch) {
this.batch = '2017-12-01';
}
if (!this.collegeMajor) {
this.collegeMajor = '全校';
}
//this.batch[1]null
this.topCollege = this.collegeMajor;
this.topBath = this.batch;
this.loading = true;
let params = {
batch: this.batch,
college: this.collegeMajor,
level: 'cet4',
};
resultData = await defHttp.get({ url: this.Url.getRate, params });
this.sumRate = resultData.sumRate;
console.log('result', resultData);
//
let tableData = [];
for (let grade in resultData.gradeData) {
tableData.push({
grade: grade + '级',
attendNumber: resultData.gradeData[grade].allStudent,
passNumber: resultData.gradeData[grade].passed,
passrate: resultData.gradeData[grade].passRate,
});
}
this.dataSourceCet4 = tableData; //
console.log(this.dataSourceCet4, 'dataSourceCet4');
} finally {
this.loading = false;
this.$nextTick(() => {
this.dataChart(resultData.data);
this.draPieChart_Participate();
});
}
},
//--
async draPieChart_Participate() {
//console.log(this.passRatePie);
//console.log('piedata', piedata);
let queryParams = {
batch: this.batch,
college: this.collegeMajor,
level: 'cet4',
};
console.log(queryParams, 'queryParams');
let url = this.Url.getRate;
this.participate_Piechartloading = true;
this.tableloading = true;
let result = await defHttp.get({ url: url, params: queryParams });
let totalRate = 0;
for (let key in result.gradeData) {
totalRate += parseFloat(result.gradeData[key].passRate);
}
console.log(totalRate, 'totalRate');
console.log(result, 'result');
let chartsData = [];
for (let key in result.gradeData) {
chartsData.push({
name: key + '级',
value: (parseFloat(result.gradeData[key].passRate) / totalRate) * 100,
});
}
let option = {
tooltip: {
trigger: 'item',
conginee: false,
formatter: function (params) {
return params.name + ': ' + params.value.toFixed(1) + '%';
},
textStyle: {
color: '#ec2e30', //
fontSize: 14, //
fontWeight: 'bold', //
},
//
itemStytle: {
//
shadowBlur: 10, //
shadowColor: 'rgba(0, 0, 0, 0.3)', //
shadowOffsetX: 5, //
shadowOffsetY: 5, //
fontSize: 100,
},
},
title: {
text: '该批次每一年级的通过率环形图',
left: 'top',
top: '0%',
textStyle: {
fontSize: 12,
},
},
legend: {
top: '10%',
left: 'center',
itemGap: 3,
selectedMode: true,
},
color: ['#ec2e30', '#c75af1', '#2b9eef', '#eee03f', '#2aed91', '#c5ed3c'],
series: [
{
name: '学院/专业该批次通过人数环形图',
type: 'pie',
radius: ['50%', '100%'],
center: ['50%', '50%'],
startAngle: 130,
top: 80,
//
itemStyle: {
// borderRadius: 5,
// borderColor: '#fff',
borderWidth: 2,
},
label: {
show: true,
// position: 'inside',
fontSize: 12,
overflow: 'truncate',
},
labelLine: {
show: true,
smooth: true,
length: 10,
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
label: {
show: true,
fontSize: 15,
fontWeight: 'bold',
},
},
data: chartsData,
},
{
type: 'pie',
startAngle: 130,
top: 80,
radius: ['40%', '51%'],
center: ['50%', '50%'],
//
itemStyle: {
// borderRadius: 3,
// borderColor: '#fff',
borderWidth: 2,
},
label: {
show: false,
},
hoverAnimation: false,
legendHoverLink: false,
// animationnfalse,
tooltip: {
show: false,
},
data: chartsData,
},
],
animationDurationUpate: 500,
};
this.participate_Piechartloading = false;
this.tableloading = false;
this.$nextTick(() => {
var myChart = echarts.init(document.getElementById('participate_Piechart'));
myChart.setOption(option);
});
},
},
};
</script>
<style lang="less" scoped>
.container {
// display: flex;
background-color: #fff;
padding: 15px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
}
.title {
font-size: 20px;
color: rgb(8, 8, 8);
font-weight: bold;
}
.tip {
margin-bottom: 10px;
background-color: #d0e5fe;
opacity: 0.9;
}
</style>

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