Merge branch 'yudaocode:master' into master

This commit is contained in:
rohit 2025-05-06 16:46:35 +08:00 committed by GitHub
commit 6289bbd3e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
73 changed files with 926 additions and 2071 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 32 KiB

BIN
.image/demo/vue3-ep.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

View File

@ -11,7 +11,7 @@
* nodejs > 16.18.0 && pnpm > 8.6.0 (强制使用pnpm)
* 演示地址【Vue3 + element-plus】<http://dashboard-vue3.yudao.iocoder.cn>
* 演示地址【Vue3 + vben(ant-design-vue)】:<http://dashboard-vben.yudao.iocoder.cn>
* 演示地址【Vue3 + vben5.0(ant-design-vue)】:<http://dashboard-vben.yudao.iocoder.cn>
* 演示地址【Vue2 + element-ui】<http://dashboard.yudao.iocoder.cn>
* 启动文档:<https://doc.iocoder.cn/quick-start/>
* 视频教程:<https://doc.iocoder.cn/video/>
@ -24,7 +24,7 @@
* 改换 saas自动引入等功能
* 使用 Element Plus 免费开源的中后台模版,具备如下特性:
![首页](public/home.png)
![首页](.image/demo/vue3-ep.png)
* **最新技术栈**:使用 Vue3、Vite4 等前端前沿技术开发
* **TypeScript**: 应用程序级 JavaScript 的语言
@ -38,15 +38,15 @@
| 框架 | 说明 | 版本 |
|----------------------------------------------------------------------|------------------|--------|
| [Vue](https://staging-cn.vuejs.org/) | Vue 框架 | 3.3.8 |
| [Vue](https://staging-cn.vuejs.org/) | Vue 框架 | 3.3.8 |
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 4.5.0 |
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.4.2 |
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.4.2 |
| [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 的超集 | 5.2.2 |
| [pinia](https://pinia.vuejs.org/) | Vue 存储库 替代 vuex5 | 2.1.7 |
| [pinia](https://pinia.vuejs.org/) | Vue 存储库 替代 vuex5 | 2.1.7 |
| [vueuse](https://vueuse.org/) | 常用工具集 | 10.6.1 |
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.6.5 |
| [vue-router](https://router.vuejs.org/) | Vue 路由 | 4.2.5 |
| [unocss](https://uno.antfu.me/) | 原子 css | 0.57.4 |
| [unocss](https://uno.antfu.me/) | 原子 css | 0.57.4 |
| [iconify](https://icon-sets.iconify.design/) | 在线图标库 | 3.1.1 |
| [wangeditor](https://www.wangeditor.com/) | 富文本编辑器 | 5.1.23 |
@ -121,9 +121,9 @@
基于 Flowable 构建,可支持信创(国产)数据库,满足中国特色流程操作:
| BPMN 设计器 | 钉钉/飞书设计器 |
|------------------------------|--------------------------------|
| ![](/.image/工作流设计器-bpmn.jpg) | ![](/.image/工作流设计器-simple.jpg) |
| BPMN 设计器 | 钉钉/飞书设计器 |
|-----------------------------|-------------------------------|
| ![](.image/工作流设计器-bpmn.jpg) | ![](.image/工作流设计器-simple.jpg) |
> 历经头部企业生产验证,工作流引擎须标配仿钉钉/飞书 + BPMN 双设计器!!!
>

View File

@ -13,7 +13,7 @@ import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import viteCompression from 'vite-plugin-compression'
import topLevelAwait from 'vite-plugin-top-level-await'
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons-ng'
import UnoCSS from 'unocss/vite'
export function createVitePlugins() {
@ -78,7 +78,6 @@ export function createVitePlugins() {
createSvgIconsPlugin({
iconDirs: [pathResolve('src/assets/svgs')],
symbolId: 'icon-[dir]-[name]',
svgoOptions: true
}),
viteCompression({
verbose: true, // 是否在控制台输出压缩结果

View File

@ -1,6 +1,6 @@
{
"name": "yudao-ui-admin-vue3",
"version": "2.4.1-snapshot",
"version": "2.4.2-snapshot",
"description": "基于vue3、vite4、element-plus、typesScript",
"author": "xingyu",
"private": false,
@ -133,7 +133,7 @@
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-progress": "^0.0.7",
"vite-plugin-purge-icons": "^0.10.0",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-svg-icons-ng": "^1.3.1",
"vite-plugin-top-level-await": "^1.4.4",
"vue-eslint-parser": "^9.3.2",
"vue-tsc": "^1.8.27"

2042
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

View File

@ -20,6 +20,6 @@ export const deleteWorkflow = async (id) => {
return await request.delete({ url: '/ai/workflow/delete?id=' + id })
}
export const updateWorkflowModel = async (data) => {
return await request.put({ url: '/ai/workflow/updateWorkflowModel', data })
export const testWorkflow = async (data) => {
return await request.post({ url: '/ai/workflow/test', data })
}

View File

@ -46,11 +46,6 @@ export type DatabaseTableVO = {
comment: string
}
export type CodegenDetailVO = {
table: CodegenTableVO
columns: CodegenColumnVO[]
}
export type CodegenPreviewVO = {
filePath: string
code: string
@ -61,11 +56,6 @@ export type CodegenUpdateReqVO = {
columns: CodegenColumnVO[]
}
export type CodegenCreateListReqVO = {
dataSourceConfigId: number
tableNames: string[]
}
// 查询列表代码生成表定义
export const getCodegenTableList = (dataSourceConfigId: number) => {
return request.get({ url: '/infra/codegen/table/list?dataSourceConfigId=' + dataSourceConfigId })
@ -81,11 +71,6 @@ export const getCodegenTable = (id: number) => {
return request.get({ url: '/infra/codegen/detail?tableId=' + id })
}
// 新增代码生成表定义
export const createCodegenTable = (data: CodegenCreateListReqVO) => {
return request.post({ url: '/infra/codegen/create', data })
}
// 修改代码生成表定义
export const updateCodegenTable = (data: CodegenUpdateReqVO) => {
return request.put({ url: '/infra/codegen/update', data })

View File

@ -1,11 +1,5 @@
import request from '@/config/axios'
export interface FilePageReqVO extends PageParam {
path?: string
type?: string
createTime?: Date[]
}
// 文件预签名地址 Response VO
export interface FilePresignedUrlRespVO {
// 文件配置编号
@ -17,7 +11,7 @@ export interface FilePresignedUrlRespVO {
}
// 查询文件列表
export const getFilePage = (params: FilePageReqVO) => {
export const getFilePage = (params: PageParam) => {
return request.get({ url: '/infra/file/page', params })
}

View File

@ -11,6 +11,7 @@ export interface FileClientConfig {
bucket?: string
accessKey?: string
accessSecret?: string
enablePathStyleAccess?: boolean
domain: string
}

View File

@ -1,5 +1,4 @@
import request from '@/config/axios'
import { getRefreshToken } from '@/utils/auth'
import type { RegisterVO, UserLoginVO } from './types'
export interface SmsCodeVO {
@ -72,7 +71,6 @@ export const socialAuthRedirect = (type: number, redirectUri: string) => {
}
// 获取验证图片以及 token
export const getCode = (data: any) => {
debugger
return request.postOriginal({ url: 'system/captcha/get', data })
}

View File

@ -41,6 +41,11 @@ export const getTenant = (id: number) => {
return request.get({ url: '/system/tenant/get?id=' + id })
}
// 获取租户精简信息列表
export const getTenantList = () => {
return request.get({ url: '/system/tenant/simple-list' })
}
// 新增租户
export const createTenant = (data: TenantVO) => {
return request.post({ url: '/system/tenant/create', data })

View File

@ -22,11 +22,6 @@ export const getUserPage = (params: PageParam) => {
return request.get({ url: '/system/user/page', params })
}
// 查询所有用户列表
export const getAllUser = () => {
return request.get({ url: '/system/user/all' })
}
// 查询用户详情
export const getUser = (id: number) => {
return request.get({ url: '/system/user/get?id=' + id })
@ -48,7 +43,7 @@ export const deleteUser = (id: number) => {
}
// 导出用户
export const exportUser = (params) => {
export const exportUser = (params: any) => {
return request.download({ url: '/system/user/export', params })
}
@ -58,7 +53,7 @@ export const importUserTemplate = () => {
}
// 用户密码重置
export const resetUserPwd = (id: number, password: string) => {
export const resetUserPassword = (id: number, password: string) => {
const data = {
id,
password

View File

@ -32,10 +32,11 @@ export interface ProfileVO {
}
export interface UserProfileUpdateReqVO {
nickname: string
email: string
mobile: string
sex: number
nickname?: string
email?: string
mobile?: string
sex?: number
avatar?: string
}
// 查询用户个人信息
@ -58,8 +59,3 @@ export const updateUserPassword = (oldPassword: string, newPassword: string) =>
}
})
}
// 用户头像上传
export const uploadAvatar = (data) => {
return request.upload({ url: '/system/user/profile/update-avatar', data: data })
}

View File

@ -1,5 +1,5 @@
<template>
<div>
<div @click.stop>
<Dialog
v-model="dialogVisible"
:canFullscreen="false"
@ -181,6 +181,7 @@ function openModal() {
}
function closeModal() {
debugger
dialogVisible.value = false
}

View File

@ -91,7 +91,7 @@ const dialogStyle = computed(() => {
icon="ep:close"
hover-color="var(--el-color-primary)"
color="var(--el-color-info)"
@click="close"
@click.stop="close"
/>
</div>
</div>

View File

@ -13,7 +13,7 @@ export const CouponDiscount = defineComponent({
setup(props) {
const coupon = props.coupon as CouponTemplateApi.CouponTemplateVO
// 折扣
let value = coupon.discountPercent + ''
let value = coupon.discountPercent / 10 + ''
let suffix = ' 折'
// 满减
if (coupon.discountType === PromotionDiscountTypeEnum.PRICE.type) {
@ -43,7 +43,7 @@ export const CouponDiscountDesc = defineComponent({
const discountDesc =
coupon.discountType === PromotionDiscountTypeEnum.PRICE.type
? `${floatToFixed2(coupon.discountPrice)}`
: `${coupon.discountPercent}`
: `${coupon.discountPercent / 10.0}`
return () => (
<div>
<span>{useCondition}</span>

View File

@ -49,7 +49,13 @@
<div class="flex flex-col justify-evenly gap-4px">
<!-- 优惠值 -->
<CouponDiscount :coupon="coupon" />
<div>{{ coupon.name }}</div>
<!-- 优惠描述 -->
<CouponDiscountDesc :coupon="coupon" />
<!-- 领取说明 -->
<div v-if="coupon.totalCount >= 0">
仅剩{{ coupon.totalCount - coupon.takeCount }}
</div>
<div v-else-if="coupon.totalCount === -1">仅剩不限制</div>
</div>
<div class="flex flex-col">
<div
@ -67,7 +73,8 @@
<div v-else class="flex flex-col items-center justify-around gap-4px p-4px">
<!-- 优惠值 -->
<CouponDiscount :coupon="coupon" />
<div>{{ coupon.name }}</div>
<!-- 优惠描述 -->
<CouponDiscountDesc :coupon="coupon" />
<div
class="rounded-20px p-x-8px p-y-2px"
:style="{
@ -124,7 +131,7 @@ watch(
() => {
// - * ( - 1)/
couponWidth.value =
(phoneWidth.value * 0.95 - props.property.space * (props.property.columns - 1)) /
(phoneWidth.value - props.property.space * (props.property.columns - 1)) /
props.property.columns
//
scrollbarWidth.value = `${

View File

@ -1,16 +1,19 @@
<template>
<div
class="relative"
:style="{ height: `${rowCount * CUBE_SIZE}px`, width: `${4 * CUBE_SIZE}px` }"
:style="{
height: `${rowCount * CUBE_SIZE}px`,
width: `${4 * CUBE_SIZE}px`,
padding: `${property.space}px`
}"
>
<div
v-for="(item, index) in property.list"
:key="index"
class="absolute"
:style="{
width: `${item.width * CUBE_SIZE - property.space * 2}px`,
height: `${item.height * CUBE_SIZE - property.space * 2}px`,
margin: `${property.space}px`,
width: `${item.width * CUBE_SIZE - property.space}px`,
height: `${item.height * CUBE_SIZE - property.space}px`,
top: `${item.top * CUBE_SIZE}px`,
left: `${item.left * CUBE_SIZE}px`
}"
@ -63,10 +66,10 @@ const rowCount = computed(() => {
let count = 0
if (props.property.list.length > 0) {
//
count = Math.max(...props.property.list.map((item) => item.bottom))
count = Math.max(...props.property.list.map((item) => item.top + item.height))
}
// 0 1
return count + 1
//
return count == 0 ? 1 : count
})
</script>

View File

@ -39,7 +39,7 @@
</span>
</div>
</div>
</el-carousel-item>
</el-carousel-item>
</el-carousel>
</template>
@ -51,7 +51,7 @@ const props = defineProps<{ property: MenuSwiperProperty }>()
//
const TITLE_HEIGHT = 20
//
const ICON_SIZE = 42
const ICON_SIZE = 32
//
const SPACE_Y = 16

View File

@ -29,7 +29,10 @@
<ColorInput v-model="formData.bgColor" />
</el-form-item>
<el-form-item label="背景图片" prop="bgImg" v-else>
<UploadImg v-model="formData.bgImg" :limit="1" width="56px" height="56px" />
<div class="flex items-center">
<UploadImg v-model="formData.bgImg" :limit="1" width="56px" height="56px" />
<span class="text-xs text-gray-400 ml-2 mb-2">建议宽度750</span>
</div>
</el-form-item>
<el-card class="property-group" shadow="never">
<template #header>
@ -39,8 +42,9 @@
<el-checkbox
v-model="formData._local.previewMp"
@change="formData._local.previewOther = !formData._local.previewMp"
>预览</el-checkbox
>
预览
</el-checkbox>
</el-form-item>
</div>
</template>
@ -54,8 +58,9 @@
<el-checkbox
v-model="formData._local.previewOther"
@change="formData._local.previewMp = !formData._local.previewOther"
>预览</el-checkbox
>
预览
</el-checkbox>
</el-form-item>
</div>
</template>

View File

@ -82,8 +82,8 @@ export const component = {
bgEndColor: '#FE832A',
imgUrl: ''
},
borderRadiusTop: 8,
borderRadiusBottom: 8,
borderRadiusTop: 6,
borderRadiusBottom: 6,
space: 8,
spuIds: [],
style: {

View File

@ -14,7 +14,10 @@
:key="index"
>
<!-- 角标 -->
<div v-if="property.badge.show" class="absolute left-0 top-0 z-1 items-center justify-center">
<div
v-if="property.badge.show && property.badge.imgUrl"
class="absolute left-0 top-0 z-1 items-center justify-center"
>
<el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" />
</div>
<!-- 商品封面图 -->

View File

@ -3,7 +3,7 @@
<!-- 表单 -->
<el-form label-width="80px" :model="formData" class="m-t-8px">
<el-card header="搜索热词" class="property-group" shadow="never">
<Draggable v-model="formData.hotKeywords" :empty-item="''">
<Draggable v-model="formData.hotKeywords" :empty-item="''" :min="0">
<template #default="{ index }">
<el-input v-model="formData.hotKeywords[index]" placeholder="请输入热词" />
</template>
@ -61,6 +61,7 @@
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import { SearchProperty } from '@/components/DiyEditor/components/mobile/SearchBar/config'
import { isString } from '@/utils/is'
/** 搜索框属性面板 */
defineOptions({ name: 'SearchProperty' })
@ -68,6 +69,19 @@ defineOptions({ name: 'SearchProperty' })
const props = defineProps<{ modelValue: SearchProperty }>()
const emit = defineEmits(['update:modelValue'])
const formData = useVModel(props, 'modelValue', emit)
//
watch(
() => formData.value.hotKeywords,
(newVal) => {
//
const nonStringIndex = newVal.findIndex((item) => !isString(item))
if (nonStringIndex !== -1) {
formData.value.hotKeywords[nonStringIndex] = ''
}
},
{ deep: true, flush: 'post' }
)
</script>
<style scoped lang="scss"></style>

View File

@ -1,7 +1,9 @@
import {ComponentStyle, DiyComponent} from '@/components/DiyEditor/util'
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
/** 标题栏属性 */
export interface TitleBarProperty {
// 背景图
bgImgUrl: string
// 偏移
marginLeft: number
// 显示位置
@ -22,6 +24,8 @@ export interface TitleBarProperty {
titleColor: string
// 描述颜色
descriptionColor: string
// 高度
height: number
// 查看更多
more: {
// 是否显示查看更多
@ -52,6 +56,8 @@ export const component = {
descriptionWeight: 200,
titleColor: 'rgba(50, 50, 51, 10)',
descriptionColor: 'rgba(150, 151, 153, 10)',
marginLeft: 0,
height: 40,
more: {
//查看更多
show: false,

View File

@ -1,55 +1,49 @@
<template>
<div
:style="{
background:
property.style.bgType === 'color' ? property.style.bgColor : `url(${property.style.bgImg})`,
backgroundSize: '100% 100%',
backgroundRepeat: 'no-repeat'
}"
class="title-bar"
>
<!-- 内容 -->
<div>
<div class="title-bar" :style="{ height: `${property.height}px` }">
<el-image v-if="property.bgImgUrl" :src="property.bgImgUrl" fit="cover" class="w-full" />
<div class="absolute left-0 top-0 w-full h-full flex flex-col justify-center">
<!-- 标题 -->
<div
v-if="property.title"
:style="{
fontSize: `${property.titleSize}px`,
fontWeight: property.titleWeight,
color: property.titleColor,
textAlign: property.textAlign
textAlign: property.textAlign,
marginLeft: `${property.marginLeft}px`,
marginBottom: '4px'
}"
v-if="property.title"
>
{{ property.title }}
</div>
<!-- 副标题 -->
<div
v-if="property.description"
:style="{
fontSize: `${property.descriptionSize}px`,
fontWeight: property.descriptionWeight,
color: property.descriptionColor,
textAlign: property.textAlign
textAlign: property.textAlign,
marginLeft: `${property.marginLeft}px`
}"
class="m-t-8px"
v-if="property.description"
>
{{ property.description }}
</div>
</div>
<!-- 更多 -->
<div
class="more"
v-show="property.more.show"
:style="{
color: property.descriptionColor
}"
class="more"
>
<span v-if="property.more.type !== 'icon'"> {{ property.more.text }} </span>
<Icon v-if="property.more.type !== 'text'" icon="ep:arrow-right" />
<Icon icon="ep:arrow-right" v-if="property.more.type !== 'text'" />
</div>
</div>
</template>
<script lang="ts" setup>
<script setup lang="ts">
import { TitleBarProperty } from './config'
/** 标题栏 */
@ -57,7 +51,7 @@ defineOptions({ name: 'TitleBar' })
defineProps<{ property: TitleBarProperty }>()
</script>
<style lang="scss" scoped>
<style scoped lang="scss">
.title-bar {
position: relative;
width: 100%;

View File

@ -1,7 +1,12 @@
<template>
<ComponentContainerProperty v-model="formData.style">
<el-form :model="formData" :rules="rules" label-width="85px">
<el-card class="property-group" header="风格" shadow="never">
<el-form label-width="85px" :model="formData" :rules="rules">
<el-card header="风格" class="property-group" shadow="never">
<el-form-item label="背景图片" prop="bgImgUrl">
<UploadImg v-model="formData.bgImgUrl" width="100%" height="40px">
<template #tip>建议尺寸 750*80</template>
</UploadImg>
</el-form-item>
<el-form-item label="标题位置" prop="textAlign">
<el-radio-group v-model="formData!.textAlign">
<el-tooltip content="居左" placement="top">
@ -16,66 +21,84 @@
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="偏移量" prop="marginLeft" label-width="70px">
<el-slider
v-model="formData.marginLeft"
:max="100"
:min="0"
show-input
input-size="small"
/>
</el-form-item>
<el-form-item label="高度" prop="height" label-width="70px">
<el-slider
v-model="formData.height"
:max="200"
:min="20"
show-input
input-size="small"
/>
</el-form-item>
</el-card>
<el-card class="property-group" header="主标题" shadow="never">
<el-form-item label="文字" label-width="40px" prop="title">
<el-card header="主标题" class="property-group" shadow="never">
<el-form-item label="文字" prop="title" label-width="40px">
<InputWithColor
v-model="formData.title"
v-model:color="formData.titleColor"
maxlength="20"
show-word-limit
maxlength="20"
/>
</el-form-item>
<el-form-item label="大小" label-width="40px" prop="titleSize">
<el-form-item label="大小" prop="titleSize" label-width="40px">
<el-slider
v-model="formData.titleSize"
:max="60"
:min="10"
input-size="small"
show-input
input-size="small"
/>
</el-form-item>
<el-form-item label="粗细" label-width="40px" prop="titleWeight">
<el-form-item label="粗细" prop="titleWeight" label-width="40px">
<el-slider
v-model="formData.titleWeight"
:max="900"
:min="100"
:max="900"
:step="100"
input-size="small"
show-input
input-size="small"
/>
</el-form-item>
</el-card>
<el-card class="property-group" header="副标题" shadow="never">
<el-form-item label="文字" label-width="40px" prop="description">
<el-card header="副标题" class="property-group" shadow="never">
<el-form-item label="文字" prop="description" label-width="40px">
<InputWithColor
v-model="formData.description"
v-model:color="formData.descriptionColor"
maxlength="50"
show-word-limit
maxlength="50"
/>
</el-form-item>
<el-form-item label="大小" label-width="40px" prop="descriptionSize">
<el-form-item label="大小" prop="descriptionSize" label-width="40px">
<el-slider
v-model="formData.descriptionSize"
:max="60"
:min="10"
input-size="small"
show-input
input-size="small"
/>
</el-form-item>
<el-form-item label="粗细" label-width="40px" prop="descriptionWeight">
<el-form-item label="粗细" prop="descriptionWeight" label-width="40px">
<el-slider
v-model="formData.descriptionWeight"
:max="900"
:min="100"
:max="900"
:step="100"
input-size="small"
show-input
input-size="small"
/>
</el-form-item>
</el-card>
<el-card class="property-group" header="查看更多" shadow="never">
<el-card header="查看更多" class="property-group" shadow="never">
<el-form-item label="是否显示" prop="more.show">
<el-checkbox v-model="formData.more.show" />
</el-form-item>
@ -88,7 +111,7 @@
<el-radio value="all">文字+图标</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-show="formData.more.type !== 'icon'" label="更多文字" prop="more.text">
<el-form-item label="更多文字" prop="more.text" v-show="formData.more.type !== 'icon'">
<el-input v-model="formData.more.text" />
</el-form-item>
<el-form-item label="跳转链接" prop="more.url">
@ -99,7 +122,7 @@
</el-form>
</ComponentContainerProperty>
</template>
<script lang="ts" setup>
<script setup lang="ts">
import { TitleBarProperty } from './config'
import { useVModel } from '@vueuse/core'
//
@ -113,4 +136,4 @@ const formData = useVModel(props, 'modelValue', emit)
const rules = {}
</script>
<style lang="scss" scoped></style>
<style scoped lang="scss"></style>

View File

@ -28,7 +28,7 @@
<Icon
icon="ep:delete"
class="cursor-pointer text-red-5"
v-if="formData.length > 1"
v-if="formData.length > min"
@click="handleDelete(index)"
/>
</el-tooltip>
@ -69,7 +69,9 @@ const props = defineProps({
//
emptyItem: any<unknown>().def({}),
// 0
limit: propTypes.number.def(0)
limit: propTypes.number.def(0),
// 1
min: propTypes.number.def(1)
})
//
const emit = defineEmits(['update:modelValue'])

View File

@ -69,11 +69,18 @@ export const useApiSelect = (option: ApiSelectProps) => {
if (isEmpty(props.url)) {
return
}
switch (props.method) {
case 'GET':
let url: string = props.url
if (props.remote) {
url = `${url}?${props.remoteField}=${queryParam.value}`
if (queryParam.value != undefined) {
if (url.includes('?')) {
url = `${url}&${props.remoteField}=${queryParam.value}`
} else {
url = `${url}?${props.remoteField}=${queryParam.value}`
}
}
}
parseOptions(await request.get({ url: url }))
break

View File

@ -17,6 +17,7 @@ export const useSelectRule = (option: SelectRuleOption) => {
icon: option.icon,
label,
name,
event: option.event,
rule() {
return {
type: name,

View File

@ -46,5 +46,6 @@ export interface SelectRuleOption {
label: string // label 名称
name: string // 组件名称
icon: string // 组件图标
props?: any[] // 组件规则
props?: any[], // 组件规则
event?: any[] // 事件配置
}

View File

@ -63,7 +63,8 @@ export const useFormCreateDesigner = async (designer: Ref) => {
name: 'ApiSelect',
label: '接口选择器',
icon: 'icon-server',
props: [...apiSelectRule]
props: [...apiSelectRule],
event: ['click', 'change', 'visibleChange', 'clear', 'blur', 'focus']
})
/**

View File

@ -35,13 +35,13 @@
>
<!-- 右上角热区删除按钮 -->
<div
v-if="selectedHotAreaIndex === index"
v-if="selectedHotAreaIndex === index && hotArea.width && hotArea.height"
class="btn-delete"
@click="handleDeleteHotArea(index)"
>
<Icon icon="ep:circle-close-filled" />
</div>
{{ `${hotArea.width}×${hotArea.height}` }}
<span v-if="hotArea.width">{{ `${hotArea.width}×${hotArea.height}` }}</span>
</div>
</table>
</div>

View File

@ -237,7 +237,7 @@ const props = defineProps({
const prefix = inject('prefix')
const width = inject('width')
const formKey = ref('')
const formKey = ref(undefined)
const businessKey = ref('')
const optionModelTitle = ref('')
const fieldList = ref<any[]>([])
@ -462,6 +462,7 @@ const updateElementExtensions = () => {
const formList = ref([]) //
onMounted(async () => {
formList.value = await FormApi.getFormSimpleList()
formKey.value = parseInt(formKey.value)
})
watch(

View File

@ -370,7 +370,6 @@ const removeListenerField = (index) => {
}
//
const removeListener = (index) => {
debugger
ElMessageBox.confirm('确认移除该监听器吗?', '提示', {
confirmButtonText: '确 认',
cancelButtonText: '取 消'

View File

@ -2,7 +2,6 @@ import { toRaw } from 'vue'
const bpmnInstances = () => (window as any)?.bpmnInstances
// 创建监听器实例
export function createListenerObject(options, isTask, prefix) {
debugger
const listenerObj = Object.create(null)
listenerObj.event = options.event
isTask && (listenerObj.id = options.id) // 任务监听器特有的 id 字段

View File

@ -3,7 +3,14 @@ import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestCo
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import qs from 'qs'
import { config } from '@/config/axios/config'
import { getAccessToken, getRefreshToken, getTenantId, removeToken, setToken } from '@/utils/auth'
import {
getAccessToken,
getRefreshToken,
getTenantId,
getVisitTenantId,
removeToken,
setToken
} from '@/utils/auth'
import errorCode from './errorCode'
import { resetRouter } from '@/router'
@ -24,7 +31,7 @@ export const isRelogin = { show: false }
let requestList: any[] = []
// 是否正在刷新中
let isRefreshToken = false
// 请求白名单,无须token的接口
// 请求白名单,无须 token 的接口
const whiteList: string[] = ['/login', '/refresh-token']
// 创建axios实例
@ -55,6 +62,11 @@ service.interceptors.request.use(
if (tenantEnable && tenantEnable === 'true') {
const tenantId = getTenantId()
if (tenantId) config.headers['tenant-id'] = tenantId
// 只有登录时,才设置 visit-tenant-id 访问租户
const visitTenantId = getVisitTenantId()
if (config.headers.Authorization && visitTenantId) {
config.headers['visit-tenant-id'] = visitTenantId
}
}
const method = config.method?.toUpperCase()
// 防止 GET 请求缓存

View File

@ -10,6 +10,7 @@ export const CACHE_KEY = {
// 用户相关
ROLE_ROUTERS: 'roleRouters',
USER: 'user',
VisitTenantId: 'visitTenantId',
// 系统设置
IS_DARK: 'isDark',
LANG: 'lang',
@ -35,5 +36,6 @@ export const deleteUserCache = () => {
const { wsCache } = useCache()
wsCache.delete(CACHE_KEY.USER)
wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
wsCache.delete(CACHE_KEY.VisitTenantId)
// 注意,不要清理 LoginForm 登录表单
}

View File

@ -1,8 +1,14 @@
import { useAppStore } from '@/store/modules/app'
import { watch } from 'vue'
const domSymbol = Symbol('watermark-dom')
export function useWatermark(appendEl: HTMLElement | null = document.body) {
let func: Fn = () => {}
const id = domSymbol.toString()
const appStore = useAppStore()
let watermarkStr = ''
const clear = () => {
const domId = document.getElementById(id)
if (domId) {
@ -22,7 +28,7 @@ export function useWatermark(appendEl: HTMLElement | null = document.body) {
if (cans) {
cans.rotate((-20 * Math.PI) / 120)
cans.font = '15px Vedana'
cans.fillStyle = 'rgba(0, 0, 0, 0.15)'
cans.fillStyle = appStore.getIsDark ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.15)'
cans.textAlign = 'left'
cans.textBaseline = 'middle'
cans.fillText(str, can.width / 20, can.height)
@ -44,6 +50,7 @@ export function useWatermark(appendEl: HTMLElement | null = document.body) {
}
function setWatermark(str: string) {
watermarkStr = str
createWatermark(str)
func = () => {
createWatermark(str)
@ -51,5 +58,15 @@ export function useWatermark(appendEl: HTMLElement | null = document.body) {
window.addEventListener('resize', func)
}
// 监听主题变化
watch(
() => appStore.getIsDark,
() => {
if (watermarkStr) {
createWatermark(watermarkStr)
}
}
)
return { setWatermark, clear }
}

View File

@ -0,0 +1,46 @@
<template>
<div>
<el-select
filterable
placeholder="请选择租户"
class="!w-180px"
v-model="value"
@change="handleChange"
clearable
>
<el-option v-for="item in tenants" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import * as TenantApi from '@/api/system/tenant'
import { getVisitTenantId, setVisitTenantId } from '@/utils/auth'
import { useMessage } from '@/hooks/web/useMessage'
import { useTagsView } from '@/hooks/web/useTagsView'
const message = useMessage() //
const tagsView = useTagsView() //
const value = ref(getVisitTenantId()) // ID
const tenants = ref<any[]>([]) //
const handleChange = (id: number) => {
// 访 ID
setVisitTenantId(id)
//
tagsView.closeOther()
//
tagsView.refreshPage()
//
const tenant = tenants.value.find((item) => item.id === id)
if (tenant) {
message.success(`切换当前租户为: ${tenant.name}`)
}
}
onMounted(async () => {
tenants.value = await TenantApi.getTenantList()
})
</script>

View File

@ -8,8 +8,10 @@ import { Breadcrumb } from '@/layout/components/Breadcrumb'
import { SizeDropdown } from '@/layout/components/SizeDropdown'
import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
import RouterSearch from '@/components/RouterSearch/index.vue'
import TenantVisit from '@/layout/components/TenantVisit/index.vue'
import { useAppStore } from '@/store/modules/app'
import { useDesign } from '@/hooks/web/useDesign'
import { checkPermi } from '@/utils/permission'
const { getPrefixCls, variables } = useDesign()
@ -41,6 +43,9 @@ const locale = computed(() => appStore.getLocale)
//
const message = computed(() => appStore.getMessage)
//
const hasTenantVisitPermission = computed(() => checkPermi(['system:tenant:visit']))
export default defineComponent({
name: 'ToolHeader',
setup() {
@ -62,6 +67,7 @@ export default defineComponent({
</div>
) : undefined}
<div class="h-full flex items-center">
{hasTenantVisitPermission.value ? <TenantVisit /> : undefined}
{screenfull.value ? (
<Screenfull class="custom-hover" color="var(--top-header-text-color)"></Screenfull>
) : undefined}

View File

@ -142,9 +142,9 @@ export default {
qrcode: '扫描二维码登录',
btnRegister: '注册',
SmsSendMsg: '验证码已发送',
resetPassword: "重置密码",
resetPasswordSuccess: "重置密码成功",
invalidTenantName: "无效的租户名称"
resetPassword: '重置密码',
resetPasswordSuccess: '重置密码成功',
invalidTenantName: '无效的租户名称'
},
captcha: {
verification: '请完成安全验证',
@ -416,9 +416,9 @@ export default {
},
info: {
title: '基本信息',
basicInfo: '基本资料',
resetPwd: '修改密码',
userSocial: '社交信息'
basicInfo: '基本设置',
resetPwd: '密码设置',
userSocial: '社交绑定'
},
rules: {
nickname: '请输入用户昵称',

View File

@ -476,9 +476,9 @@ const remainingRouter: AppRouteRecordRaw[] = [
name: 'DiyTemplateDecorate',
meta: {
title: '模板装修',
noCache: true,
noCache: false,
hidden: true,
activeMenu: '/mall/promotion/diy/template'
activeMenu: '/mall/promotion/diy-template/diy-template'
},
component: () => import('@/views/mall/promotion/diy/template/decorate.vue')
},
@ -487,9 +487,9 @@ const remainingRouter: AppRouteRecordRaw[] = [
name: 'DiyPageDecorate',
meta: {
title: '页面装修',
noCache: true,
noCache: false,
hidden: true,
activeMenu: '/mall/promotion/diy/page'
activeMenu: '/mall/promotion/diy-template/diy-page'
},
component: () => import('@/views/mall/promotion/diy/page/decorate.vue')
}

View File

@ -56,6 +56,11 @@ export const useUserStore = defineStore('admin-user', {
let userInfo = wsCache.get(CACHE_KEY.USER)
if (!userInfo) {
userInfo = await getInfo()
} else {
// 特殊:在有缓存的情况下,进行加载。但是即使加载失败,也不影响后续的操作,保证可以进入系统
try {
userInfo = await getInfo()
} catch (error) {}
}
this.permissions = new Set(userInfo.permissions)
this.roles = userInfo.roles

View File

@ -67,6 +67,14 @@ export const getTenantId = () => {
return wsCache.get(CACHE_KEY.TenantId)
}
export const setTenantId = (username: string) => {
wsCache.set(CACHE_KEY.TenantId, username)
export const setTenantId = (tenantId: number) => {
wsCache.set(CACHE_KEY.TenantId, tenantId)
}
export const getVisitTenantId = () => {
return wsCache.get(CACHE_KEY.VisitTenantId)
}
export const setVisitTenantId = (visitTenantId: number) => {
wsCache.set(CACHE_KEY.VisitTenantId, visitTenantId)
}

View File

@ -71,7 +71,7 @@ export const SystemUserSocialTypeEnum = {
export const InfraCodegenTemplateTypeEnum = {
CRUD: 1, // 基础 CRUD
TREE: 2, // 树形 CRUD
SUB: 3 // 主子表 CRUD
SUB: 15 // 主子表 CRUD
}
/**
@ -461,5 +461,5 @@ export const BpmProcessInstanceStatus = {
export const BpmAutoApproveType = {
NONE: 0, // 不自动通过
APPROVE_ALL: 1, // 仅审批一次,后续重复的审批节点均自动通过
APPROVE_SEQUENT: 2, // 仅针对连续审批的节点自动通过
APPROVE_SEQUENT: 2 // 仅针对连续审批的节点自动通过
}

View File

@ -517,8 +517,8 @@ export function jsonParse(str: string) {
try {
return JSON.parse(str)
} catch (e) {
console.log(`str[${str}] 不是一个 JSON 字符串`)
return ''
console.warn(`str[${str}] 不是一个 JSON 字符串`)
return str
}
}

View File

@ -83,12 +83,16 @@
:sm="24"
:xs="24"
>
<el-card shadow="hover" class="mr-5px mt-5px">
<el-card
shadow="hover"
class="mr-5px mt-5px cursor-pointer"
@click="handleProjectClick(item.message)"
>
<div class="flex items-center">
<Icon :icon="item.icon" :size="25" class="mr-8px" />
<Icon :icon="item.icon" :size="25" class="mr-8px" :style="{ color: item.color }" />
<span class="text-16px">{{ item.name }}</span>
</div>
<div class="mt-12px text-9px text-gray-400">{{ t(item.message) }}</div>
<div class="mt-12px text-12px text-gray-400">{{ t(item.message) }}</div>
<div class="mt-12px flex justify-between text-12px text-gray-400">
<span>{{ item.personal }}</span>
<span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
@ -131,8 +135,8 @@
<el-row>
<el-col v-for="item in shortcut" :key="`team-${item.name}`" :span="8" class="mb-8px">
<div class="flex items-center">
<Icon :icon="item.icon" class="mr-8px" />
<el-link type="default" :underline="false" @click="setWatermark(item.name)">
<Icon :icon="item.icon" class="mr-8px" :style="{ color: item.color }" />
<el-link type="default" :underline="false" @click="handleShortcutClick(item.url)">
{{ item.name }}
</el-link>
</div>
@ -180,10 +184,12 @@ import { useUserStore } from '@/store/modules/user'
import { useWatermark } from '@/hooks/web/useWatermark'
import type { WorkplaceTotal, Project, Notice, Shortcut } from './types'
import { pieOptions, barOptions } from './echarts-data'
import { useRouter } from 'vue-router'
defineOptions({ name: 'Home' })
const { t } = useI18n()
const router = useRouter()
const userStore = useUserStore()
const { setWatermark } = useWatermark()
const loading = ref(true)
@ -212,45 +218,51 @@ const getProject = async () => {
const data = [
{
name: 'ruoyi-vue-pro',
icon: 'akar-icons:github-fill',
message: 'https://github.com/YunaiV/ruoyi-vue-pro',
icon: 'simple-icons:springboot',
message: 'github.com/YunaiV/ruoyi-vue-pro',
personal: 'Spring Boot 单体架构',
time: new Date()
time: new Date('2025-01-02'),
color: '#6DB33F'
},
{
name: 'yudao-ui-admin-vue3',
icon: 'logos:vue',
message: 'https://github.com/yudaocode/yudao-ui-admin-vue3',
personal: 'Vue3 + element-plus',
time: new Date()
},
{
name: 'yudao-ui-admin-vben',
icon: 'logos:vue',
message: 'https://github.com/yudaocode/yudao-ui-admin-vben',
personal: 'Vue3 + vben(antd)',
time: new Date()
},
{
name: 'yudao-cloud',
icon: 'akar-icons:github',
message: 'https://github.com/YunaiV/yudao-cloud',
personal: 'Spring Cloud 微服务架构',
time: new Date()
icon: 'ep:element-plus',
message: 'github.com/yudaocode/yudao-ui-admin-vue3',
personal: 'Vue3 + element-plus 管理后台',
time: new Date('2025-02-03'),
color: '#409EFF'
},
{
name: 'yudao-ui-mall-uniapp',
icon: 'logos:vue',
message: 'https://github.com/yudaocode/yudao-ui-admin-uniapp',
personal: 'Vue3 + uniapp',
time: new Date()
icon: 'icon-park-outline:mall-bag',
message: 'github.com/yudaocode/yudao-ui-mall-uniapp',
personal: 'Vue3 + uniapp 商城手机端',
time: new Date('2025-03-04'),
color: '#ff4d4f'
},
{
name: 'yudao-ui-admin-vue2',
icon: 'logos:vue',
message: 'https://github.com/yudaocode/yudao-ui-admin-vue2',
personal: 'Vue2 + element-ui',
time: new Date()
name: 'yudao-cloud',
icon: 'material-symbols:cloud-outline',
message: 'github.com/YunaiV/yudao-cloud',
personal: 'Spring Cloud 微服务架构',
time: new Date('2025-04-05'),
color: '#1890ff'
},
{
name: 'yudao-ui-admin-vben',
icon: 'devicon:antdesign',
message: 'github.com/yudaocode/yudao-ui-admin-vben',
personal: 'Vue3 + vben5(antd) 管理后台',
time: new Date('2025-05-06'),
color: '#e18525'
},
{
name: 'yudao-ui-admin-uniapp',
icon: 'ant-design:mobile',
message: 'github.com/yudaocode/yudao-ui-admin-uniapp',
personal: 'Vue3 + uniapp 管理手机端',
time: new Date('2025-06-01'),
color: '#2979ff'
}
]
projects = Object.assign(projects, data)
@ -262,26 +274,26 @@ const getNotice = async () => {
const data = [
{
title: '系统支持 JDK 8/17/21Vue 2/3',
type: '通知',
keys: ['通知', '8', '17', '21', '2', '3'],
type: '技术兼容性',
keys: ['JDK', 'Vue'],
date: new Date()
},
{
title: '后端提供 Spring Boot 2.7/3.2 + Cloud 双架构',
type: '公告',
keys: ['公告', 'Boot', 'Cloud'],
type: '架构灵活性',
keys: ['Boot', 'Cloud'],
date: new Date()
},
{
title: '全部开源,个人与企业可 100% 直接使用,无需授权',
type: '通知',
keys: ['通知', '无需授权'],
type: '开源免授权',
keys: ['无需授权'],
date: new Date()
},
{
title: '国内使用最广泛的快速开发平台,超 300+ 人贡献',
type: '公告',
keys: ['公告', '最广泛'],
title: '国内使用最广泛的快速开发平台,远超 10w+ 企业使用',
type: '广泛企业认可',
keys: ['最广泛', '10w+'],
date: new Date()
}
]
@ -294,34 +306,40 @@ let shortcut = reactive<Shortcut[]>([])
const getShortcut = async () => {
const data = [
{
name: 'Github',
icon: 'akar-icons:github-fill',
url: 'github.io'
name: '首页',
icon: 'ion:home-outline',
url: '/',
color: '#1fdaca'
},
{
name: 'Vue',
icon: 'logos:vue',
url: 'vuejs.org'
name: '商城中心',
icon: 'ep:shop',
url: '/mall/home',
color: '#ff6b6b'
},
{
name: 'Vite',
icon: 'vscode-icons:file-type-vite',
url: 'https://vitejs.dev/'
name: 'AI 大模型',
icon: 'tabler:ai',
url: '/ai/chat',
color: '#7c3aed'
},
{
name: 'Angular',
icon: 'logos:angular-icon',
url: 'github.io'
name: 'ERP 系统',
icon: 'simple-icons:erpnext',
url: '/erp/home',
color: '#3fb27f'
},
{
name: 'React',
icon: 'logos:react',
url: 'github.io'
name: 'CRM 系统',
icon: 'simple-icons:civicrm',
url: '/crm/backlog',
color: '#4daf1bc9'
},
{
name: 'Webpack',
icon: 'logos:webpack',
url: 'github.io'
name: 'IoT 物联网',
icon: 'fa-solid:hdd',
url: '/iot/home',
color: '#1a73e8'
}
]
shortcut = Object.assign(shortcut, data)
@ -387,5 +405,13 @@ const getAllApi = async () => {
loading.value = false
}
const handleProjectClick = (message: string) => {
window.open(`https://${message}`, '_blank')
}
const handleShortcutClick = (url: string) => {
router.push(url)
}
getAllApi()
</script>

View File

@ -10,6 +10,7 @@ export type Project = {
message: string
personal: string
time: Date | number | string
color: string
}
export type Notice = {
@ -23,6 +24,7 @@ export type Shortcut = {
name: string
icon: string
url: string
color: string
}
export type RadarData = {

View File

@ -312,8 +312,8 @@ const doSocialLogin = async (type: number) => {
}
}
// redirectUri
// tricky: typeredirectencode
// Login/SocialLogin.vue#getUrlValue() 使
// : typeredirect encode
// social-login.vue#getUrlValue() 使
const redirectUri =
location.origin +
'/social-login?' +

View File

@ -1,4 +1,5 @@
<template>
<!-- TODO @芋艿可优化对标 vben 版本 -->
<div class="flex">
<el-card class="user w-1/3" shadow="hover">
<template #header>
@ -9,11 +10,6 @@
<ProfileUser />
</el-card>
<el-card class="user ml-3 w-2/3" shadow="hover">
<template #header>
<div class="card-header">
<span>{{ t('profile.info.title') }}</span>
</div>
</template>
<div>
<el-tabs v-model="activeName" class="profile-tabs" style="height: 400px" tab-position="top">
<el-tab-pane :label="t('profile.info.basicInfo')" name="basicInfo">

View File

@ -12,11 +12,13 @@
</template>
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import { uploadAvatar } from '@/api/system/user/profile'
import { updateUserProfile } from '@/api/system/user/profile'
import { CropperAvatar } from '@/components/Cropper'
import { useUserStore } from '@/store/modules/user'
import { useUpload } from '@/components/UploadFile/src/useUpload'
import { UploadRequestOptions } from 'element-plus/es/components/upload/src/upload'
// TODO @ ProfileUser
defineOptions({ name: 'UserAvatar' })
defineProps({
@ -25,12 +27,18 @@ defineProps({
const userStore = useUserStore()
const cropperRef = ref()
const handelUpload = async ({ data }) => {
const res = await uploadAvatar({ avatarFile: data })
const { httpRequest } = useUpload()
const avatar = ((await httpRequest({
file: data,
filename: 'avatar.png',
} as UploadRequestOptions)) as unknown as { data: string }).data
await updateUserProfile({ avatar })
// userStore
cropperRef.value.close()
userStore.setUserAvatarAction(res.data)
await userStore.setUserAvatarAction(avatar)
}
</script>

View File

@ -462,6 +462,8 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
(error) => {
message.alert(`对话异常! ${error}`)
stopStream()
//
throw error
},
() => {
stopStream()

View File

@ -80,6 +80,8 @@ const submit = (data: AiMindMapGenerateReqVO) => {
onError(err) {
console.error('生成思维导图失败', err)
stopStream()
//
throw error
},
ctrl: ctrl.value
})

View File

@ -4,7 +4,7 @@
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
label-width="130px"
v-loading="formLoading"
>
<el-form-item label="所属平台" prop="platform">
@ -146,7 +146,10 @@ const formRules = reactive({
platform: [{ required: true, message: '所属平台不能为空', trigger: 'blur' }],
type: [{ required: true, message: '模型类型不能为空', trigger: 'blur' }],
sort: [{ required: true, message: '排序不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
temperature: [{ required: true, message: '温度参数不能为空', trigger: 'blur' }],
maxTokens: [{ required: true, message: '回复数 Token 数不能为空', trigger: 'blur' }],
maxContexts: [{ required: true, message: '上下文数量不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
const apiKeyList = ref([] as ApiKeyVO[]) // API

View File

@ -13,11 +13,58 @@
测试
</el-button>
</div>
<!-- 测试窗口 -->
<el-drawer v-model="showTestDrawer" title="工作流测试" :modal="false">
<fieldset>
<legend class="ml-15px"><h3>运行参数配置</h3></legend>
<div class="p-20px">
<div
class="flex justify-around mb-10px"
v-for="(param, index) in params4Test"
:key="index"
>
<el-select class="w-200px!" v-model="param.key" placeholder="参数名">
<el-option
v-for="(value, key) in paramsOfStartNode"
:key="key"
:label="value?.description || key"
:value="key"
:disabled="!!value?.disabled"
/>
</el-select>
<el-input class="w-200px!" v-model="param.value" placeholder="参数值" />
<el-button type="danger" plain :icon="Delete" circle @click="removeParam(index)" />
</div>
<!-- TODO @lesan是不是不用添加和删除参数直接把必填和选填列出来然后加上参数校验 -->
<el-button type="primary" plain @click="addParam">添加参数</el-button>
</div>
</fieldset>
<fieldset class="mt-20px bg-#f8f9fa">
<legend class="ml-15px"><h3>运行结果</h3></legend>
<div class="p-20px">
<div v-if="loading"> <el-text type="primary">执行中...</el-text></div>
<div v-else-if="error">
<el-text type="danger">{{ error }}</el-text>
</div>
<pre v-else-if="testResult" class="result-content"
>{{ JSON.stringify(testResult, null, 2) }}
</pre>
<div v-else> <el-text type="info">点击运行查看结果</el-text> </div>
</div>
</fieldset>
<el-button class="mt-20px w-100%" size="large" type="success" @click="goRun">
运行流程
</el-button>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import Tinyflow from '@/components/Tinyflow/Tinyflow.vue'
import * as WorkflowApi from '@/api/ai/workflow'
// TODO @lesan使 ICon
import { Delete } from '@element-plus/icons-vue'
defineProps<{
provider: any
@ -25,9 +72,149 @@ defineProps<{
const tinyflowRef = ref()
const workflowData = inject('workflowData') as Ref
const showTestDrawer = ref(false)
const params4Test = ref([])
const paramsOfStartNode = ref({})
const testResult = ref(null)
const loading = ref(false)
const error = ref(null)
/** 展示工作流测试抽屉 */
const testWorkflowModel = () => {
// TODO @lesan
showTestDrawer.value = !showTestDrawer.value
}
/** 运行流程 */
const goRun = async () => {
try {
const val = tinyflowRef.value.getData()
loading.value = true
error.value = null
testResult.value = null
/// start
const startNode = getStartNode()
//
const parameters = startNode.data?.parameters || []
const paramDefinitions = {}
parameters.forEach((param) => {
paramDefinitions[param.name] = param.dataType
})
//
const convertedParams = {}
for (const { key, value } of params4Test.value) {
const paramKey = key.trim()
if (!paramKey) continue
let dataType = paramDefinitions[paramKey]
if (!dataType) {
dataType = 'String'
}
try {
convertedParams[paramKey] = convertParamValue(value, dataType)
} catch (e) {
throw new Error(`参数 ${paramKey} 转换失败: ${e.message}`)
}
}
const data = {
graph: JSON.stringify(val),
params: convertedParams
}
const response = await WorkflowApi.testWorkflow(data)
testResult.value = response
} catch (err) {
error.value = err.response?.data?.message || '运行失败,请检查参数和网络连接'
} finally {
loading.value = false
}
}
/** 监听测试抽屉的开启,获取开始节点参数列表 */
watch(showTestDrawer, (value) => {
if (!value) return
/// start
const startNode = getStartNode()
//
const parameters = startNode.data?.parameters || []
const paramDefinitions = {}
// 便
parameters.forEach((param) => {
paramDefinitions[param.name] = param
})
function mergeIfRequiredButNotSet(target) {
let needPushList = []
for (let key in paramDefinitions) {
let param = paramDefinitions[key]
if (param.required) {
let item = target.find((item) => item.key === key)
if (!item) {
needPushList.push({ key: param.name, value: param.defaultValue || '' })
}
}
}
target.push(...needPushList)
}
//
mergeIfRequiredButNotSet(params4Test.value)
paramsOfStartNode.value = paramDefinitions
})
/** 获取开始节点 */
const getStartNode = () => {
const val = tinyflowRef.value.getData()
const startNode = val.nodes.find((node) => node.type === 'startNode')
if (!startNode) {
throw new Error('流程缺少开始节点')
}
return startNode
}
/** 添加参数项 */
const addParam = () => {
params4Test.value.push({ key: '', value: '' })
}
/** 删除参数项 */
const removeParam = (index) => {
params4Test.value.splice(index, 1)
}
/** 类型转换函数 */
const convertParamValue = (value, dataType) => {
if (value === '') return null //
switch (dataType) {
case 'String':
return String(value)
case 'Number':
const num = Number(value)
if (isNaN(num)) throw new Error('非数字格式')
return num
case 'Boolean':
if (value.toLowerCase() === 'true') return true
if (value.toLowerCase() === 'false') return false
throw new Error('必须为 true/false')
case 'Object':
case 'Array':
try {
return JSON.parse(value)
} catch (e) {
throw new Error(`JSON格式错误: ${e.message}`)
}
default:
throw new Error(`不支持的类型: ${dataType}`)
}
}
/** 表单校验 */
@ -47,3 +234,17 @@ defineExpose({
validate
})
</script>
<style lang="css" scoped>
.result-content {
background: white;
padding: 12px;
border-radius: 4px;
max-height: 300px;
overflow: auto;
font-family: Monaco, Consolas, monospace;
font-size: 14px;
line-height: 1.5;
white-space: pre-wrap;
}
</style>

View File

@ -59,7 +59,7 @@
<WorkflowDesign
v-if="currentStep === 1"
v-model="formData"
:provider="provider"
:provider="llmProvider"
ref="workflowDesignRef"
/>
</div>
@ -73,7 +73,8 @@ import { CommonStatusEnum } from '@/utils/constants'
import * as WorkflowApi from '@/api/ai/workflow'
import BasicInfo from './BasicInfo.vue'
import WorkflowDesign from './WorkflowDesign.vue'
import { ApiKeyApi } from '@/api/ai/model/apiKey'
import { ModelApi } from '@/api/ai/model/model'
import { AiModelTypeEnum } from '@/views/ai/utils/constants'
const router = useRouter()
const { delView } = useTagsViewStore()
@ -104,31 +105,35 @@ const formData: any = ref({
graph: '',
status: CommonStatusEnum.ENABLE
})
// TODO @lesan
const provider = ref<any>()
const llmProvider = ref<any>([])
const workflowData = ref<any>({})
provide('workflowData', workflowData)
/** 初始化数据 */
const actionType = route.params.type as string
const initData = async () => {
//
if (actionType === 'update') {
const workflowId = route.params.id as string
formData.value = await WorkflowApi.getWorkflow(workflowId)
workflowData.value = JSON.parse(formData.value.graph)
}
const apiKeys = await ApiKeyApi.getApiKeySimpleList()
provider.value = {
//
const models = await ModelApi.getModelSimpleList(AiModelTypeEnum.CHAT)
llmProvider.value = {
llm: () =>
apiKeys.map(({ id, name }) => ({
models.map(({ id, name }) => ({
value: id,
label: name
})),
knowledge: () => [],
internal: () => []
}
// TODO @lesan knowledge
// TODO @lesan pr
//
currentStep.value = 0
}
@ -164,17 +169,17 @@ const handleSave = async () => {
//
const data = {
...formData.value
...formData.value,
graph: JSON.stringify(workflowData.value)
}
data.graph = JSON.stringify(workflowData.value)
if (actionType === 'update') {
await WorkflowApi.updateWorkflow(data)
} else {
await WorkflowApi.createWorkflow(data)
}
//
message.success('保存成功')
delView(unref(router.currentRoute))
await router.push({ name: 'AiWorkflow' })
} catch (error: any) {

View File

@ -57,9 +57,11 @@ const submit = (data: WriteVO) => {
},
ctrl: abortController.value,
onClose: stopStream,
onError: (...err) => {
console.error('写作异常', ...err)
onError: (error) => {
console.error('写作异常', error)
stopStream()
//
throw error
}
})
}

View File

@ -449,7 +449,6 @@ const handleChangeState = async (row: any) => {
try {
//
const id = row.id
debugger
const statusState = state === 1 ? '停用' : '启用'
const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?'
await message.confirm(content)

View File

@ -62,7 +62,11 @@
</el-row>
<!-- 操作 -->
<template #footer>
<el-button :disabled="tableList.length === 0" type="primary" @click="handleImportTable">
<el-button
:disabled="tableList.length === 0 || dbTableLoading"
type="primary"
@click="handleImportTable"
>
导入
</el-button>
<el-button @click="close">关闭</el-button>
@ -139,13 +143,18 @@ const handleSelectionChange = (selection) => {
/** 导入按钮操作 */
const handleImportTable = async () => {
await CodegenApi.createCodegenList({
dataSourceConfigId: queryParams.dataSourceConfigId,
tableNames: tableList.value
})
message.success('导入成功')
emit('success')
close()
dbTableLoading.value = true
try {
await CodegenApi.createCodegenList({
dataSourceConfigId: queryParams.dataSourceConfigId,
tableNames: tableList.value
})
message.success('导入成功')
emit('success')
close()
} finally {
dbTableLoading.value = false
}
}
const emit = defineEmits(['success'])
</script>

View File

@ -5,7 +5,7 @@
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="120px"
label-width="130px"
>
<el-form-item label="配置名" prop="name">
<el-input v-model="formData.name" placeholder="请输入配置名" />
@ -83,6 +83,16 @@
<el-form-item v-if="formData.storage === 20" label="accessSecret" prop="config.accessSecret">
<el-input v-model="formData.config.accessSecret" placeholder="请输入 accessSecret" />
</el-form-item>
<el-form-item
v-if="formData.storage === 20"
label="是否 Path Style"
prop="config.enablePathStyleAccess"
>
<el-radio-group v-model="formData.config.enablePathStyleAccess">
<el-radio key="true" :value="true">启用</el-radio>
<el-radio key="false" :value="false">禁用</el-radio>
</el-radio-group>
</el-form-item>
<!-- 通用 -->
<el-form-item v-if="formData.storage === 20" label="自定义域名">
<!-- 无需参数校验所以去掉 prop -->
@ -133,6 +143,9 @@ const formRules = reactive<FormRules>({
bucket: [{ required: true, message: '存储 bucket 不能为空', trigger: 'blur' }],
accessKey: [{ required: true, message: 'accessKey 不能为空', trigger: 'blur' }],
accessSecret: [{ required: true, message: 'accessSecret 不能为空', trigger: 'blur' }],
enablePathStyleAccess: [
{ required: true, message: '是否 PathStyle 访问不能为空', trigger: 'change' }
],
domain: [{ required: true, message: '自定义域名不能为空', trigger: 'blur' }]
} as FormRules
})

View File

@ -132,7 +132,6 @@ defineExpose({ open, close: () => (dialogVisible.value = false) })
/** 提交表单 */
const emit = defineEmits(['success'])
const submitForm = async () => {
debugger
await formRef.value.validate()
formLoading.value = true
try {

View File

@ -62,20 +62,13 @@
/>
</template>
</el-table-column>
<el-table-column label="模板名称" align="center" prop="name" />
<el-table-column label="模板名称" align="center" prop="name" min-width="180" />
<el-table-column label="是否使用" align="center" prop="used">
<template #default="scope">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.used" />
</template>
</el-table-column>
<el-table-column
label="使用时间"
align="center"
prop="usedTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="备注" align="center" prop="remark" min-width="180" />
<el-table-column
label="创建时间"
align="center"

View File

@ -16,7 +16,6 @@ import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
import { getRefreshToken } from '@/utils/auth'
import { useWebSocket } from '@vueuse/core'
import { useMallKefuStore } from '@/store/modules/mall/kefu'
import { jsonParse } from '@/utils'
defineOptions({ name: 'KeFu' })
@ -66,7 +65,8 @@ watch(
// 2.3 KEFU_MESSAGE_ADMIN_READ
if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_ADMIN_READ) {
//
kefuStore.updateConversationStatus(jsonParse(jsonMessage.content))
const message = JSON.parse(jsonMessage.content)
kefuStore.updateConversationStatus(message.conversationId)
}
} catch (error) {
console.error(error)

View File

@ -120,7 +120,7 @@
v-if="scope.row.deliveryType === DeliveryTypeEnum.EXPRESS.type"
class="flex flex-col"
>
<span>买家{{ scope.row.user.nickname }}</span>
<span>买家{{ scope.row.user?.nickname }}</span>
<span>
收货人{{ scope.row.receiverName }} {{ scope.row.receiverMobile }}
{{ scope.row.receiverAreaName }} {{ scope.row.receiverDetailAddress }}

View File

@ -1,8 +1,8 @@
import type { UploadProps, UploadRawFile } from 'element-plus'
import { getAccessToken } from '@/utils/auth'
import { getRefreshToken } from '@/utils/auth'
import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 请求头
const HEADERS = { Authorization: 'Bearer ' + getRefreshToken() } // 请求头(解决 el-upload 上传过程中,无法刷新令牌的问题)
const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-permanent' // 上传地址
interface UploadData {

View File

@ -6,7 +6,11 @@
</ContentWrap>
</template>
<script lang="ts" setup>
import { getAccessToken, getRefreshToken } from '@/utils/auth'
defineOptions({ name: 'GoView' })
const src = ref(import.meta.env.VITE_GOVIEW_URL)
const src = ref(
`${import.meta.env.VITE_GOVIEW_URL}?accessToken=${getAccessToken()}&refreshToken=${getRefreshToken()}`
)
</script>

View File

@ -0,0 +1,15 @@
<template>
<doc-alert title="大屏设计器" url="https://doc.iocoder.cn/screen/" />
<ContentWrap :bodyStyle="{ padding: '0px' }" class="!mb-0">
<IFrame :src="src" />
</ContentWrap>
</template>
<script lang="ts" setup>
import { getRefreshToken } from '@/utils/auth'
defineOptions({ name: 'JimuBI' })
// 使 getRefreshToken() 使 getAccessToken() 便访
const src = ref(import.meta.env.VITE_BASE_URL + '/drag/list?token=' + getRefreshToken())
</script>

View File

@ -147,6 +147,7 @@ const formData = ref({
})
const formRules = reactive({
name: [{ required: true, message: '菜单名称不能为空', trigger: 'blur' }],
type: [{ required: true, message: '菜单类型不能为空', trigger: 'blur' }],
sort: [{ required: true, message: '菜单顺序不能为空', trigger: 'blur' }],
path: [{ required: true, message: '路由地址不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]

View File

@ -20,7 +20,7 @@
</el-form>
<el-form-item
v-if="formData.dataScope === SystemDataScopeEnum.DEPT_CUSTOM"
label="权限范围"
label="部门范围"
label-width="80px"
>
<el-card class="w-full h-400px !overflow-y-scroll" shadow="never">

View File

@ -41,7 +41,7 @@
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="用户状态"
placeholder="请选择用户状态"
clearable
class="!w-240px"
>
@ -345,7 +345,7 @@ const handleResetPwd = async (row: UserApi.UserVO) => {
)
const password = result.value
//
await UserApi.resetUserPwd(row.id, password)
await UserApi.resetUserPassword(row.id, password)
message.success('修改成功,新密码是:' + password)
} catch {}
}

View File

@ -37,7 +37,6 @@ ${selector}:before {
position: absolute;
top: 0;
left: 0;
width: 1px;
height: 100%;
background-color: var(--el-border-color);
z-index: 3;