Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/iot

# Conflicts:
#	src/api/system/dept/index.ts
pull/803/MERGE
YunaiV 2025-08-02 10:54:08 +08:00
commit a269c5988a
131 changed files with 2709 additions and 2434 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "yudao-ui-admin-vue3", "name": "yudao-ui-admin-vue3",
"version": "2.6.0-snapshot", "version": "2025.08-snapshot",
"description": "基于vue3、vite4、element-plus、typesScript", "description": "基于vue3、vite4、element-plus、typesScript",
"author": "xingyu", "author": "xingyu",
"private": false, "private": false,

View File

@ -48,6 +48,7 @@ export type ApprovalNodeInfo = {
status: number status: number
startTime?: Date startTime?: Date
endTime?: Date endTime?: Date
processInstanceId?: string
candidateUsers?: User[] candidateUsers?: User[]
tasks: ApprovalTaskInfo[] tasks: ApprovalTaskInfo[]
} }

View File

@ -4,6 +4,10 @@ import request from '@/config/axios'
* *
*/ */
export enum TaskStatusEnum { export enum TaskStatusEnum {
/**
*
*/
SKIP = -2,
/** /**
* *
*/ */

View File

@ -105,3 +105,8 @@ export const createCodegenList = (data) => {
export const deleteCodegenTable = (id: number) => { export const deleteCodegenTable = (id: number) => {
return request.delete({ url: '/infra/codegen/delete?tableId=' + id }) return request.delete({ url: '/infra/codegen/delete?tableId=' + id })
} }
// 批量删除代码生成表定义
export const deleteCodegenTableList = (ids: number[]) => {
return request.delete({ url: '/infra/codegen/delete-list', params: { tableIds: ids.join(',') } })
}

View File

@ -42,7 +42,12 @@ export const deleteConfig = (id: number) => {
return request.delete({ url: '/infra/config/delete?id=' + id }) return request.delete({ url: '/infra/config/delete?id=' + id })
} }
// 批量删除参数
export const deleteConfigList = (ids: number[]) => {
return request.delete({ url: '/infra/config/delete-list', params: { ids: ids.join(',') } })
}
// 导出参数 // 导出参数
export const exportConfig = (params) => { export const exportConfig = (params) => {
return request.download({ url: '/infra/config/export', params }) return request.download({ url: '/infra/config/export-excel', params })
} }

View File

@ -24,6 +24,11 @@ export const deleteDataSourceConfig = (id: number) => {
return request.delete({ url: '/infra/data-source-config/delete?id=' + id }) return request.delete({ url: '/infra/data-source-config/delete?id=' + id })
} }
// 批量删除数据源配置
export const deleteDataSourceConfigList = (ids: number[]) => {
return request.delete({ url: '/infra/data-source-config/delete-list', params: { ids: ids.join(',') } })
}
// 查询数据源配置详情 // 查询数据源配置详情
export const getDataSourceConfig = (id: number) => { export const getDataSourceConfig = (id: number) => {
return request.get({ url: '/infra/data-source-config/get?id=' + id }) return request.get({ url: '/infra/data-source-config/get?id=' + id })

View File

@ -22,6 +22,11 @@ export const deleteFile = (id: number) => {
return request.delete({ url: '/infra/file/delete?id=' + id }) return request.delete({ url: '/infra/file/delete?id=' + id })
} }
// 批量删除文件
export const deleteFileList = (ids: number[]) => {
return request.delete({ url: '/infra/file/delete-list', params: { ids: ids.join(',') } })
}
// 获取文件预签名地址 // 获取文件预签名地址
export const getFilePresignedUrl = (name: string, directory?: string) => { export const getFilePresignedUrl = (name: string, directory?: string) => {
return request.get<FilePresignedUrlRespVO>({ return request.get<FilePresignedUrlRespVO>({

View File

@ -56,6 +56,11 @@ export const deleteFileConfig = (id: number) => {
return request.delete({ url: '/infra/file-config/delete?id=' + id }) return request.delete({ url: '/infra/file-config/delete?id=' + id })
} }
// 批量删除文件配置
export const deleteFileConfigList = (ids: number[]) => {
return request.delete({ url: '/infra/file-config/delete-list', params: { ids: ids.join(',') } })
}
// 测试文件配置 // 测试文件配置
export const testFileConfig = (id: number) => { export const testFileConfig = (id: number) => {
return request.get({ url: '/infra/file-config/test?id=' + id }) return request.get({ url: '/infra/file-config/test?id=' + id })

View File

@ -38,6 +38,11 @@ export const deleteJob = (id: number) => {
return request.delete({ url: '/infra/job/delete?id=' + id }) return request.delete({ url: '/infra/job/delete?id=' + id })
} }
// 批量删除定时任务调度
export const deleteJobList = (ids: number[]) => {
return request.delete({ url: '/infra/job/delete-list', params: { ids: ids.join(',') } })
}
// 导出定时任务调度 // 导出定时任务调度
export const exportJob = (params) => { export const exportJob = (params) => {
return request.download({ url: '/infra/job/export-excel', params }) return request.download({ url: '/infra/job/export-excel', params })

View File

@ -102,7 +102,7 @@ export const deleteSpu = (id: number) => {
// 导出商品 Spu Excel // 导出商品 Spu Excel
export const exportSpu = async (params: any) => { export const exportSpu = async (params: any) => {
return await request.download({ url: '/product/spu/export', params }) return await request.download({ url: '/product/spu/export-excel', params })
} }
// 获得商品 SPU 精简列表 // 获得商品 SPU 精简列表

View File

@ -1,7 +1,7 @@
import request from '@/config/axios' import request from '@/config/axios'
export interface DeptVO { export interface DeptVO {
id?: number id: number
name: string name: string
parentId: number parentId: number
status: number status: number
@ -13,31 +13,41 @@ export interface DeptVO {
} }
// 查询部门(精简)列表 // 查询部门(精简)列表
export const getSimpleDeptList = async (): Promise<DeptVO[]> => { export const getSimpleDeptList = (): Promise<DeptVO[]> => {
return await request.get({ url: '/system/dept/simple-list' }) return request.get({ url: '/system/dept/simple-list' })
} }
// 查询部门列表 // 查询部门列表
export const getDeptList = async () => { export const getDeptList = (params: any) => {
return await request.get({ url: '/system/dept/list' }) return request.get({ url: '/system/dept/list', params })
}
// 查询部门分页
export const getDeptPage = async (params: PageParam) => {
return await request.get({ url: '/system/dept/list', params })
} }
// 查询部门详情 // 查询部门详情
export const getDept = async (id: number) => { export const getDept = (id: number) => {
return await request.get({ url: '/system/dept/get?id=' + id }) return request.get({ url: '/system/dept/get?id=' + id })
} }
// 新增部门 // 新增部门
export const createDept = async (data: DeptVO) => { export const createDept = (data: DeptVO) => {
return await request.post({ url: '/system/dept/create', data: data }) return request.post({ url: '/system/dept/create', data })
} }
// 修改部门 // 修改部门
export const updateDept = async (params: DeptVO) => { export const updateDept = (data: DeptVO) => {
return await request.put({ url: '/system/dept/update', data: params }) return request.put({ url: '/system/dept/update', data })
} }
// 删除部门 // 删除部门
export const deleteDept = async (id: number) => { export const deleteDept = async (id: number) => {
return await request.delete({ url: '/system/dept/delete?id=' + id }) return await request.delete({ url: '/system/dept/delete?id=' + id })
} }
// 批量删除部门
export const deleteDeptList = async (ids: number[]) => {
return await request.delete({ url: '/system/dept/delete-list', params: { ids: ids.join(',') } })
}

View File

@ -1,8 +1,8 @@
import request from '@/config/axios' import request from '@/config/axios'
export type DictDataVO = { export interface DictDataVO {
id: number | undefined id: number
sort: number | undefined sort: number
label: string label: string
value: string value: string
dictType: string dictType: string
@ -28,6 +28,11 @@ export const getDictData = (id: number) => {
return request.get({ url: '/system/dict-data/get?id=' + id }) return request.get({ url: '/system/dict-data/get?id=' + id })
} }
// 根据字典类型查询字典数据
export const getDictDataByType = (dictType: string) => {
return request.get({ url: '/system/dict-data/type?type=' + dictType })
}
// 新增字典数据 // 新增字典数据
export const createDictData = (data: DictDataVO) => { export const createDictData = (data: DictDataVO) => {
return request.post({ url: '/system/dict-data/create', data }) return request.post({ url: '/system/dict-data/create', data })
@ -43,7 +48,12 @@ export const deleteDictData = (id: number) => {
return request.delete({ url: '/system/dict-data/delete?id=' + id }) return request.delete({ url: '/system/dict-data/delete?id=' + id })
} }
// 导出字典类型数据 // 批量删除字典数据
export const exportDictData = (params) => { export const deleteDictDataList = (ids: number[]) => {
return request.download({ url: '/system/dict-data/export', params }) return request.delete({ url: '/system/dict-data/delete-list', params: { ids: ids.join(',') } })
}
// 导出字典数据
export const exportDictData = (params: any) => {
return request.download({ url: '/system/dict-data/export-excel', params })
} }

View File

@ -1,7 +1,7 @@
import request from '@/config/axios' import request from '@/config/axios'
export type DictTypeVO = { export interface DictTypeVO {
id: number | undefined id: number
name: string name: string
type: string type: string
status: number status: number
@ -10,8 +10,8 @@ export type DictTypeVO = {
} }
// 查询字典(精简)列表 // 查询字典(精简)列表
export const getSimpleDictTypeList = () => { export const getSimpleDictTypeList = (): Promise<DictTypeVO[]> => {
return request.get({ url: '/system/dict-type/list-all-simple' }) return request.get({ url: '/system/dict-type/simple-list' })
} }
// 查询字典列表 // 查询字典列表
@ -38,7 +38,16 @@ export const updateDictType = (data: DictTypeVO) => {
export const deleteDictType = (id: number) => { export const deleteDictType = (id: number) => {
return request.delete({ url: '/system/dict-type/delete?id=' + id }) return request.delete({ url: '/system/dict-type/delete?id=' + id })
} }
// 导出字典类型
export const exportDictType = (params) => { // 批量删除字典类型
return request.download({ url: '/system/dict-type/export', params }) export const deleteDictTypeList = (ids: number[]) => {
return request.delete({ url: '/system/dict-type/delete-list', params: { ids: ids.join(',') } })
}
// 导出字典
export const exportDictType = (params) => {
return request.download({
url: '/system/dict-type/export-excel',
params
})
} }

View File

@ -21,5 +21,5 @@ export const getLoginLogPage = (params: PageParam) => {
// 导出登录日志 // 导出登录日志
export const exportLoginLog = (params) => { export const exportLoginLog = (params) => {
return request.download({ url: '/system/login-log/export', params }) return request.download({ url: '/system/login-log/export-excel', params })
} }

View File

@ -36,6 +36,11 @@ export const deleteMailAccount = async (id: number) => {
return await request.delete({ url: '/system/mail-account/delete?id=' + id }) return await request.delete({ url: '/system/mail-account/delete?id=' + id })
} }
// 批量删除邮箱账号
export const deleteMailAccountList = async (ids: number[]) => {
return await request.delete({ url: '/system/mail-account/delete-list', params: { ids: ids.join(',') } })
}
// 获得邮箱账号精简列表 // 获得邮箱账号精简列表
export const getSimpleMailAccountList = async () => { export const getSimpleMailAccountList = async () => {
return request.get({ url: '/system/mail-account/simple-list' }) return request.get({ url: '/system/mail-account/simple-list' })

View File

@ -28,3 +28,8 @@ export const getMailLogPage = async (params: PageParam) => {
export const getMailLog = async (id: number) => { export const getMailLog = async (id: number) => {
return await request.get({ url: '/system/mail-log/get?id=' + id }) return await request.get({ url: '/system/mail-log/get?id=' + id })
} }
// 导出邮件日志
export const exportMailLog = (params) => {
return request.download({ url: '/system/mail-log/export-excel', params })
}

View File

@ -44,6 +44,11 @@ export const deleteMailTemplate = async (id: number) => {
return await request.delete({ url: '/system/mail-template/delete?id=' + id }) return await request.delete({ url: '/system/mail-template/delete?id=' + id })
} }
// 批量删除邮件模版
export const deleteMailTemplateList = async (ids: number[]) => {
return await request.delete({ url: '/system/mail-template/delete-list', params: { ids: ids.join(',') } })
}
// 发送邮件 // 发送邮件
export const sendMail = (data: MailSendReqVO) => { export const sendMail = (data: MailSendReqVO) => {
return request.post({ url: '/system/mail-template/send-mail', data }) return request.post({ url: '/system/mail-template/send-mail', data })

View File

@ -36,6 +36,11 @@ export const deleteNotice = (id: number) => {
return request.delete({ url: '/system/notice/delete?id=' + id }) return request.delete({ url: '/system/notice/delete?id=' + id })
} }
// 批量删除公告
export const deleteNoticeList = (ids: number[]) => {
return request.delete({ url: '/system/notice/delete-list', params: { ids: ids.join(',') } })
}
// 推送公告 // 推送公告
export const pushNotice = (id: number) => { export const pushNotice = (id: number) => {
return request.post({ url: '/system/notice/push?id=' + id }) return request.post({ url: '/system/notice/push?id=' + id })

View File

@ -43,6 +43,11 @@ export const deleteNotifyTemplate = async (id: number) => {
return await request.delete({ url: '/system/notify-template/delete?id=' + id }) return await request.delete({ url: '/system/notify-template/delete?id=' + id })
} }
// 批量删除站内信模板
export const deleteNotifyTemplateList = async (ids: number[]) => {
return await request.delete({ url: '/system/notify-template/delete-list', params: { ids: ids.join(',') } })
}
// 发送站内信 // 发送站内信
export const sendNotify = (data: NotifySendReqVO) => { export const sendNotify = (data: NotifySendReqVO) => {
return request.post({ url: '/system/notify-template/send-notify', data }) return request.post({ url: '/system/notify-template/send-notify', data })

View File

@ -45,3 +45,8 @@ export const updateOAuth2Client = (data: OAuth2ClientVO) => {
export const deleteOAuth2Client = (id: number) => { export const deleteOAuth2Client = (id: number) => {
return request.delete({ url: '/system/oauth2-client/delete?id=' + id }) return request.delete({ url: '/system/oauth2-client/delete?id=' + id })
} }
// 批量删除 OAuth2 客户端
export const deleteOAuth2ClientList = (ids: number[]) => {
return request.delete({ url: '/system/oauth2-client/delete-list', params: { ids: ids.join(',') } })
}

View File

@ -26,5 +26,5 @@ export const getOperateLogPage = (params: PageParam) => {
} }
// 导出操作日志 // 导出操作日志
export const exportOperateLog = (params: any) => { export const exportOperateLog = (params: any) => {
return request.download({ url: '/system/operate-log/export', params }) return request.download({ url: '/system/operate-log/export-excel', params })
} }

View File

@ -40,7 +40,12 @@ export const deletePost = async (id: number) => {
return await request.delete({ url: '/system/post/delete?id=' + id }) return await request.delete({ url: '/system/post/delete?id=' + id })
} }
// 批量删除岗位
export const deletePostList = async (ids: number[]) => {
return await request.delete({ url: '/system/post/delete-list', params: { ids: ids.join(',') } })
}
// 导出岗位 // 导出岗位
export const exportPost = async (params) => { export const exportPost = async (params) => {
return await request.download({ url: '/system/post/export', params }) return await request.download({ url: '/system/post/export-excel', params })
} }

View File

@ -42,6 +42,11 @@ export const deleteRole = async (id: number) => {
return await request.delete({ url: '/system/role/delete?id=' + id }) return await request.delete({ url: '/system/role/delete?id=' + id })
} }
// 批量删除角色
export const deleteRoleList = async (ids: number[]) => {
return await request.delete({ url: '/system/role/delete-list', params: { ids: ids.join(',') } })
}
// 导出角色 // 导出角色
export const exportRole = (params: any) => { export const exportRole = (params: any) => {
return request.download({ return request.download({

View File

@ -41,3 +41,8 @@ export const updateSmsChannel = (data: SmsChannelVO) => {
export const deleteSmsChannel = (id: number) => { export const deleteSmsChannel = (id: number) => {
return request.delete({ url: '/system/sms-channel/delete?id=' + id }) return request.delete({ url: '/system/sms-channel/delete?id=' + id })
} }
// 批量删除短信渠道
export const deleteSmsChannelList = (ids: number[]) => {
return request.delete({ url: '/system/sms-channel/delete-list', params: { ids: ids.join(',') } })
}

View File

@ -46,6 +46,11 @@ export const deleteSmsTemplate = (id: number) => {
return request.delete({ url: '/system/sms-template/delete?id=' + id }) return request.delete({ url: '/system/sms-template/delete?id=' + id })
} }
// 批量删除短信模板
export const deleteSmsTemplateList = (ids: number[]) => {
return request.delete({ url: '/system/sms-template/delete-list', params: { ids: ids.join(',') } })
}
// 导出短信模板 // 导出短信模板
export const exportSmsTemplate = (params) => { export const exportSmsTemplate = (params) => {
return request.download({ return request.download({

View File

@ -14,7 +14,7 @@ export interface SocialUserVO {
} }
// 查询社交用户列表 // 查询社交用户列表
export const getSocialUserPage = async (params) => { export const getSocialUserPage = async (params: any) => {
return await request.get({ url: `/system/social-user/page`, params }) return await request.get({ url: `/system/social-user/page`, params })
} }
@ -22,3 +22,8 @@ export const getSocialUserPage = async (params) => {
export const getSocialUser = async (id: number) => { export const getSocialUser = async (id: number) => {
return await request.get({ url: `/system/social-user/get?id=` + id }) return await request.get({ url: `/system/social-user/get?id=` + id })
} }
// 获得绑定社交用户列表
export const getBindSocialUserList = async () => {
return await request.get({ url: '/system/social-user/get-bind-list' })
}

View File

@ -61,6 +61,11 @@ export const deleteTenant = (id: number) => {
return request.delete({ url: '/system/tenant/delete?id=' + id }) return request.delete({ url: '/system/tenant/delete?id=' + id })
} }
// 批量删除租户
export const deleteTenantList = (ids: number[]) => {
return request.delete({ url: '/system/tenant/delete-list', params: { ids: ids.join(',') } })
}
// 导出租户 // 导出租户
export const exportTenant = (params: TenantExportReqVO) => { export const exportTenant = (params: TenantExportReqVO) => {
return request.download({ url: '/system/tenant/export-excel', params }) return request.download({ url: '/system/tenant/export-excel', params })

View File

@ -36,6 +36,12 @@ export const updateTenantPackage = (data: TenantPackageVO) => {
export const deleteTenantPackage = (id: number) => { export const deleteTenantPackage = (id: number) => {
return request.delete({ url: '/system/tenant-package/delete?id=' + id }) return request.delete({ url: '/system/tenant-package/delete?id=' + id })
} }
// 批量删除租户套餐
export const deleteTenantPackageList = (ids: number[]) => {
return request.delete({ url: '/system/tenant-package/delete-list', params: { ids: ids.join(',') } })
}
// 获取租户套餐精简信息列表 // 获取租户套餐精简信息列表
export const getTenantPackageList = () => { export const getTenantPackageList = () => {
return request.get({ url: '/system/tenant-package/simple-list' }) return request.get({ url: '/system/tenant-package/simple-list' })

View File

@ -42,9 +42,14 @@ export const deleteUser = (id: number) => {
return request.delete({ url: '/system/user/delete?id=' + id }) return request.delete({ url: '/system/user/delete?id=' + id })
} }
// 批量删除用户
export const deleteUserList = (ids: number[]) => {
return request.delete({ url: '/system/user/delete-list', params: { ids: ids.join(',') } })
}
// 导出用户 // 导出用户
export const exportUser = (params: any) => { export const exportUser = (params: any) => {
return request.download({ url: '/system/user/export', params }) return request.download({ url: '/system/user/export-excel', params })
} }
// 下载用户导入模板 // 下载用户导入模板

View File

@ -16,10 +16,6 @@ export interface ProfileVO {
id: number id: number
name: string name: string
}[] }[]
socialUsers: {
type: number
openid: string
}[]
email: string email: string
mobile: string mobile: string
sex: number sex: number

View File

@ -80,7 +80,8 @@ const activeAppLink = ref({} as AppLink)
/** 打开弹窗 */ /** 打开弹窗 */
const dialogVisible = ref(false) const dialogVisible = ref(false)
const open = (link: string) => { const open = (link: string) => {
activeAppLink.value.path = link // activeAppLink
activeAppLink.value = { name: '', path: '' }
dialogVisible.value = true dialogVisible.value = true
// //
@ -102,8 +103,11 @@ defineExpose({ open })
// APP // APP
const handleAppLinkSelected = (appLink: AppLink) => { const handleAppLinkSelected = (appLink: AppLink) => {
//
if (!isSameLink(appLink.path, activeAppLink.value.path)) { if (!isSameLink(appLink.path, activeAppLink.value.path)) {
activeAppLink.value = appLink // path 沿 activeAppLink path
const path = appLink.path || activeAppLink.value.path
activeAppLink.value = { ...appLink, path: path }
} }
switch (appLink.type) { switch (appLink.type) {
case APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST: case APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST:
@ -170,7 +174,7 @@ const groupBtnRefs = ref<ButtonInstance[]>([])
const scrollToGroupBtn = (group: string) => { const scrollToGroupBtn = (group: string) => {
const groupBtn = groupBtnRefs.value const groupBtn = groupBtnRefs.value
.map((btn: ButtonInstance) => btn['ref']) .map((btn: ButtonInstance) => btn['ref'])
.find((ref: Node) => ref.textContent === group) .find((ref: HTMLButtonElement) => ref.textContent === group)
if (groupBtn) { if (groupBtn) {
groupScrollbar.value?.setScrollTop(groupBtn.offsetTop) groupScrollbar.value?.setScrollTop(groupBtn.offsetTop)
} }

View File

@ -181,7 +181,6 @@ function openModal() {
} }
function closeModal() { function closeModal() {
debugger
dialogVisible.value = false dialogVisible.value = false
} }

View File

@ -10,6 +10,8 @@ export interface CarouselProperty {
autoplay: boolean autoplay: boolean
// 播放间隔 // 播放间隔
interval: number interval: number
// 轮播高度
height: number
// 轮播内容 // 轮播内容
items: CarouselItemProperty[] items: CarouselItemProperty[]
// 组件样式 // 组件样式
@ -37,6 +39,7 @@ export const component = {
indicator: 'dot', indicator: 'dot',
autoplay: false, autoplay: false,
interval: 3, interval: 3,
height: 174,
items: [ items: [
{ type: 'img', imgUrl: 'https://static.iocoder.cn/mall/banner-01.jpg', videoUrl: '' }, { type: 'img', imgUrl: 'https://static.iocoder.cn/mall/banner-01.jpg', videoUrl: '' },
{ type: 'img', imgUrl: 'https://static.iocoder.cn/mall/banner-02.jpg', videoUrl: '' } { type: 'img', imgUrl: 'https://static.iocoder.cn/mall/banner-02.jpg', videoUrl: '' }

View File

@ -8,7 +8,7 @@
</div> </div>
<div v-else class="relative"> <div v-else class="relative">
<el-carousel <el-carousel
height="174px" :height="property.height + 'px'"
:type="property.type === 'card' ? 'card' : ''" :type="property.type === 'card' ? 'card' : ''"
:autoplay="property.autoplay" :autoplay="property.autoplay"
:interval="property.interval * 1000" :interval="property.interval * 1000"

View File

@ -16,6 +16,9 @@
</el-tooltip> </el-tooltip>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="高度" prop="height">
<el-input-number class="!w-50% mr-10px" controls-position="right" v-model="formData.height" /> px
</el-form-item>
<el-form-item label="指示器" prop="indicator"> <el-form-item label="指示器" prop="indicator">
<el-radio-group v-model="formData.indicator"> <el-radio-group v-model="formData.indicator">
<el-radio value="dot">小圆点</el-radio> <el-radio value="dot">小圆点</el-radio>

View File

@ -13,7 +13,7 @@
<template v-for="(cell, cellIndex) in cellList" :key="cellIndex"> <template v-for="(cell, cellIndex) in cellList" :key="cellIndex">
<template v-if="selectedHotAreaIndex === cellIndex"> <template v-if="selectedHotAreaIndex === cellIndex">
<el-form-item :prop="`cell[${cellIndex}].type`" label="类型"> <el-form-item :prop="`cell[${cellIndex}].type`" label="类型">
<el-radio-group v-model="cell.type"> <el-radio-group v-model="cell.type" @change="handleHotAreaSelected(cell, cellIndex)">
<el-radio value="text">文字</el-radio> <el-radio value="text">文字</el-radio>
<el-radio value="image">图片</el-radio> <el-radio value="image">图片</el-radio>
<el-radio value="search">搜索框</el-radio> <el-radio value="search">搜索框</el-radio>
@ -44,9 +44,32 @@
</template> </template>
<!-- 3. 搜索框 --> <!-- 3. 搜索框 -->
<template v-else> <template v-else>
<el-form-item label="框体颜色" prop="backgroundColor">
<ColorInput v-model="cell.backgroundColor" />
</el-form-item>
<el-form-item class="lef" label="文本颜色" prop="textColor">
<ColorInput v-model="cell.textColor" />
</el-form-item>
<el-form-item :prop="`cell[${cellIndex}].placeholder`" label="提示文字"> <el-form-item :prop="`cell[${cellIndex}].placeholder`" label="提示文字">
<el-input v-model="cell.placeholder" maxlength="10" show-word-limit /> <el-input v-model="cell.placeholder" maxlength="10" show-word-limit />
</el-form-item> </el-form-item>
<el-form-item label="文本位置" prop="placeholderPosition">
<el-radio-group v-model="cell!.placeholderPosition">
<el-tooltip content="居左" placement="top">
<el-radio-button value="left">
<Icon icon="ant-design:align-left-outlined" />
</el-radio-button>
</el-tooltip>
<el-tooltip content="居中" placement="top">
<el-radio-button value="center">
<Icon icon="ant-design:align-center-outlined" />
</el-radio-button>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="扫一扫" prop="showScan">
<el-switch v-model="cell!.showScan" />
</el-form-item>
<el-form-item :prop="`cell[${cellIndex}].borderRadius`" label="圆角"> <el-form-item :prop="`cell[${cellIndex}].borderRadius`" label="圆角">
<el-slider <el-slider
v-model="cell.borderRadius" v-model="cell.borderRadius"
@ -88,10 +111,17 @@ const cellCount = computed(() => (props.isMp ? 6 : 8))
const selectedHotAreaIndex = ref(0) const selectedHotAreaIndex = ref(0)
const handleHotAreaSelected = (cellValue: NavigationBarCellProperty, index: number) => { const handleHotAreaSelected = (cellValue: NavigationBarCellProperty, index: number) => {
selectedHotAreaIndex.value = index selectedHotAreaIndex.value = index
//
if (!cellValue.type) { if (!cellValue.type) {
cellValue.type = 'text' cellValue.type = 'text'
cellValue.textColor = '#111111' cellValue.textColor = '#111111'
} }
//
if (cellValue.type === 'search') {
cellValue.placeholderPosition = 'left'
cellValue.backgroundColor = '#EEEEEE'
cellValue.textColor = '#969799'
}
} }
</script> </script>

View File

@ -45,8 +45,14 @@ export interface NavigationBarCellProperty {
imgUrl: string imgUrl: string
// 图片链接 // 图片链接
url: string url: string
// 搜索框:框体颜色
backgroundColor: string
// 搜索框:提示文字 // 搜索框:提示文字
placeholder: string placeholder: string
// 搜索框:提示文字位置
placeholderPosition: string
// 搜索框:是否显示扫一扫
showScan: boolean
// 搜索框:边框圆角半径 // 搜索框:边框圆角半径
borderRadius: number borderRadius: number
} }

View File

@ -54,9 +54,12 @@ const getCellStyle = (cell: NavigationBarCellProperty) => {
const getSearchProp = computed(() => (cell: NavigationBarCellProperty) => { const getSearchProp = computed(() => (cell: NavigationBarCellProperty) => {
return { return {
height: 30, height: 30,
showScan: false, backgroundColor: cell.backgroundColor,
showScan: cell.showScan,
placeholder: cell.placeholder, placeholder: cell.placeholder,
borderRadius: cell.borderRadius borderRadius: cell.borderRadius,
textColor: cell.textColor,
placeholderPosition: cell.placeholderPosition
} as SearchProperty } as SearchProperty
}) })
</script> </script>

View File

@ -269,6 +269,11 @@ watch(
if (!val || selectedComponentIndex.value === -1) { if (!val || selectedComponentIndex.value === -1) {
return return
} }
// -1
// https://gitee.com/yudaocode/yudao-ui-admin-vue3/pulls/792
if (props.showTabBar) {
selectedComponentIndex.value = -1
}
pageComponents.value[selectedComponentIndex.value] = selectedComponent.value! pageComponents.value[selectedComponentIndex.value] = selectedComponent.value!
}, },
{ deep: true } { deep: true }

View File

@ -72,6 +72,7 @@ watch(
(options) => { (options) => {
if (echartRef) { if (echartRef) {
echartRef?.setOption(options) echartRef?.setOption(options)
echartRef?.resize()
} }
}, },
{ {

View File

@ -97,8 +97,6 @@ defineOptions({
name: 'NodeHandler' name: 'NodeHandler'
}) })
const message = useMessage() //
const popoverShow = ref(false) const popoverShow = ref(false)
const props = defineProps({ const props = defineProps({
childNode: { childNode: {
@ -115,17 +113,6 @@ const emits = defineEmits(['update:childNode'])
const readonly = inject<Boolean>('readonly') // const readonly = inject<Boolean>('readonly') //
const addNode = (type: number) => { const addNode = (type: number) => {
//
if (
type === NodeType.PARALLEL_BRANCH_NODE &&
[NodeType.CONDITION_BRANCH_NODE, NodeType.INCLUSIVE_BRANCH_NODE].includes(
props.currentNode?.type
)
) {
message.error('条件分支、包容分支后面,不允许直接添加并行分支')
return
}
popoverShow.value = false popoverShow.value = false
if (type === NodeType.USER_TASK_NODE || type === NodeType.TRANSACTOR_NODE) { if (type === NodeType.USER_TASK_NODE || type === NodeType.TRANSACTOR_NODE) {
const id = 'Activity_' + generateUUID() const id = 'Activity_' + generateUUID()

View File

@ -26,8 +26,7 @@
<script setup lang="ts"> <script setup lang="ts">
import SimpleProcessModel from './SimpleProcessModel.vue' import SimpleProcessModel from './SimpleProcessModel.vue'
import { SimpleFlowNode, NodeType, NodeId, NODE_DEFAULT_TEXT } from './consts' import { SimpleFlowNode, NodeType, NodeId, NODE_DEFAULT_TEXT } from './consts'
import { getModel } from '@/api/bpm/model' import { getForm } from '@/api/bpm/form'
import { getForm, FormVO } from '@/api/bpm/form'
import { handleTree } from '@/utils/tree' import { handleTree } from '@/utils/tree'
import * as RoleApi from '@/api/system/role' import * as RoleApi from '@/api/system/role'
import * as DeptApi from '@/api/system/dept' import * as DeptApi from '@/api/system/dept'
@ -43,18 +42,22 @@ defineOptions({
const emits = defineEmits(['success']) // const emits = defineEmits(['success']) //
const props = defineProps({ const props = defineProps({
modelId: {
type: String,
required: false
},
modelKey: {
type: String,
required: false
},
modelName: { modelName: {
type: String, type: String,
required: false required: false
}, },
// ID
modelFormId: {
type: Number,
required: false,
default: undefined,
},
//
modelFormType: {
type: Number,
required: false,
default: BpmModelFormType.NORMAL,
},
// //
startUserIds: { startUserIds: {
type: Array, type: Array,
@ -70,7 +73,31 @@ const props = defineProps({
const processData = inject('processData') as Ref const processData = inject('processData') as Ref
const loading = ref(false) const loading = ref(false)
const formFields = ref<string[]>([]) const formFields = ref<string[]>([])
const formType = ref(20) const formType = ref(props.modelFormType);
// modelFormType
watch(
() => props.modelFormType,
(newVal) => {
formType.value = newVal;
},
);
// modelFormId
watch(
() => props.modelFormId,
async (newVal) => {
if (newVal) {
const form = await getForm(newVal);
formFields.value = form?.fields;
} else {
// modelFormId
formFields.value = [];
}
},
{ immediate: true },
);
const roleOptions = ref<RoleApi.RoleVO[]>([]) // const roleOptions = ref<RoleApi.RoleVO[]>([]) //
const postOptions = ref<PostApi.PostVO[]>([]) // const postOptions = ref<PostApi.PostVO[]>([]) //
const userOptions = ref<UserApi.UserVO[]>([]) // const userOptions = ref<UserApi.UserVO[]>([]) //
@ -90,6 +117,8 @@ provide('startUserIds', props.startUserIds)
provide('startDeptIds', props.startDeptIds) provide('startDeptIds', props.startDeptIds)
provide('tasks', []) provide('tasks', [])
provide('processInstance', {}) provide('processInstance', {})
const message = useMessage() // const message = useMessage() //
const processNodeTree = ref<SimpleFlowNode | undefined>() const processNodeTree = ref<SimpleFlowNode | undefined>()
provide('processNodeTree', processNodeTree) provide('processNodeTree', processNodeTree)
@ -169,17 +198,17 @@ const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNo
onMounted(async () => { onMounted(async () => {
try { try {
loading.value = true loading.value = true
// // //
if (props.modelId) { // if (props.modelId) {
const bpmnModel = await getModel(props.modelId) // const bpmnModel = await getModel(props.modelId)
if (bpmnModel) { // if (bpmnModel) {
formType.value = bpmnModel.formType // formType.value = bpmnModel.formType
if (formType.value === BpmModelFormType.NORMAL && bpmnModel.formId) { // if (formType.value === BpmModelFormType.NORMAL && bpmnModel.formId) {
const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO // const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO
formFields.value = bpmnForm?.fields // formFields.value = bpmnForm?.fields
} // }
} // }
} // }
// //
roleOptions.value = await RoleApi.getSimpleRoleList() roleOptions.value = await RoleApi.getSimpleRoleList()
// //

View File

@ -131,6 +131,8 @@ export interface SimpleFlowNode {
signEnable?: boolean signEnable?: boolean
// 审批意见 // 审批意见
reasonRequire?: boolean reasonRequire?: boolean
// 跳过表达式
skipExpression?: string
// 触发器设置 // 触发器设置
triggerSetting?: TriggerSetting triggerSetting?: TriggerSetting
// 子流程 // 子流程

View File

@ -177,6 +177,7 @@ export type UserTaskFormType = {
} }
signEnable: boolean signEnable: boolean
reasonRequire: boolean reasonRequire: boolean
skipExpression?: string
} }
export type CopyTaskFormType = { export type CopyTaskFormType = {

View File

@ -411,6 +411,12 @@
/> />
</el-form-item> </el-form-item>
</div> </div>
<div>
<el-divider content-position="left">跳过表达式</el-divider>
<el-form-item prop="skipExpression">
<el-input v-model="configForm.skipExpression" type="textarea" />
</el-form-item>
</div>
</el-form> </el-form>
</div> </div>
</el-tab-pane> </el-tab-pane>
@ -770,6 +776,8 @@ const saveConfig = async () => {
currentNode.value.signEnable = configForm.value.signEnable currentNode.value.signEnable = configForm.value.signEnable
// //
currentNode.value.reasonRequire = configForm.value.reasonRequire currentNode.value.reasonRequire = configForm.value.reasonRequire
//
currentNode.value.skipExpression = configForm.value.skipExpression
currentNode.value.showText = showText currentNode.value.showText = showText
settingVisible.value = false settingVisible.value = false
@ -851,6 +859,8 @@ const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
configForm.value.signEnable = node?.signEnable ?? false configForm.value.signEnable = node?.signEnable ?? false
// 7. // 7.
configForm.value.reasonRequire = node?.reasonRequire ?? false configForm.value.reasonRequire = node?.reasonRequire ?? false
// 8.
configForm.value.skipExpression = node?.skipExpression ?? ''
} }
defineExpose({ openDrawer, showUserTaskNodeConfig }) // defineExpose({ openDrawer, showUserTaskNodeConfig }) //

View File

@ -1,6 +1,6 @@
<template> <template>
<el-form-item label-position="top" label="请求头"> <el-form-item label-position="top" label="请求头">
<div class="flex pt-2" v-for="(item, index) in props.header" :key="index"> <div class="flex pb-4" v-for="(item, index) in props.header" :key="index">
<div class="mr-2"> <div class="mr-2">
<el-form-item <el-form-item
:prop="`${bind}.header.${index}.key`" :prop="`${bind}.header.${index}.key`"
@ -10,18 +10,20 @@
trigger: 'blur' trigger: 'blur'
}" }"
> >
<el-input class="w-160px" v-model="item.key" /> <el-input v-model="item.key" style="width: 160px" />
</el-form-item> </el-form-item>
</div> </div>
<div class="mr-2"> <div class="mr-2">
<el-select class="w-100px!" v-model="item.type"> <el-form-item>
<el-option <el-select v-model="item.type" style="width: 160px" @change="handleTypeChange(item)">
v-for="types in BPM_HTTP_REQUEST_PARAM_TYPES" <el-option
:key="types.value" v-for="types in BPM_HTTP_REQUEST_PARAM_TYPES"
:label="types.label" :key="types.value"
:value="types.value" :label="types.label"
/> :value="types.value"
</el-select> />
</el-select>
</el-form-item>
</div> </div>
<div class="mr-2"> <div class="mr-2">
<el-form-item <el-form-item
@ -34,8 +36,8 @@
> >
<el-input <el-input
v-if="item.type === BpmHttpRequestParamTypeEnum.FIXED_VALUE" v-if="item.type === BpmHttpRequestParamTypeEnum.FIXED_VALUE"
class="w-160px"
v-model="item.value" v-model="item.value"
style="width: 200px"
/> />
</el-form-item> </el-form-item>
<el-form-item <el-form-item
@ -48,8 +50,8 @@
> >
<el-select <el-select
v-if="item.type === BpmHttpRequestParamTypeEnum.FROM_FORM" v-if="item.type === BpmHttpRequestParamTypeEnum.FROM_FORM"
class="w-160px!"
v-model="item.value" v-model="item.value"
style="width: 200px"
> >
<el-option <el-option
v-for="(field, fIdx) in formFieldOptions" v-for="(field, fIdx) in formFieldOptions"
@ -70,7 +72,7 @@
</el-button> </el-button>
</el-form-item> </el-form-item>
<el-form-item label-position="top" label="请求体"> <el-form-item label-position="top" label="请求体">
<div class="flex pt-2" v-for="(item, index) in props.body" :key="index"> <div class="flex pb-4" v-for="(item, index) in props.body" :key="index">
<div class="mr-2"> <div class="mr-2">
<el-form-item <el-form-item
:prop="`${bind}.body.${index}.key`" :prop="`${bind}.body.${index}.key`"
@ -80,18 +82,20 @@
trigger: 'blur' trigger: 'blur'
}" }"
> >
<el-input class="w-160px" v-model="item.key" /> <el-input v-model="item.key" style="width: 160px" />
</el-form-item> </el-form-item>
</div> </div>
<div class="mr-2"> <div class="mr-2">
<el-select class="w-100px!" v-model="item.type"> <el-form-item>
<el-option <el-select v-model="item.type" style="width: 160px" @change="handleTypeChange(item)">
v-for="types in BPM_HTTP_REQUEST_PARAM_TYPES" <el-option
:key="types.value" v-for="types in BPM_HTTP_REQUEST_PARAM_TYPES"
:label="types.label" :key="types.value"
:value="types.value" :label="types.label"
/> :value="types.value"
</el-select> />
</el-select>
</el-form-item>
</div> </div>
<div class="mr-2"> <div class="mr-2">
<el-form-item <el-form-item
@ -104,8 +108,8 @@
> >
<el-input <el-input
v-if="item.type === BpmHttpRequestParamTypeEnum.FIXED_VALUE" v-if="item.type === BpmHttpRequestParamTypeEnum.FIXED_VALUE"
class="w-160px"
v-model="item.value" v-model="item.value"
style="width: 200px"
/> />
</el-form-item> </el-form-item>
<el-form-item <el-form-item
@ -118,8 +122,8 @@
> >
<el-select <el-select
v-if="item.type === BpmHttpRequestParamTypeEnum.FROM_FORM" v-if="item.type === BpmHttpRequestParamTypeEnum.FROM_FORM"
class="w-160px!"
v-model="item.value" v-model="item.value"
style="width: 200px"
> >
<el-option <el-option
v-for="(field, fIdx) in formFieldOptions" v-for="(field, fIdx) in formFieldOptions"
@ -170,6 +174,13 @@ const props = defineProps({
// //
const formFieldOptions = useFormFieldsAndStartUser() const formFieldOptions = useFormFieldsAndStartUser()
/** 监听类型变化,清空值 */
const handleTypeChange = (item: HttpRequestParam) => {
//
item.value = ''
}
/** 添加请求配置项 */ /** 添加请求配置项 */
const addHttpRequestParam = (arr: HttpRequestParam[]) => { const addHttpRequestParam = (arr: HttpRequestParam[]) => {
arr.push({ arr.push({

View File

@ -33,7 +33,7 @@
/> />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<div class="flex pt-2" v-for="(item, index) in setting.response" :key="index"> <div class="flex pt-4" v-for="(item, index) in setting.response" :key="index">
<div class="mr-2"> <div class="mr-2">
<el-form-item <el-form-item
:prop="`${formItemPrefix}.response.${index}.key`" :prop="`${formItemPrefix}.response.${index}.key`"
@ -74,10 +74,12 @@
/> />
</div> </div>
</div> </div>
</el-form-item>
<div class="pt-1">
<el-button type="primary" text @click="addHttpResponseSetting(setting.response!)"> <el-button type="primary" text @click="addHttpResponseSetting(setting.response!)">
<Icon icon="ep:plus" class="mr-5px" />添加一行 <Icon icon="ep:plus" class="mr-5px" />添加一行
</el-button> </el-button>
</el-form-item> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@ -122,7 +122,9 @@ const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {
return false return false
} }
message.success('正在上传文件,请稍候...') message.success('正在上传文件,请稍候...')
//
uploadNumber.value++ uploadNumber.value++
return true
} }
// //
// const handleFileChange = (uploadFile: UploadFile): void => { // const handleFileChange = (uploadFile: UploadFile): void => {
@ -149,6 +151,8 @@ const handleExceed: UploadProps['onExceed'] = (): void => {
// //
const excelUploadError: UploadProps['onError'] = (): void => { const excelUploadError: UploadProps['onError'] = (): void => {
message.error('导入数据失败,请您重新上传!') message.error('导入数据失败,请您重新上传!')
//
uploadNumber.value = Math.max(0, uploadNumber.value - 1)
} }
// //
const handleRemove = (file: UploadFile) => { const handleRemove = (file: UploadFile) => {

View File

@ -97,20 +97,28 @@ const uploadList = ref<UploadUserFile[]>([])
const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => { const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
const imgSize = rawFile.size / 1024 / 1024 < props.fileSize const imgSize = rawFile.size / 1024 / 1024 < props.fileSize
const imgType = props.fileType const imgType = props.fileType
if (!imgType.includes(rawFile.type as FileTypes)) const isValidType = imgType.includes(rawFile.type as FileTypes)
const isValidSize = imgSize
if (!isValidType)
ElNotification({ ElNotification({
title: '温馨提示', title: '温馨提示',
message: '上传图片不符合所需的格式!', message: '上传图片不符合所需的格式!',
type: 'warning' type: 'warning'
}) })
if (!imgSize) if (!isValidSize)
ElNotification({ ElNotification({
title: '温馨提示', title: '温馨提示',
message: `上传图片大小不能超过 ${props.fileSize}M`, message: `上传图片大小不能超过 ${props.fileSize}M`,
type: 'warning' type: 'warning'
}) })
uploadNumber.value++
return imgType.includes(rawFile.type as FileTypes) && imgSize //
if (isValidType && isValidSize) {
uploadNumber.value++
}
return isValidType && isValidSize
} }
// //
@ -172,6 +180,8 @@ const uploadError = () => {
message: '图片上传失败,请您重新上传!', message: '图片上传失败,请您重新上传!',
type: 'error' type: 'error'
}) })
//
uploadNumber.value = Math.max(0, uploadNumber.value - 1)
} }
// //

View File

@ -1014,6 +1014,16 @@
"name": "fields", "name": "fields",
"type": "Field", "type": "Field",
"isMany": true "isMany": true
},
{
"name": "id",
"type": "String",
"isAttr": true
},
{
"name": "eventDefinitions",
"type": "bpmn:TimerEventDefinition",
"isMany": true
} }
] ]
}, },

View File

@ -28,7 +28,12 @@
v-model="timeDuration" v-model="timeDuration"
:min="1" :min="1"
controls-position="right" controls-position="right"
@change="() => updateTimeModdle()" @change="
() => {
updateTimeModdle()
updateElementExtensions()
}
"
/> />
</el-form-item> </el-form-item>
<el-select <el-select
@ -55,7 +60,12 @@
v-model="maxRemindCount" v-model="maxRemindCount"
:min="1" :min="1"
:max="10" :max="10"
@change="() => updateTimeModdle()" @change="
() => {
updateTimeModdle()
updateElementExtensions()
}
"
/> />
</el-form-item> </el-form-item>
</div> </div>
@ -65,7 +75,7 @@
import { import {
TimeUnitType, TimeUnitType,
TIME_UNIT_TYPES, TIME_UNIT_TYPES,
TIMEOUT_HANDLER_TYPES, TIMEOUT_HANDLER_TYPES
} from '@/components/SimpleProcessDesignerV2/src/consts' } from '@/components/SimpleProcessDesignerV2/src/consts'
import { convertTimeUnit } from '@/components/SimpleProcessDesignerV2/src/utils' import { convertTimeUnit } from '@/components/SimpleProcessDesignerV2/src/utils'
@ -195,6 +205,7 @@ const onTimeUnitChange = () => {
timeDuration.value = 1 timeDuration.value = 1
} }
updateTimeModdle() updateTimeModdle()
updateElementExtensions()
} }
const updateTimeModdle = () => { const updateTimeModdle = () => {

View File

@ -354,12 +354,13 @@ const resetTaskForm = () => {
const changeCandidateStrategy = () => { const changeCandidateStrategy = () => {
userTaskForm.value.candidateParam = [] userTaskForm.value.candidateParam = []
deptLevel.value = 1 deptLevel.value = 1
if (userTaskForm.value.candidateStrategy === CandidateStrategy.FORM_USER) { // by https://t.zsxq.com/xNmas
// // if (userTaskForm.value.candidateStrategy === CandidateStrategy.FORM_USER) {
if (!userFieldOnFormOptions.value || userFieldOnFormOptions.value.length <= 1) { // //
userTaskForm.value.candidateStrategy = CandidateStrategy.START_USER // if (!userFieldOnFormOptions.value || userFieldOnFormOptions.value.length <= 1) {
} // userTaskForm.value.candidateStrategy = CandidateStrategy.START_USER
} // }
// }
updateElementTask() updateElementTask()
} }

View File

@ -55,6 +55,7 @@ import {
ElCollapse, ElCollapse,
ElCollapseItem, ElCollapseItem,
ElCard, ElCard,
ElTreeSelect
// ElFormItem, // ElFormItem,
// ElOption // ElOption
} from 'element-plus' } from 'element-plus'
@ -97,6 +98,7 @@ const components = [
ElTableColumn, ElTableColumn,
ElTabPane, ElTabPane,
ElTabs, ElTabs,
ElTreeSelect,
ElDropdown, ElDropdown,
ElDropdownMenu, ElDropdownMenu,
ElDropdownItem, ElDropdownItem,
@ -119,7 +121,7 @@ const components = [
Editor, Editor,
ElCollapse, ElCollapse,
ElCollapseItem, ElCollapseItem,
ElCard, ElCard
] ]
// 参考 http://www.form-create.com/v3/element-ui/auto-import.html 文档 // 参考 http://www.form-create.com/v3/element-ui/auto-import.html 文档

View File

@ -12,7 +12,7 @@ const router = createRouter({
}) })
export const resetRouter = (): void => { export const resetRouter = (): void => {
const resetWhiteNameList = ['Redirect', 'Login', 'NoFind', 'Root'] const resetWhiteNameList = ['Redirect', 'Login', 'NoFound', 'Home']
router.getRoutes().forEach((route) => { router.getRoutes().forEach((route) => {
const { name } = route const { name } = route
if (name && !resetWhiteNameList.includes(name as string)) { if (name && !resetWhiteNameList.includes(name as string)) {

View File

@ -62,7 +62,7 @@ export const useUserStore = defineStore('admin-user', {
userInfo = await getInfo() userInfo = await getInfo()
} catch (error) {} } catch (error) {}
} }
this.permissions = new Set(userInfo.permissions) this.permissions = new Set(userInfo.permissions || []) // 兜底为 [] https://t.zsxq.com/xCJew
this.roles = userInfo.roles this.roles = userInfo.roles
this.user = userInfo.user this.user = userInfo.user
this.isSetUser = true this.isSetUser = true

View File

@ -1,6 +1,7 @@
/** /**
* https://github.com/xaboy/form-create-designer 封装的工具类 * https://github.com/xaboy/form-create-designer 封装的工具类
*/ */
import { isRef } from 'vue'
// 编码表单 Conf // 编码表单 Conf
export const encodeConf = (designerRef: object) => { export const encodeConf = (designerRef: object) => {
@ -47,10 +48,156 @@ export const setConfAndFields2 = (
// @ts-ignore // @ts-ignore
detailPreview = detailPreview.value detailPreview = detailPreview.value
} }
// 修复所有函数类型(解决设计器保存后函数变成字符串的问题)。例如说:
// https://t.zsxq.com/rADff
// https://t.zsxq.com/ZfbGt
// https://t.zsxq.com/mHOoj
// https://t.zsxq.com/BSylB
const option = JSON.parse(conf)
const rule = decodeFields(fields)
// 🔧 修复所有函数类型 - 解决设计器保存后函数变成字符串的问题
const fixFunctions = (obj: any) => {
if (obj && typeof obj === 'object') {
Object.keys(obj).forEach((key) => {
// 检查是否是函数相关的属性
if (isFunctionProperty(key)) {
// 如果不是函数类型,重新构建为函数
if (typeof obj[key] !== 'function') {
obj[key] = createDefaultFunction(key)
}
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
// 递归处理嵌套对象
fixFunctions(obj[key])
}
})
}
}
// 判断是否是函数属性
const isFunctionProperty = (key: string): boolean => {
const functionKeys = [
'beforeFetch', // 请求前处理
'afterFetch', // 请求后处理
'onSubmit', // 表单提交
'onReset', // 表单重置
'onChange', // 值变化
'onInput', // 输入事件
'onClick', // 点击事件
'onFocus', // 获取焦点
'onBlur', // 失去焦点
'onMounted', // 组件挂载
'onCreated', // 组件创建
'onReload', // 重新加载
'remoteMethod', // 远程搜索方法
'parseFunc', // 解析函数
'validator', // 验证器
'asyncValidator', // 异步验证器
'formatter', // 格式化函数
'parser', // 解析函数
'beforeUpload', // 上传前处理
'onSuccess', // 成功回调
'onError', // 错误回调
'onProgress', // 进度回调
'onPreview', // 预览回调
'onRemove', // 移除回调
'onExceed', // 超出限制回调
'filterMethod', // 过滤方法
'sortMethod', // 排序方法
'loadData', // 加载数据
'renderContent', // 渲染内容
'render' // 渲染函数
]
// 检查是否以函数相关前缀开头
const functionPrefixes = ['on', 'before', 'after', 'handle']
return functionKeys.includes(key) || functionPrefixes.some((prefix) => key.startsWith(prefix))
}
// 根据函数名创建默认函数
const createDefaultFunction = (key: string): Function => {
switch (key) {
case 'beforeFetch':
return (config: any) => {
// 添加 Token 认证头。例如说:
// https://t.zsxq.com/hK3FO
const token = localStorage.getItem('token')
if (token) {
config.headers = {
...config.headers,
Authorization: 'Bearer ' + token
}
}
// 添加通用请求头
config.headers = {
...config.headers,
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
// 添加时间戳防止缓存
config.params = {
...config.params,
_t: Date.now()
}
return config
}
case 'afterFetch':
return (data: any) => {
return data
}
case 'onSubmit':
return (_formData: any) => {
return true
}
case 'onReset':
return () => {
return true
}
case 'onChange':
return (_value: any, _oldValue: any) => {}
case 'remoteMethod':
return (query: string) => {
console.log('remoteMethod被调用:', query)
}
case 'parseFunc':
return (data: any) => {
// 默认解析逻辑如果是数组直接返回否则尝试获取list属性
if (Array.isArray(data)) {
return data
}
return data?.list || data?.data || []
}
case 'validator':
return (_rule: any, _value: any, callback: Function) => {
callback()
}
case 'beforeUpload':
return (_file: any) => {
return true
}
default:
// 通用默认函数
return (...args: any[]) => {
// 对于事件处理函数返回true表示继续执行
if (key.startsWith('on') || key.startsWith('handle')) {
return true
}
// 对于其他函数,返回第一个参数(通常是数据传递)
return args[0]
}
}
}
// 修复 option 中的所有函数
fixFunctions(option)
// 修复 rule 中的所有函数(包括组件的 props
if (Array.isArray(rule)) {
rule.forEach((item: any) => {
fixFunctions(item)
})
}
// @ts-ignore // @ts-ignore
detailPreview.option = JSON.parse(conf) detailPreview.option = option
// @ts-ignore // @ts-ignore
detailPreview.rule = decodeFields(fields) detailPreview.rule = rule
if (value) { if (value) {
// @ts-ignore // @ts-ignore
detailPreview.value = value detailPreview.value = value

View File

@ -101,7 +101,7 @@ export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecord
if (!route.children && route.parentId == 0 && route.component) { if (!route.children && route.parentId == 0 && route.component) {
data.component = Layout data.component = Layout
data.meta = { data.meta = {
hidden: meta.hidden, hidden: meta.hidden
} }
data.name = toCamelCase(route.path, true) + 'Parent' data.name = toCamelCase(route.path, true) + 'Parent'
data.redirect = '' data.redirect = ''
@ -170,8 +170,9 @@ const generateRoutePath = (parentPath: string, path: string) => {
} }
export const pathResolve = (parentPath: string, path: string) => { export const pathResolve = (parentPath: string, path: string) => {
if (isUrl(path)) return path if (isUrl(path)) return path
const childPath = path.startsWith('/') || !path ? path : `/${path}` if (!path) return parentPath // 修复 path 为空时返回 parentPath避免拼接出错 https://t.zsxq.com/QVr6b
return `${parentPath}${childPath}`.replace(/\/\//g, '/') const childPath = path.startsWith('/') ? path : `/${path}`
return `${parentPath}${childPath}`.replace(/\/+/g, '/')
} }
// 路由降级 // 路由降级

View File

@ -118,4 +118,4 @@ $prefix-cls: #{$namespace}-login;
background-color: var(--login-bg-color); background-color: var(--login-bg-color);
} }
} }
</style> </style>

View File

@ -9,14 +9,14 @@
label-width="120px" label-width="120px"
size="large" size="large"
> >
<el-row style="margin-right: -10px; margin-left: -10px"> <el-row class="mx-[-10px]">
<!-- 租户名 --> <!-- 租户名 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item> <el-form-item>
<LoginFormTitle style="width: 100%" /> <LoginFormTitle class="w-full" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item v-if="resetPasswordData.tenantEnable === 'true'" prop="tenantName"> <el-form-item v-if="resetPasswordData.tenantEnable === 'true'" prop="tenantName">
<el-input <el-input
v-model="resetPasswordData.tenantName" v-model="resetPasswordData.tenantName"
@ -28,7 +28,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<!-- 手机号 --> <!-- 手机号 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item prop="mobile"> <el-form-item prop="mobile">
<el-input <el-input
v-model="resetPasswordData.mobile" v-model="resetPasswordData.mobile"
@ -45,7 +45,7 @@
@success="getSmsCode" @success="getSmsCode"
/> />
<!-- 验证码 --> <!-- 验证码 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item prop="code"> <el-form-item prop="code">
<el-row :gutter="5" justify="space-between" style="width: 100%"> <el-row :gutter="5" justify="space-between" style="width: 100%">
<el-col :span="24"> <el-col :span="24">
@ -73,44 +73,44 @@
</el-row> </el-row>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item prop="password"> <el-form-item prop="password">
<InputPassword <InputPassword
v-model="resetPasswordData.password" v-model="resetPasswordData.password"
:placeholder="t('login.passwordPlaceholder')" :placeholder="t('login.passwordPlaceholder')"
style="width: 100%" class="w-full"
strength="true" :strength="true"
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item prop="check_password"> <el-form-item prop="check_password">
<InputPassword <InputPassword
v-model="resetPasswordData.check_password" v-model="resetPasswordData.check_password"
:placeholder="t('login.checkPassword')" :placeholder="t('login.checkPassword')"
style="width: 100%" class="w-full"
strength="true" :strength="true"
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<!-- 登录按钮 / 返回按钮 --> <!-- 登录按钮 / 返回按钮 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item> <el-form-item>
<XButton <XButton
:loading="loginLoading" :loading="loginLoading"
:title="t('login.resetPassword')" :title="t('login.resetPassword')"
class="w-[100%]" class="w-full"
type="primary" type="primary"
@click="resetPassword()" @click="resetPassword()"
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item> <el-form-item>
<XButton <XButton
:loading="loginLoading" :loading="loginLoading"
:title="t('login.backLogin')" :title="t('login.backLogin')"
class="w-[100%]" class="w-full"
@click="handleBackLogin()" @click="handleBackLogin()"
/> />
</el-form-item> </el-form-item>
@ -134,7 +134,7 @@ const verify = ref()
const { t } = useI18n() const { t } = useI18n()
const message = useMessage() const message = useMessage()
const { currentRoute, push } = useRouter() const { currentRoute } = useRouter()
const formSmsResetPassword = ref() const formSmsResetPassword = ref()
const loginLoading = ref(false) const loginLoading = ref(false)
const iconHouse = useIcon({ icon: 'ep:house' }) const iconHouse = useIcon({ icon: 'ep:house' })
@ -145,7 +145,7 @@ const { handleBackLogin, getLoginState, setLoginState } = useLoginState()
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.RESET_PASSWORD) const getShow = computed(() => unref(getLoginState) === LoginStateEnum.RESET_PASSWORD)
const captchaType = ref('blockPuzzle') // blockPuzzle clickWord const captchaType = ref('blockPuzzle') // blockPuzzle clickWord
const validatePass2 = (rule, value, callback) => { const validatePass2 = (_rule, value, callback) => {
if (value === '') { if (value === '') {
callback(new Error('请再次输入密码')) callback(new Error('请再次输入密码'))
} else if (value !== resetPasswordData.password) { } else if (value !== resetPasswordData.password) {

View File

@ -9,13 +9,13 @@
label-width="120px" label-width="120px"
size="large" size="large"
> >
<el-row style="margin-right: -10px; margin-left: -10px"> <el-row class="mx-[-10px]">
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item> <el-form-item>
<LoginFormTitle style="width: 100%" /> <LoginFormTitle class="w-full" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName"> <el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
<el-input <el-input
v-model="loginData.loginForm.tenantName" v-model="loginData.loginForm.tenantName"
@ -26,7 +26,7 @@
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item prop="username"> <el-form-item prop="username">
<el-input <el-input
v-model="loginData.loginForm.username" v-model="loginData.loginForm.username"
@ -35,7 +35,7 @@
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item prop="password"> <el-form-item prop="password">
<el-input <el-input
v-model="loginData.loginForm.password" v-model="loginData.loginForm.password"
@ -49,7 +49,7 @@
</el-col> </el-col>
<el-col <el-col
:span="24" :span="24"
style="padding-right: 10px; padding-left: 10px; margin-top: -20px; margin-bottom: -20px" class="px-10px mt-[-20px] mb-[-20px]"
> >
<el-form-item> <el-form-item>
<el-row justify="space-between" style="width: 100%"> <el-row justify="space-between" style="width: 100%">
@ -60,7 +60,7 @@
</el-col> </el-col>
<el-col :offset="6" :span="12"> <el-col :offset="6" :span="12">
<el-link <el-link
style="float: right" class="float-right"
type="primary" type="primary"
@click="setLoginState(LoginStateEnum.RESET_PASSWORD)" @click="setLoginState(LoginStateEnum.RESET_PASSWORD)"
> >
@ -70,12 +70,12 @@
</el-row> </el-row>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item> <el-form-item>
<XButton <XButton
:loading="loginLoading" :loading="loginLoading"
:title="t('login.login')" :title="t('login.login')"
class="w-[100%]" class="w-full"
type="primary" type="primary"
@click="getCode()" @click="getCode()"
/> />
@ -89,27 +89,27 @@
mode="pop" mode="pop"
@success="handleLogin" @success="handleLogin"
/> />
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item> <el-form-item>
<el-row :gutter="5" justify="space-between" style="width: 100%"> <el-row :gutter="5" justify="space-between" style="width: 100%">
<el-col :span="8"> <el-col :span="8">
<XButton <XButton
:title="t('login.btnMobile')" :title="t('login.btnMobile')"
class="w-[100%]" class="w-full"
@click="setLoginState(LoginStateEnum.MOBILE)" @click="setLoginState(LoginStateEnum.MOBILE)"
/> />
</el-col> </el-col>
<el-col :span="8"> <el-col :span="8">
<XButton <XButton
:title="t('login.btnQRCode')" :title="t('login.btnQRCode')"
class="w-[100%]" class="w-full"
@click="setLoginState(LoginStateEnum.QR_CODE)" @click="setLoginState(LoginStateEnum.QR_CODE)"
/> />
</el-col> </el-col>
<el-col :span="8"> <el-col :span="8">
<XButton <XButton
:title="t('login.btnRegister')" :title="t('login.btnRegister')"
class="w-[100%]" class="w-full"
@click="setLoginState(LoginStateEnum.REGISTER)" @click="setLoginState(LoginStateEnum.REGISTER)"
/> />
</el-col> </el-col>
@ -117,9 +117,9 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-divider content-position="center">{{ t('login.otherLogin') }}</el-divider> <el-divider content-position="center">{{ t('login.otherLogin') }}</el-divider>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item> <el-form-item>
<div class="w-[100%] flex justify-between"> <div class="w-full flex justify-between">
<Icon <Icon
v-for="(item, key) in socialList" v-for="(item, key) in socialList"
:key="key" :key="key"
@ -133,9 +133,9 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-divider content-position="center">萌新必读</el-divider> <el-divider content-position="center">萌新必读</el-divider>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item> <el-form-item>
<div class="w-[100%] flex justify-between"> <div class="w-full flex justify-between">
<el-link href="https://doc.iocoder.cn/" target="_blank">📚开发指南</el-link> <el-link href="https://doc.iocoder.cn/" target="_blank">📚开发指南</el-link>
<el-link href="https://doc.iocoder.cn/video/" target="_blank">🔥视频教程</el-link> <el-link href="https://doc.iocoder.cn/video/" target="_blank">🔥视频教程</el-link>
<el-link href="https://www.iocoder.cn/Interview/good-collection/" target="_blank"> <el-link href="https://www.iocoder.cn/Interview/good-collection/" target="_blank">
@ -239,11 +239,13 @@ const getLoginFormCache = () => {
} }
// //
const getTenantByWebsite = async () => { const getTenantByWebsite = async () => {
const website = location.host if (loginData.tenantEnable === 'true') {
const res = await LoginApi.getTenantByWebsite(website) const website = location.host
if (res) { const res = await LoginApi.getTenantByWebsite(website)
loginData.loginForm.tenantName = res.name if (res) {
authUtil.setTenantId(res.id) loginData.loginForm.tenantName = res.name
authUtil.setTenantId(res.id)
}
} }
} }
const loading = ref() // ElLoading.service const loading = ref() // ElLoading.service

View File

@ -9,14 +9,14 @@
label-width="120px" label-width="120px"
size="large" size="large"
> >
<el-row style="margin-right: -10px; margin-left: -10px"> <el-row class="mx-[-10px]">
<!-- 租户名 --> <!-- 租户名 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item> <el-form-item>
<LoginFormTitle style="width: 100%" /> <LoginFormTitle class="w-full" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName"> <el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
<el-input <el-input
v-model="loginData.loginForm.tenantName" v-model="loginData.loginForm.tenantName"
@ -28,7 +28,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<!-- 手机号 --> <!-- 手机号 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item prop="mobileNumber"> <el-form-item prop="mobileNumber">
<el-input <el-input
v-model="loginData.loginForm.mobileNumber" v-model="loginData.loginForm.mobileNumber"
@ -38,7 +38,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<!-- 验证码 --> <!-- 验证码 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item prop="code"> <el-form-item prop="code">
<el-row :gutter="5" justify="space-between" style="width: 100%"> <el-row :gutter="5" justify="space-between" style="width: 100%">
<el-col :span="24"> <el-col :span="24">
@ -68,23 +68,23 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<!-- 登录按钮 / 返回按钮 --> <!-- 登录按钮 / 返回按钮 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item> <el-form-item>
<XButton <XButton
:loading="loginLoading" :loading="loginLoading"
:title="t('login.login')" :title="t('login.login')"
class="w-[100%]" class="w-full"
type="primary" type="primary"
@click="signIn()" @click="signIn()"
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item> <el-form-item>
<XButton <XButton
:loading="loginLoading" :loading="loginLoading"
:title="t('login.backLogin')" :title="t('login.backLogin')"
class="w-[100%]" class="w-full"
@click="handleBackLogin()" @click="handleBackLogin()"
/> />
</el-form-item> </el-form-item>

View File

@ -1,17 +1,17 @@
<template> <template>
<el-row v-show="getShow" class="login-form" style="margin-right: -10px; margin-left: -10px"> <el-row v-show="getShow" class="login-form mx-[-10px]">
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<LoginFormTitle style="width: 100%" /> <LoginFormTitle class="w-full" />
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-card class="mb-10px text-center" shadow="hover"> <el-card class="mb-10px text-center" shadow="hover">
<Qrcode :logo="logoImg" /> <Qrcode :logo="logoImg" />
</el-card> </el-card>
</el-col> </el-col>
<el-divider class="enter-x">{{ t('login.qrcode') }}</el-divider> <el-divider class="enter-x">{{ t('login.qrcode') }}</el-divider>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<div class="mt-15px w-[100%]"> <div class="mt-4 w-full">
<XButton :title="t('login.backLogin')" class="w-[100%]" @click="handleBackLogin()" /> <XButton :title="t('login.backLogin')" class="w-full" @click="handleBackLogin()" />
</div> </div>
</el-col> </el-col>
</el-row> </el-row>

View File

@ -9,13 +9,13 @@
label-width="120px" label-width="120px"
size="large" size="large"
> >
<el-row style="margin-right: -10px; margin-left: -10px"> <el-row class="mx-[-10px]">
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item> <el-form-item>
<LoginFormTitle style="width: 100%" /> <LoginFormTitle class="w-full" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item v-if="registerData.tenantEnable === 'true'" prop="tenantName"> <el-form-item v-if="registerData.tenantEnable === 'true'" prop="tenantName">
<el-input <el-input
v-model="registerData.registerForm.tenantName" v-model="registerData.registerForm.tenantName"
@ -27,7 +27,7 @@
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item prop="username"> <el-form-item prop="username">
<el-input <el-input
v-model="registerData.registerForm.username" v-model="registerData.registerForm.username"
@ -37,7 +37,7 @@
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item prop="username"> <el-form-item prop="username">
<el-input <el-input
v-model="registerData.registerForm.nickname" v-model="registerData.registerForm.nickname"
@ -47,7 +47,7 @@
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item prop="password"> <el-form-item prop="password">
<el-input <el-input
v-model="registerData.registerForm.password" v-model="registerData.registerForm.password"
@ -60,7 +60,7 @@
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item prop="confirmPassword"> <el-form-item prop="confirmPassword">
<el-input <el-input
v-model="registerData.registerForm.confirmPassword" v-model="registerData.registerForm.confirmPassword"
@ -73,12 +73,12 @@
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" class="px-10px">
<el-form-item> <el-form-item>
<XButton <XButton
:loading="loginLoading" :loading="loginLoading"
:title="t('login.register')" :title="t('login.register')"
class="w-[100%]" class="w-full"
type="primary" type="primary"
@click="getCode()" @click="getCode()"
/> />
@ -93,7 +93,7 @@
@success="handleRegister" @success="handleRegister"
/> />
</el-row> </el-row>
<XButton :title="t('login.hasUser')" class="w-[100%]" @click="handleBackLogin()" /> <XButton :title="t('login.hasUser')" class="w-full" @click="handleBackLogin()" />
</el-form> </el-form>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -123,7 +123,7 @@ const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.REGISTER) const getShow = computed(() => unref(getLoginState) === LoginStateEnum.REGISTER)
const equalToPassword = (rule, value, callback) => { const equalToPassword = (_rule, value, callback) => {
if (registerData.registerForm.password !== value) { if (registerData.registerForm.password !== value) {
callback(new Error('两次输入的密码不一致')) callback(new Error('两次输入的密码不一致'))
} else { } else {
@ -233,11 +233,13 @@ const getTenantId = async () => {
// //
const getTenantByWebsite = async () => { const getTenantByWebsite = async () => {
const website = location.host if (registerData.tenantEnable === 'true') {
const res = await LoginApi.getTenantByWebsite(website) const website = location.host
if (res) { const res = await LoginApi.getTenantByWebsite(website)
registerData.registerForm.tenantName = res.name if (res) {
authUtil.setTenantId(res.id) registerData.registerForm.tenantName = res.name
authUtil.setTenantId(res.id)
}
} }
} }
const loading = ref() // ElLoading.service const loading = ref() // ElLoading.service

View File

@ -1,7 +1,7 @@
<template> <template>
<div v-show="ssoVisible" class="form-cont"> <div v-show="ssoVisible" class="form-cont">
<!-- 应用名 --> <!-- 应用名 -->
<LoginFormTitle style="width: 100%" /> <LoginFormTitle class="w-full" />
<el-tabs class="form" style="float: none" value="uname"> <el-tabs class="form" style="float: none" value="uname">
<el-tab-pane :label="client.name" name="uname" /> <el-tab-pane :label="client.name" name="uname" />
</el-tabs> </el-tabs>
@ -15,17 +15,17 @@
v-for="scope in queryParams.scopes" v-for="scope in queryParams.scopes"
:key="scope" :key="scope"
:value="scope" :value="scope"
style="display: block; margin-bottom: -10px" class="block mb-[-10px]"
> >
{{ formatScope(scope) }} {{ formatScope(scope) }}
</el-checkbox> </el-checkbox>
</el-checkbox-group> </el-checkbox-group>
</el-form-item> </el-form-item>
<!-- 下方的登录按钮 --> <!-- 下方的登录按钮 -->
<el-form-item class="w-1/1"> <el-form-item class="w-full">
<el-button <el-button
:loading="formLoading" :loading="formLoading"
class="w-6/10" class="w-3/5"
type="primary" type="primary"
@click.prevent="handleAuthorize(true)" @click.prevent="handleAuthorize(true)"
> >

View File

@ -18,7 +18,6 @@ import { useUserStore } from '@/store/modules/user'
import { useUpload } from '@/components/UploadFile/src/useUpload' import { useUpload } from '@/components/UploadFile/src/useUpload'
import { UploadRequestOptions } from 'element-plus/es/components/upload/src/upload' import { UploadRequestOptions } from 'element-plus/es/components/upload/src/upload'
// TODO @ ProfileUser
defineOptions({ name: 'UserAvatar' }) defineOptions({ name: 'UserAvatar' })
defineProps({ defineProps({
@ -30,10 +29,12 @@ const userStore = useUserStore()
const cropperRef = ref() const cropperRef = ref()
const handelUpload = async ({ data }) => { const handelUpload = async ({ data }) => {
const { httpRequest } = useUpload() const { httpRequest } = useUpload()
const avatar = ((await httpRequest({ const avatar = (
file: data, (await httpRequest({
filename: 'avatar.png', file: data,
} as UploadRequestOptions)) as unknown as { data: string }).data filename: 'avatar.png'
} as UploadRequestOptions)) as unknown as { data: string }
).data
await updateUserProfile({ avatar }) await updateUserProfile({ avatar })
// userStore // userStore

View File

@ -23,7 +23,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { SystemUserSocialTypeEnum } from '@/utils/constants' import { SystemUserSocialTypeEnum } from '@/utils/constants'
import { getUserProfile, ProfileVO } from '@/api/system/user/profile' import { getBindSocialUserList } from '@/api/system/social/user'
import { socialAuthRedirect, socialBind, socialUnbind } from '@/api/system/user/socialUser' import { socialAuthRedirect, socialBind, socialUnbind } from '@/api/system/user/socialUser'
defineOptions({ name: 'UserSocial' }) defineOptions({ name: 'UserSocial' })
@ -32,19 +32,19 @@ defineProps<{
}>() }>()
const message = useMessage() const message = useMessage()
const socialUsers = ref<any[]>([]) const socialUsers = ref<any[]>([])
const userInfo = ref<ProfileVO>()
const initSocial = async () => { const initSocial = async () => {
socialUsers.value = [] // socialUsers.value = [] //
const res = await getUserProfile() //
userInfo.value = res const bindSocialUserList = await getBindSocialUserList()
//
for (const i in SystemUserSocialTypeEnum) { for (const i in SystemUserSocialTypeEnum) {
const socialUser = { ...SystemUserSocialTypeEnum[i] } const socialUser = { ...SystemUserSocialTypeEnum[i] }
socialUsers.value.push(socialUser) socialUsers.value.push(socialUser)
if (userInfo.value?.socialUsers) { if (bindSocialUserList && bindSocialUserList.length > 0) {
for (const j in userInfo.value.socialUsers) { for (const bindUser of bindSocialUserList) {
if (socialUser.type === userInfo.value.socialUsers[j].type) { if (socialUser.type === bindUser.type) {
socialUser.openid = userInfo.value.socialUsers[j].openid socialUser.openid = bindUser.openid
break break
} }
} }

View File

@ -1,9 +1,12 @@
<!-- AI 对话 --> <!-- AI 对话 -->
<template> <template>
<el-aside width="260px" class="conversation-container h-100%"> <el-aside
width="260px"
class="h-100% relative flex flex-col justify-between px-2.5 pt-2.5 pb-0 overflow-hidden"
>
<!-- 左顶部对话 --> <!-- 左顶部对话 -->
<div class="h-100%"> <div class="h-100%">
<el-button class="w-1/1 btn-new-conversation" type="primary" @click="createConversation"> <el-button class="w-1/1 py-4.5" type="primary" @click="createConversation">
<Icon icon="ep:plus" class="mr-5px" /> <Icon icon="ep:plus" class="mr-5px" />
新建对话 新建对话
</el-button> </el-button>
@ -12,7 +15,7 @@
<el-input <el-input
v-model="searchName" v-model="searchName"
size="large" size="large"
class="mt-10px search-input" class="mt-5"
placeholder="搜索历史记录" placeholder="搜索历史记录"
@keyup="searchConversation" @keyup="searchConversation"
> >
@ -22,19 +25,18 @@
</el-input> </el-input>
<!-- 左中间对话列表 --> <!-- 左中间对话列表 -->
<div class="conversation-list"> <div class="overflow-auto h-full">
<!-- 情况一加载中 --> <!-- 情况一加载中 -->
<el-empty v-if="loading" description="." :v-loading="loading" /> <el-empty v-if="loading" description="." :v-loading="loading" />
<!-- 情况二按照 group 分组展示聊天会话 list 列表 --> <!-- 情况二按照 group 分组展示聊天会话 list 列表 -->
<div v-for="conversationKey in Object.keys(conversationMap)" :key="conversationKey"> <div v-for="conversationKey in Object.keys(conversationMap)" :key="conversationKey">
<div <div class="mt-1.25 pt-2.5" v-if="conversationMap[conversationKey].length">
class="conversation-item classify-title" <el-text class="mx-1" size="small" tag="b">
v-if="conversationMap[conversationKey].length" {{ conversationKey }}
> </el-text>
<el-text class="mx-1" size="small" tag="b">{{ conversationKey }}</el-text>
</div> </div>
<div <div
class="conversation-item" class="mt-1.25"
v-for="conversation in conversationMap[conversationKey]" v-for="conversation in conversationMap[conversationKey]"
:key="conversation.id" :key="conversation.id"
@click="handleConversationClick(conversation.id)" @click="handleConversationClick(conversation.id)"
@ -42,25 +44,48 @@
@mouseout="hoverConversationId = ''" @mouseout="hoverConversationId = ''"
> >
<div <div
:class=" class="flex flex-row justify-between flex-1 px-1.25 cursor-pointer rounded-1.25 items-center leading-7.5"
conversation.id === activeConversationId ? 'conversation active' : 'conversation' :style="
conversation.id === activeConversationId
? 'background-color: var(--el-color-primary-light-9); border: 1px solid var(--el-color-primary-light-7);'
: ''
" "
> >
<div class="title-wrapper"> <div class="flex flex-row items-center">
<img class="avatar" :src="conversation.roleAvatar || roleAvatarDefaultImg" /> <img
<span class="title">{{ conversation.title }}</span> class="w-6.25 h-6.25 rounded-1.25 flex flex-row justify-center"
:src="conversation.roleAvatar || roleAvatarDefaultImg"
/>
<span
class="py-0.5 px-2.5"
style="
max-width: 220px;
font-size: 14px;
font-weight: 400;
color: var(--el-text-color-regular);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
"
>
{{ conversation.title }}
</span>
</div> </div>
<div class="button-wrapper" v-show="hoverConversationId === conversation.id"> <div
<el-button class="btn" link @click.stop="handleTop(conversation)"> class="right-0.5 flex flex-row justify-center"
style="color: var(--el-text-color-regular)"
v-show="hoverConversationId === conversation.id"
>
<el-button class="m-0" link @click.stop="handleTop(conversation)">
<el-icon title="置顶" v-if="!conversation.pinned"><Top /></el-icon> <el-icon title="置顶" v-if="!conversation.pinned"><Top /></el-icon>
<el-icon title="置顶" v-if="conversation.pinned"><Bottom /></el-icon> <el-icon title="置顶" v-if="conversation.pinned"><Bottom /></el-icon>
</el-button> </el-button>
<el-button class="btn" link @click.stop="updateConversationTitle(conversation)"> <el-button class="m-0" link @click.stop="updateConversationTitle(conversation)">
<el-icon title="编辑"> <el-icon title="编辑">
<Icon icon="ep:edit" /> <Icon icon="ep:edit" />
</el-icon> </el-icon>
</el-button> </el-button>
<el-button class="btn" link @click.stop="deleteChatConversation(conversation)"> <el-button class="m-0" link @click.stop="deleteChatConversation(conversation)">
<el-icon title="删除对话"> <el-icon title="删除对话">
<Icon icon="ep:delete" /> <Icon icon="ep:delete" />
</el-icon> </el-icon>
@ -75,14 +100,29 @@
</div> </div>
<!-- 左底部工具栏 --> <!-- 左底部工具栏 -->
<div class="tool-box"> <div
<div @click="handleRoleRepository"> class="absolute bottom-0 left-0 right-0 px-5 leading-8.75 flex justify-between items-center"
style="
background-color: var(--el-fill-color-extra-light);
box-shadow: 0 0 1px 1px var(--el-border-color-lighter);
color: var(--el-text-color);
"
>
<div
class="flex items-center p-0 m-0 cursor-pointer"
style="color: var(--el-text-color-regular)"
@click="handleRoleRepository"
>
<Icon icon="ep:user" /> <Icon icon="ep:user" />
<el-text size="small">角色仓库</el-text> <el-text class="ml-1.25" size="small">角色仓库</el-text>
</div> </div>
<div @click="handleClearConversation"> <div
class="flex items-center p-0 m-0 cursor-pointer"
style="color: var(--el-text-color-regular)"
@click="handleClearConversation"
>
<Icon icon="ep:delete" /> <Icon icon="ep:delete" />
<el-text size="small">清空未置顶对话</el-text> <el-text class="ml-1.25" size="small">清空未置顶对话</el-text>
</div> </div>
</div> </div>
@ -193,12 +233,12 @@ const getConversationGroupByCreateTime = async (list: ChatConversationVO[]) => {
// (30) // (30)
// noinspection NonAsciiCharacters // noinspection NonAsciiCharacters
const groupMap = { const groupMap = {
置顶: [], 置顶: [] as ChatConversationVO[],
今天: [], 今天: [] as ChatConversationVO[],
一天前: [], 一天前: [] as ChatConversationVO[],
三天前: [], 三天前: [] as ChatConversationVO[],
七天前: [], 七天前: [] as ChatConversationVO[],
三十天前: [] 三十天前: [] as ChatConversationVO[]
} }
// //
const now = Date.now() const now = Date.now()
@ -349,124 +389,3 @@ onMounted(async () => {
} }
}) })
</script> </script>
<style scoped lang="scss">
.conversation-container {
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 10px 10px 0;
overflow: hidden;
.btn-new-conversation {
padding: 18px 0;
}
.search-input {
margin-top: 20px;
}
.conversation-list {
overflow: auto;
height: 100%;
.classify-title {
padding-top: 10px;
}
.conversation-item {
margin-top: 5px;
}
.conversation {
display: flex;
flex-direction: row;
justify-content: space-between;
flex: 1;
padding: 0 5px;
cursor: pointer;
border-radius: 5px;
align-items: center;
line-height: 30px;
&.active {
background-color: #e6e6e6;
.button {
display: inline-block;
}
}
.title-wrapper {
display: flex;
flex-direction: row;
align-items: center;
}
.title {
padding: 2px 10px;
max-width: 220px;
font-size: 14px;
font-weight: 400;
color: rgba(0, 0, 0, 0.77);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.avatar {
width: 25px;
height: 25px;
border-radius: 5px;
display: flex;
flex-direction: row;
justify-items: center;
}
//
.button-wrapper {
right: 2px;
display: flex;
flex-direction: row;
justify-items: center;
color: #606266;
.btn {
margin: 0;
}
}
}
}
//
.tool-box {
position: absolute;
bottom: 0;
left: 0;
right: 0;
//width: 100%;
padding: 0 20px;
background-color: #f4f4f4;
box-shadow: 0 0 1px 1px rgba(228, 228, 228, 0.8);
line-height: 35px;
display: flex;
justify-content: space-between;
align-items: center;
color: var(--el-text-color);
> div {
display: flex;
align-items: center;
color: #606266;
padding: 0;
margin: 0;
cursor: pointer;
> span {
margin-left: 5px;
}
}
}
}
</style>

View File

@ -1,52 +1,86 @@
<template> <template>
<div ref="messageContainer" class="h-100% overflow-y-auto relative"> <div ref="messageContainer" class="h-100% overflow-y-auto relative">
<div class="chat-list" v-for="(item, index) in list" :key="index"> <div class="flex flex-col overflow-y-hidden px-20px" v-for="(item, index) in list" :key="index">
<!-- 靠左 messagesystemassistant 类型 --> <!-- 靠左 messagesystemassistant 类型 -->
<div class="left-message message-item" v-if="item.type !== 'user'"> <div class="flex flex-row mt-50px" v-if="item.type !== 'user'">
<div class="avatar"> <div class="avatar">
<el-avatar :src="roleAvatar" /> <el-avatar :src="roleAvatar" />
</div> </div>
<div class="message"> <div class="flex flex-col text-left mx-15px">
<div> <div>
<el-text class="time">{{ formatDate(item.createTime) }}</el-text> <el-text class="text-left leading-30px">{{ formatDate(item.createTime) }}</el-text>
</div> </div>
<div class="left-text-container" ref="markdownViewRef"> <div
<MarkdownView class="left-text" :content="item.content" /> class="relative flex flex-col break-words bg-[var(--el-fill-color-light)] shadow-[0_0_0_1px_var(--el-border-color-light)] rounded-10px pt-10px px-10px pb-5px"
ref="markdownViewRef"
>
<MarkdownView
class="text-[var(--el-text-color-primary)] text-[0.95rem]"
:content="item.content"
/>
<MessageKnowledge v-if="item.segments" :segments="item.segments" /> <MessageKnowledge v-if="item.segments" :segments="item.segments" />
</div> </div>
<div class="left-btns"> <div class="flex flex-row mt-8px">
<el-button class="btn-cus" link @click="copyContent(item.content)"> <el-button
<img class="btn-image" src="@/assets/ai/copy.svg" /> class="flex bg-transparent items-center hover:cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
link
@click="copyContent(item.content)"
>
<img class="h-20px" src="@/assets/ai/copy.svg" />
</el-button> </el-button>
<el-button v-if="item.id > 0" class="btn-cus" link @click="onDelete(item.id)"> <el-button
<img class="btn-image h-17px" src="@/assets/ai/delete.svg" /> v-if="item.id > 0"
class="flex bg-transparent items-center hover:cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
link
@click="onDelete(item.id)"
>
<img class="h-17px" src="@/assets/ai/delete.svg" />
</el-button> </el-button>
</div> </div>
</div> </div>
</div> </div>
<!-- 靠右 messageuser 类型 --> <!-- 靠右 messageuser 类型 -->
<div class="right-message message-item" v-if="item.type === 'user'"> <div class="flex flex-row-reverse justify-start mt-50px" v-if="item.type === 'user'">
<div class="avatar"> <div class="avatar">
<el-avatar :src="userAvatar" /> <el-avatar :src="userAvatar" />
</div> </div>
<div class="message"> <div class="flex flex-col text-left mx-15px">
<div> <div>
<el-text class="time">{{ formatDate(item.createTime) }}</el-text> <el-text class="text-left leading-30px">{{ formatDate(item.createTime) }}</el-text>
</div> </div>
<div class="right-text-container"> <div class="flex flex-row-reverse">
<div class="right-text">{{ item.content }}</div> <div
class="text-[0.95rem] text-[var(--el-color-white)] inline bg-[var(--el-color-primary)] shadow-[0_0_0_1px_var(--el-color-primary)] rounded-10px p-10px w-auto break-words whitespace-pre-wrap"
>{{ item.content }}</div
>
</div> </div>
<div class="right-btns"> <div class="flex flex-row-reverse mt-8px">
<el-button class="btn-cus" link @click="copyContent(item.content)"> <el-button
<img class="btn-image" src="@/assets/ai/copy.svg" /> class="flex bg-transparent items-center hover:cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
link
@click="copyContent(item.content)"
>
<img class="h-20px" src="@/assets/ai/copy.svg" />
</el-button> </el-button>
<el-button class="btn-cus" link @click="onDelete(item.id)"> <el-button
<img class="btn-image h-17px mr-12px" src="@/assets/ai/delete.svg" /> class="flex bg-transparent items-center hover:cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
link
@click="onDelete(item.id)"
>
<img class="h-17px mr-12px" src="@/assets/ai/delete.svg" />
</el-button> </el-button>
<el-button class="btn-cus" link @click="onRefresh(item)"> <el-button
class="flex bg-transparent items-center hover:cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
link
@click="onRefresh(item)"
>
<el-icon size="17"><RefreshRight /></el-icon> <el-icon size="17"><RefreshRight /></el-icon>
</el-button> </el-button>
<el-button class="btn-cus" link @click="onEdit(item)"> <el-button
class="flex bg-transparent items-center hover:cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
link
@click="onEdit(item)"
>
<el-icon size="17"><Edit /></el-icon> <el-icon size="17"><Edit /></el-icon>
</el-button> </el-button>
</div> </div>
@ -55,7 +89,7 @@
</div> </div>
</div> </div>
<!-- 回到底部 --> <!-- 回到底部 -->
<div v-if="isScrolling" class="to-bottom" @click="handleGoBottom"> <div v-if="isScrolling" class="absolute z-1000 bottom-0 right-50%" @click="handleGoBottom">
<el-button :icon="ArrowDownBold" circle /> <el-button :icon="ArrowDownBold" circle />
</div> </div>
</template> </template>
@ -142,7 +176,7 @@ defineExpose({ scrollToBottom, handlerGoTop }) // 提供方法给 parent 调用
// ============ ============== // ============ ==============
/** 复制 */ /** 复制 */
const copyContent = async (content) => { const copyContent = async (content: string) => {
await copy(content) await copy(content)
message.success('复制成功!') message.success('复制成功!')
} }
@ -171,114 +205,3 @@ onMounted(async () => {
messageContainer.value.addEventListener('scroll', handleScroll) messageContainer.value.addEventListener('scroll', handleScroll)
}) })
</script> </script>
<style scoped lang="scss">
.message-container {
position: relative;
overflow-y: scroll;
}
//
.chat-list {
display: flex;
flex-direction: column;
overflow-y: hidden;
padding: 0 20px;
.message-item {
margin-top: 50px;
}
.left-message {
display: flex;
flex-direction: row;
}
.right-message {
display: flex;
flex-direction: row-reverse;
justify-content: flex-start;
}
.message {
display: flex;
flex-direction: column;
text-align: left;
margin: 0 15px;
.time {
text-align: left;
line-height: 30px;
}
.left-text-container {
position: relative;
display: flex;
flex-direction: column;
overflow-wrap: break-word;
background-color: rgba(228, 228, 228, 0.8);
box-shadow: 0 0 0 1px rgba(228, 228, 228, 0.8);
border-radius: 10px;
padding: 10px 10px 5px 10px;
.left-text {
color: #393939;
font-size: 0.95rem;
}
}
.right-text-container {
display: flex;
flex-direction: row-reverse;
.right-text {
font-size: 0.95rem;
color: #fff;
display: inline;
background-color: #267fff;
box-shadow: 0 0 0 1px #267fff;
border-radius: 10px;
padding: 10px;
width: auto;
overflow-wrap: break-word;
white-space: pre-wrap;
}
}
.left-btns {
display: flex;
flex-direction: row;
margin-top: 8px;
}
.right-btns {
display: flex;
flex-direction: row-reverse;
margin-top: 8px;
}
}
//
.btn-cus {
display: flex;
background-color: transparent;
align-items: center;
.btn-image {
height: 20px;
}
}
.btn-cus:hover {
cursor: pointer;
background-color: #f6f6f6;
}
}
//
.to-bottom {
position: absolute;
z-index: 1000;
bottom: 0;
right: 50%;
}
</style>

View File

@ -1,12 +1,12 @@
<!-- 消息列表为空时展示 prompt 列表 --> <!-- 消息列表为空时展示 prompt 列表 -->
<template> <template>
<div class="chat-empty"> <div class="relative flex flex-row justify-center w-full h-full">
<!-- title --> <!-- title -->
<div class="center-container"> <div class="flex flex-col justify-center">
<div class="title">芋道 AI</div> <div class="text-28px font-bold text-center">芋道 AI</div>
<div class="role-list"> <div class="flex flex-row flex-wrap items-center justify-center w-460px mt-20px">
<div <div
class="role-item" class="flex justify-center w-180px leading-50px border border-solid border-[#e4e4e4] rounded-10px m-10px cursor-pointer hover:bg-[rgba(243,243,243,0.73)]"
v-for="prompt in promptList" v-for="prompt in promptList"
:key="prompt.prompt" :key="prompt.prompt"
@click="handlerPromptClick(prompt)" @click="handlerPromptClick(prompt)"
@ -34,50 +34,3 @@ const handlerPromptClick = async ({ prompt }) => {
emits('onPrompt', prompt) emits('onPrompt', prompt)
} }
</script> </script>
<style scoped lang="scss">
.chat-empty {
position: relative;
display: flex;
flex-direction: row;
justify-content: center;
width: 100%;
height: 100%;
.center-container {
display: flex;
flex-direction: column;
justify-content: center;
.title {
font-size: 28px;
font-weight: bold;
text-align: center;
}
.role-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: center;
width: 460px;
margin-top: 20px;
.role-item {
display: flex;
justify-content: center;
width: 180px;
line-height: 50px;
border: 1px solid #e4e4e4;
border-radius: 10px;
margin: 10px;
cursor: pointer;
}
.role-item:hover {
background-color: rgba(243, 243, 243, 0.73);
}
}
}
}
</style>

View File

@ -1,15 +1,6 @@
<!-- message 加载页面 --> <!-- message 加载页面 -->
<template> <template>
<div class="message-loading" > <div class="p-30px">
<el-skeleton animated /> <el-skeleton animated />
</div> </div>
</template> </template>
<script setup lang="ts">
</script>
<style scoped lang="scss">
.message-loading {
padding: 30px 30px;
}
</style>

View File

@ -1,9 +1,9 @@
<!-- 无聊天对话时 message 区域可以新增对话 --> <!-- 无聊天对话时 message 区域可以新增对话 -->
<template> <template>
<div class="new-chat"> <div class="flex flex-row justify-center w-100% h-100%">
<div class="box-center"> <div class="flex flex-col justify-center">
<div class="tip">点击下方按钮开始你的对话吧</div> <div class="text-14px text-#858585">点击下方按钮开始你的对话吧</div>
<div class="btns"> <div class="flex flex-row justify-center mt-20px">
<el-button type="primary" round @click="handlerNewChat"></el-button> <el-button type="primary" round @click="handlerNewChat"></el-button>
</div> </div>
</div> </div>
@ -17,30 +17,3 @@ const handlerNewChat = () => {
emits('onNewConversation') emits('onNewConversation')
} }
</script> </script>
<style scoped lang="scss">
.new-chat {
display: flex;
flex-direction: row;
justify-content: center;
width: 100%;
height: 100%;
.box-center {
display: flex;
flex-direction: column;
justify-content: center;
.tip {
font-size: 14px;
color: #858585;
}
.btns {
display: flex;
flex-direction: row;
justify-content: center;
margin-top: 20px;
}
}
}
</style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="category-list"> <div class="flex flex-row flex-wrap items-center">
<div class="category" v-for="category in categoryList" :key="category"> <div class="flex flex-row mr-10px" v-for="category in categoryList" :key="category">
<el-button <el-button
plain plain
round round
@ -37,17 +37,3 @@ const handleCategoryClick = async (category: string) => {
emits('onCategoryClick', category) emits('onCategoryClick', category)
} }
</script> </script>
<style scoped lang="scss">
.category-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
.category {
display: flex;
flex-direction: row;
margin-right: 10px;
}
}
</style>

View File

@ -1,10 +1,10 @@
<!-- header --> <!-- header -->
<template> <template>
<el-header class="chat-header"> <el-header class="flex flex-row justify-between items-center px-10px whitespace-nowrap text-ellipsis w-full" :style="{ backgroundColor: 'var(--el-bg-color-page)' }">
<div class="title"> <div class="text-20px font-bold overflow-hidden max-w-220px" :style="{ color: 'var(--el-text-color-primary)' }">
{{ title }} {{ title }}
</div> </div>
<div class="title-right"> <div class="flex flex-row">
<slot></slot> <slot></slot>
</div> </div>
</el-header> </el-header>
@ -19,30 +19,3 @@ defineProps({
} }
}) })
</script> </script>
<style scoped lang="scss">
.chat-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0 10px;
white-space: nowrap;
text-overflow: ellipsis;
background-color: #ececec;
width: 100%;
.title {
font-size: 20px;
font-weight: bold;
overflow: hidden;
color: #3e3e3e;
max-width: 220px;
}
.title-right {
display: flex;
flex-direction: row;
}
}
</style>

View File

@ -1,9 +1,16 @@
<template> <template>
<div class="card-list" ref="tabsRef" @scroll="handleTabsScroll"> <div
<div class="card-item" v-for="role in roleList" :key="role.id"> class="flex flex-row flex-wrap relative h-full overflow-auto px-25px pb-140px items-start content-start justify-start"
<el-card class="card" body-class="card-body"> ref="tabsRef"
@scroll="handleTabsScroll"
>
<div v-for="role in roleList" :key="role.id">
<el-card
class="inline-block mr-20px rounded-10px mb-20px relative"
body-class="max-w-240px w-240px pt-15px px-15px pb-10px flex flex-row justify-start relative"
>
<!-- 更多操作 --> <!-- 更多操作 -->
<div class="more-container" v-if="showMore"> <div class="absolute top-0 right-12px" v-if="showMore">
<el-dropdown @command="handleMoreClick"> <el-dropdown @command="handleMoreClick">
<span class="el-dropdown-link"> <span class="el-dropdown-link">
<el-button type="text"> <el-button type="text">
@ -13,10 +20,10 @@
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item :command="['edit', role]"> <el-dropdown-item :command="['edit', role]">
<Icon icon="ep:edit" color="#787878" />编辑 <Icon icon="ep:edit" color="var(--el-text-color-placeholder)" />编辑
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item :command="['delete', role]" style="color: red"> <el-dropdown-item :command="['delete', role]" style="color: var(--el-color-danger)">
<Icon icon="ep:delete" color="red" />删除 <Icon icon="ep:delete" color="var(--el-color-danger)" />删除
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
@ -24,14 +31,18 @@
</div> </div>
<!-- 角色信息 --> <!-- 角色信息 -->
<div> <div>
<img class="avatar" :src="role.avatar" /> <img class="w-40px h-40px rounded-10px overflow-hidden" :src="role.avatar" />
</div> </div>
<div class="right-container"> <div class="ml-10px w-full">
<div class="content-container"> <div class="h-85px">
<div class="title">{{ role.name }}</div> <div class="text-18px font-bold" style="color: var(--el-text-color-primary)">
<div class="description">{{ role.description }}</div> {{ role.name }}
</div>
<div class="mt-10px text-14px" style="color: var(--el-text-color-regular)">
{{ role.description }}
</div>
</div> </div>
<div class="btn-container"> <div class="flex flex-row-reverse mt-2px">
<el-button type="primary" size="small" @click="handleUseClick(role)">使</el-button> <el-button type="primary" size="small" @click="handleUseClick(role)">使</el-button>
</div> </div>
</div> </div>
@ -79,7 +90,7 @@ const handleMoreClick = async (data) => {
} }
/** 选中 */ /** 选中 */
const handleUseClick = (role) => { const handleUseClick = (role: any) => {
emits('onUse', role) emits('onUse', role)
} }
@ -88,87 +99,8 @@ const handleTabsScroll = async () => {
if (tabsRef.value) { if (tabsRef.value) {
const { scrollTop, scrollHeight, clientHeight } = tabsRef.value const { scrollTop, scrollHeight, clientHeight } = tabsRef.value
if (scrollTop + clientHeight >= scrollHeight - 20 && !props.loading) { if (scrollTop + clientHeight >= scrollHeight - 20 && !props.loading) {
await emits('onPage') emits('onPage')
} }
} }
} }
</script> </script>
<style lang="scss">
// card body
.card-body {
max-width: 240px;
width: 240px;
padding: 15px 15px 10px 15px;
display: flex;
flex-direction: row;
justify-content: flex-start;
position: relative;
}
</style>
<style scoped lang="scss">
//
.card-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
position: relative;
height: 100%;
overflow: auto;
padding: 0px 25px;
padding-bottom: 140px;
align-items: start;
align-content: flex-start;
justify-content: start;
.card {
display: inline-block;
margin-right: 20px;
border-radius: 10px;
margin-bottom: 20px;
position: relative;
.more-container {
position: absolute;
top: 0;
right: 12px;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 10px;
overflow: hidden;
}
.right-container {
margin-left: 10px;
width: 100%;
//height: 100px;
.content-container {
height: 85px;
.title {
font-size: 18px;
font-weight: bold;
color: #3e3e3e;
}
.description {
margin-top: 10px;
font-size: 14px;
color: #6a6a6a;
}
}
.btn-container {
display: flex;
flex-direction: row-reverse;
margin-top: 2px;
}
}
}
}
</style>

View File

@ -1,17 +1,19 @@
<!-- chat 角色仓库 --> <!-- chat 角色仓库 -->
<template> <template>
<el-container class="role-container"> <el-container
class="role-container absolute w-full h-full m-0 p-0 left-0 right-0 top-0 bottom-0 bg-[var(--el-bg-color)] overflow-hidden flex !flex-col"
>
<ChatRoleForm ref="formRef" @success="handlerAddRoleSuccess" /> <ChatRoleForm ref="formRef" @success="handlerAddRoleSuccess" />
<!-- header --> <!-- header -->
<RoleHeader title="角色仓库" class="relative" /> <RoleHeader title="角色仓库" class="relative" />
<!-- main --> <!-- main -->
<el-main class="role-main"> <el-main class="flex-1 overflow-hidden m-0 !p-0 relative">
<div class="search-container"> <div class="mx-5 mt-5 mb-0 absolute right-0 -top-1.25 z-100">
<!-- 搜索按钮 --> <!-- 搜索按钮 -->
<el-input <el-input
:loading="loading" :loading="loading"
v-model="search" v-model="search"
class="search-input" class="!w-60"
size="default" size="default"
placeholder="请输入搜索的内容" placeholder="请输入搜索的内容"
:suffix-icon="Search" :suffix-icon="Search"
@ -23,13 +25,21 @@
@click="handlerAddRole" @click="handlerAddRole"
class="ml-20px" class="ml-20px"
> >
<Icon icon="ep:user" style="margin-right: 5px;" /> <Icon icon="ep:user" class="mr-1.25" />
添加角色 添加角色
</el-button> </el-button>
</div> </div>
<!-- tabs --> <!-- tabs -->
<el-tabs v-model="activeTab" class="tabs" @tab-click="handleTabsClick"> <el-tabs
<el-tab-pane class="role-pane" label="我的角色" name="my-role"> v-model="activeTab"
@tab-click="handleTabsClick"
class="relative h-full [&_.el-tabs__nav-scroll]:my-2.5 [&_.el-tabs__nav-scroll]:mx-5"
>
<el-tab-pane
label="我的角色"
name="my-role"
class="flex flex-col h-full overflow-y-auto relative"
>
<RoleList <RoleList
:loading="loading" :loading="loading"
:role-list="myRoleList" :role-list="myRoleList"
@ -43,7 +53,7 @@
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="公共角色" name="public-role"> <el-tab-pane label="公共角色" name="public-role">
<RoleCategoryList <RoleCategoryList
class="role-category-list" class="mx-6.75"
:category-list="categoryList" :category-list="categoryList"
:active="activeCategory" :active="activeCategory"
@on-category-click="handlerCategoryClick" @on-category-click="handlerCategoryClick"
@ -64,15 +74,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref} from 'vue' import { ref } from 'vue'
import RoleHeader from './RoleHeader.vue' import RoleHeader from './RoleHeader.vue'
import RoleList from './RoleList.vue' import RoleList from './RoleList.vue'
import ChatRoleForm from '@/views/ai/model/chatRole/ChatRoleForm.vue' import ChatRoleForm from '@/views/ai/model/chatRole/ChatRoleForm.vue'
import RoleCategoryList from './RoleCategoryList.vue' import RoleCategoryList from './RoleCategoryList.vue'
import {ChatRoleApi, ChatRolePageReqVO, ChatRoleVO} from '@/api/ai/model/chatRole' import { ChatRoleApi, ChatRolePageReqVO, ChatRoleVO } from '@/api/ai/model/chatRole'
import {ChatConversationApi, ChatConversationVO} from '@/api/ai/chat/conversation' import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
import {Search} from '@element-plus/icons-vue' import { Search } from '@element-plus/icons-vue'
import {TabsPaneContext} from 'element-plus' import { TabsPaneContext } from 'element-plus'
const router = useRouter() // const router = useRouter() //
@ -220,70 +230,9 @@ onMounted(async () => {
await getActiveTabsRole() await getActiveTabsRole()
}) })
</script> </script>
<!-- 覆盖 element ui css --> <!-- 覆盖 element plus css -->
<style lang="scss"> <style lang="scss">
.el-tabs__content {
position: relative;
height: 100%;
overflow: hidden;
}
.el-tabs__nav-scroll { .el-tabs__nav-scroll {
margin: 10px 20px; margin: 10px 20px;
} }
</style> </style>
<!-- 样式 -->
<style scoped lang="scss">
//
.role-container {
position: absolute;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: #ffffff;
overflow: hidden;
display: flex;
flex-direction: column;
.role-main {
flex: 1;
overflow: hidden;
margin: 0;
padding: 0;
position: relative;
.search-container {
margin: 20px 20px 0px 20px;
position: absolute;
right: 0;
top: -5px;
z-index: 100;
}
.search-input {
width: 240px;
}
.tabs {
position: relative;
height: 100%;
.role-category-list {
margin: 0 27px;
}
}
.role-pane {
display: flex;
flex-direction: column;
height: 100%;
overflow-y: auto;
position: relative;
}
}
}
</style>

View File

@ -1,5 +1,5 @@
<template> <template>
<el-container class="ai-layout"> <el-container class="absolute flex-1 top-0 left-0 h-full w-full">
<!-- 左侧对话列表 --> <!-- 左侧对话列表 -->
<ConversationList <ConversationList
:active-id="activeConversationId" :active-id="activeConversationId"
@ -10,33 +10,38 @@
@on-conversation-delete="handlerConversationDelete" @on-conversation-delete="handlerConversationDelete"
/> />
<!-- 右侧对话详情 --> <!-- 右侧对话详情 -->
<el-container class="detail-container"> <el-container class="bg-[var(--el-bg-color)]">
<el-header class="header"> <el-header
<div class="title"> class="flex flex-row items-center justify-between bg-[var(--el-bg-color-page)] shadow-[0_0_0_0_var(--el-border-color-light)]"
>
<div class="text-18px font-bold">
{{ activeConversation?.title ? activeConversation?.title : '对话' }} {{ activeConversation?.title ? activeConversation?.title : '对话' }}
<span v-if="activeMessageList.length">({{ activeMessageList.length }})</span> <span v-if="activeMessageList.length">({{ activeMessageList.length }})</span>
</div> </div>
<div class="btns" v-if="activeConversation"> <div class="flex w-300px flex-row justify-end" v-if="activeConversation">
<el-button type="primary" bg plain size="small" @click="openChatConversationUpdateForm"> <el-button type="primary" bg plain size="small" @click="openChatConversationUpdateForm">
<span v-html="activeConversation?.modelName"></span> <span v-html="activeConversation?.modelName"></span>
<Icon icon="ep:setting" class="ml-10px" /> <Icon icon="ep:setting" class="ml-10px" />
</el-button> </el-button>
<el-button size="small" class="btn" @click="handlerMessageClear"> <el-button size="small" class="p-10px" @click="handlerMessageClear">
<Icon icon="heroicons-outline:archive-box-x-mark" color="#787878" /> <Icon
icon="heroicons-outline:archive-box-x-mark"
color="var(--el-text-color-placeholder)"
/>
</el-button> </el-button>
<el-button size="small" class="btn"> <el-button size="small" class="p-10px">
<Icon icon="ep:download" color="#787878" /> <Icon icon="ep:download" color="var(--el-text-color-placeholder)" />
</el-button> </el-button>
<el-button size="small" class="btn" @click="handleGoTopMessage"> <el-button size="small" class="p-10px" @click="handleGoTopMessage">
<Icon icon="ep:top" color="#787878" /> <Icon icon="ep:top" color="var(--el-text-color-placeholder)" />
</el-button> </el-button>
</div> </div>
</el-header> </el-header>
<!-- main消息列表 --> <!-- main消息列表 -->
<el-main class="main-container"> <el-main class="m-0 p-0 relative h-full w-full">
<div> <div>
<div class="message-container"> <div class="absolute top-0 bottom-0 left-0 right-0 overflow-y-hidden p-0 m-0">
<!-- 情况一消息加载中 --> <!-- 情况一消息加载中 -->
<MessageLoading v-if="activeMessageListLoading" /> <MessageLoading v-if="activeMessageListLoading" />
<!-- 情况二无聊天对话时 --> <!-- 情况二无聊天对话时 -->
@ -64,10 +69,14 @@
</el-main> </el-main>
<!-- 底部 --> <!-- 底部 -->
<el-footer class="footer-container"> <el-footer class="flex flex-col !h-auto !p-0">
<form class="prompt-from"> <!-- TODO @芋艿这块要想办法迁移下 -->
<form
class="mt-10px mx-20px mb-20px py-9px px-10px flex flex-col h-auto rounded-10px"
style="border: 1px solid var(--el-border-color)"
>
<textarea <textarea
class="prompt-input" class="h-80px border-none box-border resize-none py-0 px-2px overflow-auto focus:outline-none"
v-model="prompt" v-model="prompt"
@keydown="handleSendByKeydown" @keydown="handleSendByKeydown"
@input="handlePromptInput" @input="handlePromptInput"
@ -75,7 +84,7 @@
@compositionend="onCompositionend" @compositionend="onCompositionend"
placeholder="问我任何问题...Shift+Enter 换行,按下 Enter 发送)" placeholder="问我任何问题...Shift+Enter 换行,按下 Enter 发送)"
></textarea> ></textarea>
<div class="prompt-btns"> <div class="flex justify-between pb-0 pt-5px">
<div> <div>
<el-switch v-model="enableContext" /> <el-switch v-model="enableContext" />
<span class="ml-5px text-14px text-#8f8f8f">上下文</span> <span class="ml-5px text-14px text-#8f8f8f">上下文</span>
@ -571,204 +580,3 @@ onMounted(async () => {
await getMessageList() await getMessageList()
}) })
</script> </script>
<style lang="scss" scoped>
.ai-layout {
position: absolute;
flex: 1;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
.conversation-container {
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 10px 10px 0;
.btn-new-conversation {
padding: 18px 0;
}
.search-input {
margin-top: 20px;
}
.conversation-list {
margin-top: 20px;
.conversation {
display: flex;
flex-direction: row;
justify-content: space-between;
flex: 1;
padding: 0 5px;
margin-top: 10px;
cursor: pointer;
border-radius: 5px;
align-items: center;
line-height: 30px;
&.active {
background-color: #e6e6e6;
.button {
display: inline-block;
}
}
.title-wrapper {
display: flex;
flex-direction: row;
align-items: center;
}
.title {
padding: 5px 10px;
max-width: 220px;
font-size: 14px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.avatar {
width: 28px;
height: 28px;
display: flex;
flex-direction: row;
justify-items: center;
}
//
.button-wrapper {
right: 2px;
display: flex;
flex-direction: row;
justify-items: center;
color: #606266;
.el-icon {
margin-right: 5px;
}
}
}
}
//
.tool-box {
line-height: 35px;
display: flex;
justify-content: space-between;
align-items: center;
color: var(--el-text-color);
> div {
display: flex;
align-items: center;
color: #606266;
padding: 0;
margin: 0;
cursor: pointer;
> span {
margin-left: 5px;
}
}
}
}
//
.detail-container {
background: #ffffff;
.header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
background: #fbfbfb;
box-shadow: 0 0 0 0 #dcdfe6;
.title {
font-size: 18px;
font-weight: bold;
}
.btns {
display: flex;
width: 300px;
flex-direction: row;
justify-content: flex-end;
//justify-content: space-between;
.btn {
padding: 10px;
}
}
}
}
// main
.main-container {
margin: 0;
padding: 0;
position: relative;
height: 100%;
width: 100%;
.message-container {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow-y: hidden;
padding: 0;
margin: 0;
}
}
//
.footer-container {
display: flex;
flex-direction: column;
height: auto;
margin: 0;
padding: 0;
.prompt-from {
display: flex;
flex-direction: column;
height: auto;
border: 1px solid #e3e3e3;
border-radius: 10px;
margin: 10px 20px 20px 20px;
padding: 9px 10px;
}
.prompt-input {
height: 80px;
//box-shadow: none;
border: none;
box-sizing: border-box;
resize: none;
padding: 0 2px;
overflow: auto;
}
.prompt-input:focus {
outline: none;
}
.prompt-btns {
display: flex;
justify-content: space-between;
padding-bottom: 0;
padding-top: 5px;
}
}
</style>

View File

@ -1,6 +1,9 @@
<template> <template>
<el-card body-class="" class="image-card"> <el-card
<div class="image-operation"> body-class=""
class="!w-80 !h-auto !rounded-10px !relative !flex !flex-col"
>
<div class="!flex !flex-row !justify-between">
<div> <div>
<el-button type="primary" text bg v-if="detail?.status === AiImageStatusEnum.IN_PROGRESS"> <el-button type="primary" text bg v-if="detail?.status === AiImageStatusEnum.IN_PROGRESS">
生成中 生成中
@ -15,24 +18,34 @@
<!-- 操作区 --> <!-- 操作区 -->
<div> <div>
<el-button <el-button
class="btn" class="!p-10px !m-0"
text text
:icon="Download" :icon="Download"
@click="handleButtonClick('download', detail)" @click="handleButtonClick('download', detail)"
/> />
<el-button <el-button
class="btn" class="!p-10px !m-0"
text text
:icon="RefreshRight" :icon="RefreshRight"
@click="handleButtonClick('regeneration', detail)" @click="handleButtonClick('regeneration', detail)"
/> />
<el-button class="btn" text :icon="Delete" @click="handleButtonClick('delete', detail)" /> <el-button
<el-button class="btn" text :icon="More" @click="handleButtonClick('more', detail)" /> class="!p-10px !m-0"
text
:icon="Delete"
@click="handleButtonClick('delete', detail)"
/>
<el-button
class="!p-10px !m-0"
text
:icon="More"
@click="handleButtonClick('more', detail)"
/>
</div> </div>
</div> </div>
<div class="image-wrapper" ref="cardImageRef"> <div class="!overflow-hidden !mt-20px !h-280px !flex-1" ref="cardImageRef">
<el-image <el-image
class="image" class="!w-full !rounded-10px"
:src="detail?.picUrl" :src="detail?.picUrl"
:preview-src-list="[detail.picUrl]" :preview-src-list="[detail.picUrl]"
preview-teleported preview-teleported
@ -42,7 +55,7 @@
</div> </div>
</div> </div>
<!-- Midjourney 专属操作 --> <!-- Midjourney 专属操作 -->
<div class="image-mj-btns"> <div class="!mt-5px !w-full !flex !flex-row !flex-wrap !justify-start">
<el-button <el-button
size="small" size="small"
v-for="button in detail?.buttons" v-for="button in detail?.buttons"
@ -116,47 +129,3 @@ onMounted(async () => {
await handleLoading(props.detail.status as string) await handleLoading(props.detail.status as string)
}) })
</script> </script>
<style scoped lang="scss">
.image-card {
width: 320px;
height: auto;
border-radius: 10px;
position: relative;
display: flex;
flex-direction: column;
.image-operation {
display: flex;
flex-direction: row;
justify-content: space-between;
.btn {
//border: 1px solid red;
padding: 10px;
margin: 0;
}
}
.image-wrapper {
overflow: hidden;
margin-top: 20px;
height: 280px;
flex: 1;
.image {
width: 100%;
border-radius: 10px;
}
}
.image-mj-btns {
margin-top: 5px;
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
}
}
</style>

View File

@ -5,139 +5,111 @@
@close="handleDrawerClose" @close="handleDrawerClose"
custom-class="drawer-class" custom-class="drawer-class"
> >
<!-- 图片 --> <!-- 图片预览 -->
<div class="item"> <div class="mb-5">
<div class="body"> <el-image
<el-image :src="detail?.picUrl"
class="image" :preview-src-list="[detail.picUrl]"
:src="detail?.picUrl" preview-teleported
:preview-src-list="[detail.picUrl]" class="w-full rounded-2"
preview-teleported fit="contain"
/> />
</div>
</div>
<!-- 时间 -->
<div class="item">
<div class="tip">时间</div>
<div class="body">
<div>提交时间{{ formatTime(detail.createTime, 'yyyy-MM-dd HH:mm:ss') }}</div>
<div>生成时间{{ formatTime(detail.finishTime, 'yyyy-MM-dd HH:mm:ss') }}</div>
</div>
</div>
<!-- 模型 -->
<div class="item">
<div class="tip">模型</div>
<div class="body"> {{ detail.model }}({{ detail.height }}x{{ detail.width }}) </div>
</div>
<!-- 提示词 -->
<div class="item">
<div class="tip">提示词</div>
<div class="body">
{{ detail.prompt }}
</div>
</div>
<!-- 地址 -->
<div class="item">
<div class="tip">图片地址</div>
<div class="body">
{{ detail.picUrl }}
</div>
</div> </div>
<!-- 基础信息 -->
<el-descriptions title="基础信息" :column="1" :label-width="100" border>
<el-descriptions-item label="提交时间">
{{ formatTime(detail.createTime, 'yyyy-MM-dd HH:mm:ss') }}
</el-descriptions-item>
<el-descriptions-item label="生成时间">
{{ formatTime(detail.finishTime, 'yyyy-MM-dd HH:mm:ss') }}
</el-descriptions-item>
<el-descriptions-item label="模型">
{{ detail.model }}({{ detail.height }}x{{ detail.width }})
</el-descriptions-item>
<el-descriptions-item label="提示词">
<div class="break-words">{{ detail.prompt }}</div>
</el-descriptions-item>
<el-descriptions-item label="图片地址">
<div class="break-all text-xs">{{ detail.picUrl }}</div>
</el-descriptions-item>
</el-descriptions>
<!-- StableDiffusion 专属区域 --> <!-- StableDiffusion 专属区域 -->
<div <el-descriptions
class="item" v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && hasStableDiffusionOptions"
v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.sampler" title="StableDiffusion 参数"
:column="1"
:label-width="100"
border
class="mt-5"
> >
<div class="tip">采样方法</div> <el-descriptions-item v-if="detail?.options?.sampler" label="采样方法">
<div class="body">
{{ {{
StableDiffusionSamplers.find( StableDiffusionSamplers.find(
(item: ImageModelVO) => item.key === detail?.options?.sampler (item: ImageModelVO) => item.key === detail?.options?.sampler
)?.name )?.name
}} }}
</div> </el-descriptions-item>
</div> <el-descriptions-item v-if="detail?.options?.clipGuidancePreset" label="CLIP">
<div
class="item"
v-if="
detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.clipGuidancePreset
"
>
<div class="tip">CLIP</div>
<div class="body">
{{ {{
StableDiffusionClipGuidancePresets.find( StableDiffusionClipGuidancePresets.find(
(item: ImageModelVO) => item.key === detail?.options?.clipGuidancePreset (item: ImageModelVO) => item.key === detail?.options?.clipGuidancePreset
)?.name )?.name
}} }}
</div> </el-descriptions-item>
</div> <el-descriptions-item v-if="detail?.options?.stylePreset" label="风格">
<div
class="item"
v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.stylePreset"
>
<div class="tip">风格</div>
<div class="body">
{{ {{
StableDiffusionStylePresets.find( StableDiffusionStylePresets.find(
(item: ImageModelVO) => item.key === detail?.options?.stylePreset (item: ImageModelVO) => item.key === detail?.options?.stylePreset
)?.name )?.name
}} }}
</div> </el-descriptions-item>
</div> <el-descriptions-item v-if="detail?.options?.steps" label="迭代步数">
<div
class="item"
v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.steps"
>
<div class="tip">迭代步数</div>
<div class="body">
{{ detail?.options?.steps }} {{ detail?.options?.steps }}
</div> </el-descriptions-item>
</div> <el-descriptions-item v-if="detail?.options?.scale" label="引导系数">
<div
class="item"
v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.scale"
>
<div class="tip">引导系数</div>
<div class="body">
{{ detail?.options?.scale }} {{ detail?.options?.scale }}
</div> </el-descriptions-item>
</div> <el-descriptions-item v-if="detail?.options?.seed" label="随机因子">
<div
class="item"
v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.seed"
>
<div class="tip">随机因子</div>
<div class="body">
{{ detail?.options?.seed }} {{ detail?.options?.seed }}
</div> </el-descriptions-item>
</div> </el-descriptions>
<!-- Dall3 专属区域 --> <!-- Dall3 专属区域 -->
<div class="item" v-if="detail.platform === AiPlatformEnum.OPENAI && detail?.options?.style"> <el-descriptions
<div class="tip">风格选择</div> v-if="detail.platform === AiPlatformEnum.OPENAI && detail?.options?.style"
<div class="body"> title="DALL-E 3 参数"
:column="1"
:label-width="100"
border
class="mt-5"
>
<el-descriptions-item label="风格选择">
{{ Dall3StyleList.find((item: ImageModelVO) => item.key === detail?.options?.style)?.name }} {{ Dall3StyleList.find((item: ImageModelVO) => item.key === detail?.options?.style)?.name }}
</div> </el-descriptions-item>
</div> </el-descriptions>
<!-- Midjourney 专属区域 --> <!-- Midjourney 专属区域 -->
<div <el-descriptions
class="item" v-if="detail.platform === AiPlatformEnum.MIDJOURNEY && hasMidjourneyOptions"
v-if="detail.platform === AiPlatformEnum.MIDJOURNEY && detail?.options?.version" title="Midjourney 参数"
:column="1"
:label-width="100"
border
class="mt-5"
> >
<div class="tip">模型版本</div> <el-descriptions-item v-if="detail?.options?.version" label="模型版本">
<div class="body">
{{ detail?.options?.version }} {{ detail?.options?.version }}
</div> </el-descriptions-item>
</div> <el-descriptions-item v-if="detail?.options?.referImageUrl" label="参考图">
<div <el-image
class="item" :src="detail.options.referImageUrl"
v-if="detail.platform === AiPlatformEnum.MIDJOURNEY && detail?.options?.referImageUrl" class="max-w-[200px] rounded-2"
> fit="contain"
<div class="tip">参考图</div> />
<div class="body"> </el-descriptions-item>
<el-image :src="detail.options.referImageUrl" /> </el-descriptions>
</div>
</div>
</el-drawer> </el-drawer>
</template> </template>
@ -156,6 +128,25 @@ import { formatTime } from '@/utils'
const showDrawer = ref<boolean>(false) // const showDrawer = ref<boolean>(false) //
const detail = ref<ImageVO>({} as ImageVO) // const detail = ref<ImageVO>({} as ImageVO) //
// StableDiffusion
const hasStableDiffusionOptions = computed(() => {
const options = detail.value?.options
return (
options?.sampler ||
options?.clipGuidancePreset ||
options?.stylePreset ||
options?.steps ||
options?.scale ||
options?.seed
)
})
// Midjourney
const hasMidjourneyOptions = computed(() => {
const options = detail.value?.options
return options?.version || options?.referImageUrl
})
const props = defineProps({ const props = defineProps({
show: { show: {
type: Boolean, type: Boolean,
@ -175,7 +166,7 @@ const handleDrawerClose = async () => {
/** 监听 drawer 是否打开 */ /** 监听 drawer 是否打开 */
const { show } = toRefs(props) const { show } = toRefs(props)
watch(show, async (newValue, oldValue) => { watch(show, async (newValue, _oldValue) => {
showDrawer.value = newValue as boolean showDrawer.value = newValue as boolean
}) })
@ -186,7 +177,7 @@ const getImageDetail = async (id: number) => {
/** 监听 id 变化,加载最新图片详情 */ /** 监听 id 变化,加载最新图片详情 */
const { id } = toRefs(props) const { id } = toRefs(props)
watch(id, async (newVal, oldVal) => { watch(id, async (newVal, _oldVal) => {
if (newVal) { if (newVal) {
await getImageDetail(newVal) await getImageDetail(newVal)
} }
@ -194,31 +185,3 @@ watch(id, async (newVal, oldVal) => {
const emits = defineEmits(['handleDrawerClose']) const emits = defineEmits(['handleDrawerClose'])
</script> </script>
<style scoped lang="scss">
.item {
margin-bottom: 20px;
width: 100%;
overflow: hidden;
word-wrap: break-word;
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.tip {
font-weight: bold;
font-size: 16px;
}
.body {
margin-top: 10px;
color: #616161;
.taskImage {
border-radius: 10px;
}
}
}
</style>

View File

@ -1,12 +1,19 @@
<template> <template>
<el-card class="dr-task" body-class="task-card" shadow="never"> <el-card
class="wh-full"
:body-style="{ margin: 0, padding: 0, height: '100%', position: 'relative' }"
shadow="never"
>
<template #header> <template #header>
绘画任务 绘画任务
<!-- TODO @fan看看怎么优化下这个样子哈 --> <!-- TODO @fan看看怎么优化下这个样子哈 -->
<el-button @click="handleViewPublic"></el-button> <el-button @click="handleViewPublic"></el-button>
</template> </template>
<!-- 图片列表 --> <!-- 图片列表 -->
<div class="task-image-list" ref="imageListRef"> <div
class="relative flex flex-row flex-wrap content-start h-full overflow-auto p-5 pb-[140px] box-border [&>div]:mr-5 [&>div]:mb-5"
ref="imageListRef"
>
<ImageCard <ImageCard
v-for="image in imageList" v-for="image in imageList"
:key="image.id" :key="image.id"
@ -15,7 +22,9 @@
@on-mj-btn-click="handleImageMidjourneyButtonClick" @on-mj-btn-click="handleImageMidjourneyButtonClick"
/> />
</div> </div>
<div class="task-image-pagination"> <div
class="absolute bottom-[60px] h-[50px] leading-[90px] w-full z-[999] bg-white flex flex-row justify-center items-center"
>
<Pagination <Pagination
:total="pageTotal" :total="pageTotal"
v-model:page="queryParams.pageNo" v-model:page="queryParams.pageNo"
@ -150,12 +159,12 @@ const handleImageButtonClick = async (type: string, imageDetail: ImageVO) => {
} }
// //
if (type === 'download') { if (type === 'download') {
await download.image({ url: imageDetail.picUrl }) download.image({ url: imageDetail.picUrl })
return return
} }
// //
if (type === 'regeneration') { if (type === 'regeneration') {
await emits('onRegeneration', imageDetail) emits('onRegeneration', imageDetail)
return return
} }
} }
@ -197,49 +206,3 @@ onUnmounted(async () => {
} }
}) })
</script> </script>
<style lang="scss">
.dr-task {
width: 100%;
height: 100%;
}
.task-card {
margin: 0;
padding: 0;
height: 100%;
position: relative;
}
.task-image-list {
position: relative;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-content: flex-start;
height: 100%;
overflow: auto;
padding: 20px 20px 140px;
box-sizing: border-box; /* 确保内边距不会增加高度 */
> div {
margin-right: 20px;
margin-bottom: 20px;
}
> div:last-of-type {
//margin-bottom: 100px;
}
}
.task-image-pagination {
position: absolute;
bottom: 60px;
height: 50px;
line-height: 90px;
width: 100%;
z-index: 999;
background-color: #ffffff;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
</style>

View File

@ -14,14 +14,14 @@
type="textarea" type="textarea"
/> />
</div> </div>
<div class="hot-words"> <div class="flex flex-col mt-30px">
<div> <div>
<el-text tag="b">随机热词</el-text> <el-text tag="b">随机热词</el-text>
</div> </div>
<el-space wrap class="word-list"> <el-space wrap class="flex flex-row flex-wrap justify-start mt-15px">
<el-button <el-button
round round
class="btn" class="m-0"
:type="selectHotWord === hotWord ? 'primary' : 'default'" :type="selectHotWord === hotWord ? 'primary' : 'default'"
v-for="hotWord in ImageHotWords" v-for="hotWord in ImageHotWords"
:key="hotWord" :key="hotWord"
@ -31,11 +31,11 @@
</el-button> </el-button>
</el-space> </el-space>
</div> </div>
<div class="group-item"> <div class="mt-30px">
<div> <div>
<el-text tag="b">平台</el-text> <el-text tag="b">平台</el-text>
</div> </div>
<el-space wrap class="group-item-body"> <el-space wrap class="mt-15px w-full">
<el-select <el-select
v-model="otherPlatform" v-model="otherPlatform"
placeholder="Select" placeholder="Select"
@ -52,11 +52,11 @@
</el-select> </el-select>
</el-space> </el-space>
</div> </div>
<div class="group-item"> <div class="mt-30px">
<div> <div>
<el-text tag="b">模型</el-text> <el-text tag="b">模型</el-text>
</div> </div>
<el-space wrap class="group-item-body"> <el-space wrap class="mt-15px w-full">
<el-select v-model="modelId" placeholder="Select" size="large" class="!w-350px"> <el-select v-model="modelId" placeholder="Select" size="large" class="!w-350px">
<el-option <el-option
v-for="item in platformModels" v-for="item in platformModels"
@ -67,16 +67,16 @@
</el-select> </el-select>
</el-space> </el-space>
</div> </div>
<div class="group-item"> <div class="mt-30px">
<div> <div>
<el-text tag="b">图片尺寸</el-text> <el-text tag="b">图片尺寸</el-text>
</div> </div>
<el-space wrap class="group-item-body"> <el-space wrap class="mt-15px w-full">
<el-input v-model="width" type="number" class="w-170px" placeholder="图片宽度" /> <el-input v-model="width" type="number" class="w-170px" placeholder="图片宽度" />
<el-input v-model="height" type="number" class="w-170px" placeholder="图片高度" /> <el-input v-model="height" type="number" class="w-170px" placeholder="图片高度" />
</el-space> </el-space>
</div> </div>
<div class="btns"> <div class="flex justify-center mt-50px">
<el-button <el-button
type="primary" type="primary"
size="large" size="large"
@ -187,38 +187,3 @@ watch(
/** 暴露组件方法 */ /** 暴露组件方法 */
defineExpose({ settingValues }) defineExpose({ settingValues })
</script> </script>
<style scoped lang="scss">
.hot-words {
display: flex;
flex-direction: column;
margin-top: 30px;
.word-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: start;
margin-top: 15px;
.btn {
margin: 0;
}
}
}
//
.group-item {
margin-top: 30px;
.group-item-body {
margin-top: 15px;
width: 100%;
}
}
.btns {
display: flex;
justify-content: center;
margin-top: 50px;
}
</style>

View File

@ -14,14 +14,14 @@
type="textarea" type="textarea"
/> />
</div> </div>
<div class="hot-words"> <div class="flex flex-col mt-30px">
<div> <div>
<el-text tag="b">随机热词</el-text> <el-text tag="b">随机热词</el-text>
</div> </div>
<el-space wrap class="word-list"> <el-space wrap class="flex flex-row flex-wrap justify-start mt-15px">
<el-button <el-button
round round
class="btn" class="m-0"
:type="selectHotWord === hotWord ? 'primary' : 'default'" :type="selectHotWord === hotWord ? 'primary' : 'default'"
v-for="hotWord in ImageHotWords" v-for="hotWord in ImageHotWords"
:key="hotWord" :key="hotWord"
@ -31,57 +31,57 @@
</el-button> </el-button>
</el-space> </el-space>
</div> </div>
<div class="model"> <div class="mt-30px">
<div> <div>
<el-text tag="b">模型选择</el-text> <el-text tag="b">模型选择</el-text>
</div> </div>
<el-space wrap class="model-list"> <el-space wrap class="mt-15px">
<div <div
:class="selectModel === model.key ? 'modal-item selectModel' : 'modal-item'" :class="selectModel === model.key ? 'w-110px overflow-hidden flex flex-col items-center border-3 border-solid border-#1293ff rounded-5px cursor-pointer' : 'w-110px overflow-hidden flex flex-col items-center border-3 border-solid border-transparent cursor-pointer'"
v-for="model in Dall3Models" v-for="model in Dall3Models"
:key="model.key" :key="model.key"
> >
<el-image :src="model.image" fit="contain" @click="handleModelClick(model)" /> <el-image :src="model.image" fit="contain" @click="handleModelClick(model)" />
<div class="model-font">{{ model.name }}</div> <div class="text-14px color-#3e3e3e font-bold">{{ model.name }}</div>
</div> </div>
</el-space> </el-space>
</div> </div>
<div class="image-style"> <div class="mt-30px">
<div> <div>
<el-text tag="b">风格选择</el-text> <el-text tag="b">风格选择</el-text>
</div> </div>
<el-space wrap class="image-style-list"> <el-space wrap class="mt-15px">
<div <div
:class="style === imageStyle.key ? 'image-style-item selectImageStyle' : 'image-style-item'" :class="style === imageStyle.key ? 'w-110px overflow-hidden flex flex-col items-center border-3 border-solid border-#1293ff rounded-5px cursor-pointer' : 'w-110px overflow-hidden flex flex-col items-center border-3 border-solid border-transparent cursor-pointer'"
v-for="imageStyle in Dall3StyleList" v-for="imageStyle in Dall3StyleList"
:key="imageStyle.key" :key="imageStyle.key"
> >
<el-image :src="imageStyle.image" fit="contain" @click="handleStyleClick(imageStyle)" /> <el-image :src="imageStyle.image" fit="contain" @click="handleStyleClick(imageStyle)" />
<div class="style-font">{{ imageStyle.name }}</div> <div class="text-14px color-#3e3e3e font-bold">{{ imageStyle.name }}</div>
</div> </div>
</el-space> </el-space>
</div> </div>
<div class="image-size"> <div class="w-full mt-30px">
<div> <div>
<el-text tag="b">画面比例</el-text> <el-text tag="b">画面比例</el-text>
</div> </div>
<el-space wrap class="size-list"> <el-space wrap class="flex flex-row justify-between w-full mt-20px">
<div <div
class="size-item" class="flex flex-col items-center cursor-pointer"
v-for="imageSize in Dall3SizeList" v-for="imageSize in Dall3SizeList"
:key="imageSize.key" :key="imageSize.key"
@click="handleSizeClick(imageSize)" @click="handleSizeClick(imageSize)"
> >
<div <div
:class="selectSize === imageSize.key ? 'size-wrapper selectImageSize' : 'size-wrapper'" :class="selectSize === imageSize.key ? 'flex flex-col items-center justify-center rounded-7px p-4px w-50px h-50px bg-white border-1 border-solid border-#1293ff' : 'flex flex-col items-center justify-center rounded-7px p-4px w-50px h-50px bg-white border-1 border-solid border-white'"
> >
<div :style="imageSize.style"></div> <div :style="imageSize.style"></div>
</div> </div>
<div class="size-font">{{ imageSize.name }}</div> <div class="text-14px color-#3e3e3e font-bold">{{ imageSize.name }}</div>
</div> </div>
</el-space> </el-space>
</div> </div>
<div class="btns"> <div class="flex justify-center mt-50px">
<el-button <el-button
type="primary" type="primary"
size="large" size="large"
@ -229,135 +229,4 @@ const settingValues = async (detail: ImageVO) => {
/** 暴露组件方法 */ /** 暴露组件方法 */
defineExpose({ settingValues }) defineExpose({ settingValues })
</script> </script>
<style scoped lang="scss">
//
.hot-words {
display: flex;
flex-direction: column;
margin-top: 30px;
.word-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: start;
margin-top: 15px;
.btn {
margin: 0;
}
}
}
//
.model {
margin-top: 30px;
.model-list {
margin-top: 15px;
.modal-item {
width: 110px;
//outline: 1px solid blue;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
border: 3px solid transparent;
cursor: pointer;
.model-font {
font-size: 14px;
color: #3e3e3e;
font-weight: bold;
}
}
.selectModel {
border: 3px solid #1293ff;
border-radius: 5px;
}
}
}
// style
.image-style {
margin-top: 30px;
.image-style-list {
margin-top: 15px;
.image-style-item {
width: 110px;
//outline: 1px solid blue;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
border: 3px solid transparent;
cursor: pointer;
.style-font {
font-size: 14px;
color: #3e3e3e;
font-weight: bold;
}
}
.selectImageStyle {
border: 3px solid #1293ff;
border-radius: 5px;
}
}
}
//
.image-size {
width: 100%;
margin-top: 30px;
.size-list {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
margin-top: 20px;
.size-item {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
.size-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 7px;
padding: 4px;
width: 50px;
height: 50px;
background-color: #fff;
border: 1px solid #fff;
}
.size-font {
font-size: 14px;
color: #3e3e3e;
font-weight: bold;
}
}
}
.selectImageSize {
border: 1px solid #1293ff !important;
}
}
.btns {
display: flex;
justify-content: center;
margin-top: 50px;
}
</style>

View File

@ -14,14 +14,14 @@
type="textarea" type="textarea"
/> />
</div> </div>
<div class="hot-words"> <div class="flex flex-col mt-30px">
<div> <div>
<el-text tag="b">随机热词</el-text> <el-text tag="b">随机热词</el-text>
</div> </div>
<el-space wrap class="word-list"> <el-space wrap class="flex flex-row flex-wrap justify-start mt-15px">
<el-button <el-button
round round
class="btn" class="m-0"
:type="selectHotWord === hotWord ? 'primary' : 'default'" :type="selectHotWord === hotWord ? 'primary' : 'default'"
v-for="hotWord in ImageHotWords" v-for="hotWord in ImageHotWords"
:key="hotWord" :key="hotWord"
@ -31,49 +31,49 @@
</el-button> </el-button>
</el-space> </el-space>
</div> </div>
<div class="image-size"> <div class="w-full mt-30px">
<div> <div>
<el-text tag="b">尺寸</el-text> <el-text tag="b">尺寸</el-text>
</div> </div>
<el-space wrap class="size-list"> <el-space wrap class="flex flex-row justify-between w-full mt-20px">
<div <div
class="size-item" class="flex flex-col items-center cursor-pointer"
v-for="imageSize in MidjourneySizeList" v-for="imageSize in MidjourneySizeList"
:key="imageSize.key" :key="imageSize.key"
@click="handleSizeClick(imageSize)" @click="handleSizeClick(imageSize)"
> >
<div <div
:class="selectSize === imageSize.key ? 'size-wrapper selectImageSize' : 'size-wrapper'" :class="selectSize === imageSize.key ? 'flex flex-col items-center justify-center rounded-7px p-4px w-50px h-50px bg-white border-1 border-solid border-#1293ff' : 'flex flex-col items-center justify-center rounded-7px p-4px w-50px h-50px bg-white border-1 border-solid border-white'"
> >
<div :style="imageSize.style"></div> <div :style="imageSize.style"></div>
</div> </div>
<div class="size-font">{{ imageSize.key }}</div> <div class="text-14px color-#3e3e3e font-bold">{{ imageSize.key }}</div>
</div> </div>
</el-space> </el-space>
</div> </div>
<div class="model"> <div class="mt-30px">
<div> <div>
<el-text tag="b">模型</el-text> <el-text tag="b">模型</el-text>
</div> </div>
<el-space wrap class="model-list"> <el-space wrap class="mt-15px">
<div <div
:class="selectModel === model.key ? 'modal-item selectModel' : 'modal-item'" :class="selectModel === model.key ? 'flex flex-col items-center w-150px overflow-hidden border-3 border-solid border-#1293ff rounded-5px cursor-pointer' : 'flex flex-col items-center w-150px overflow-hidden border-3 border-solid border-transparent cursor-pointer'"
v-for="model in MidjourneyModels" v-for="model in MidjourneyModels"
:key="model.key" :key="model.key"
> >
<el-image :src="model.image" fit="contain" @click="handleModelClick(model)" /> <el-image :src="model.image" fit="contain" @click="handleModelClick(model)" />
<div class="model-font">{{ model.name }}</div> <div class="text-14px color-#3e3e3e font-bold">{{ model.name }}</div>
</div> </div>
</el-space> </el-space>
</div> </div>
<div class="version"> <div class="mt-20px">
<div> <div>
<el-text tag="b">版本</el-text> <el-text tag="b">版本</el-text>
</div> </div>
<el-space wrap class="version-list"> <el-space wrap class="mt-20px w-full">
<el-select <el-select
v-model="selectVersion" v-model="selectVersion"
class="version-select !w-350px" class="!w-350px"
clearable clearable
placeholder="请选择版本" placeholder="请选择版本"
> >
@ -86,15 +86,15 @@
</el-select> </el-select>
</el-space> </el-space>
</div> </div>
<div class="model"> <div class="mt-30px">
<div> <div>
<el-text tag="b">参考图</el-text> <el-text tag="b">参考图</el-text>
</div> </div>
<el-space wrap class="model-list"> <el-space wrap class="mt-15px">
<UploadImg v-model="referImageUrl" height="120px" width="120px" /> <UploadImg v-model="referImageUrl" height="120px" width="120px" />
</el-space> </el-space>
</div> </div>
<div class="btns"> <div class="flex justify-center mt-50px">
<el-button <el-button
type="primary" type="primary"
size="large" size="large"
@ -233,118 +233,4 @@ const settingValues = async (detail: ImageVO) => {
/** 暴露组件方法 */ /** 暴露组件方法 */
defineExpose({ settingValues }) defineExpose({ settingValues })
</script> </script>
<style scoped lang="scss">
//
.prompt {
}
//
.hot-words {
display: flex;
flex-direction: column;
margin-top: 30px;
.word-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: start;
margin-top: 15px;
.btn {
margin: 0;
}
}
}
// version
.version {
margin-top: 20px;
.version-list {
margin-top: 20px;
width: 100%;
}
}
//
.model {
margin-top: 30px;
.model-list {
margin-top: 15px;
.modal-item {
display: flex;
flex-direction: column;
align-items: center;
width: 150px;
//outline: 1px solid blue;
overflow: hidden;
border: 3px solid transparent;
cursor: pointer;
.model-font {
font-size: 14px;
color: #3e3e3e;
font-weight: bold;
}
}
.selectModel {
border: 3px solid #1293ff;
border-radius: 5px;
}
}
}
//
.image-size {
width: 100%;
margin-top: 30px;
.size-list {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
margin-top: 20px;
.size-item {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
.size-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 7px;
padding: 4px;
width: 50px;
height: 50px;
background-color: #fff;
border: 1px solid #fff;
}
.size-font {
font-size: 14px;
color: #3e3e3e;
font-weight: bold;
}
}
}
.selectImageSize {
border: 1px solid #1293ff !important;
}
}
.btns {
display: flex;
justify-content: center;
margin-top: 50px;
}
</style>

View File

@ -14,14 +14,14 @@
type="textarea" type="textarea"
/> />
</div> </div>
<div class="hot-words"> <div class="flex flex-col mt-30px">
<div> <div>
<el-text tag="b">随机热词</el-text> <el-text tag="b">随机热词</el-text>
</div> </div>
<el-space wrap class="word-list"> <el-space wrap class="flex flex-row flex-wrap justify-start mt-15px">
<el-button <el-button
round round
class="btn" class="m-0"
:type="selectHotWord === hotWord ? 'primary' : 'default'" :type="selectHotWord === hotWord ? 'primary' : 'default'"
v-for="hotWord in ImageHotEnglishWords" v-for="hotWord in ImageHotEnglishWords"
:key="hotWord" :key="hotWord"
@ -31,11 +31,11 @@
</el-button> </el-button>
</el-space> </el-space>
</div> </div>
<div class="group-item"> <div class="mt-30px">
<div> <div>
<el-text tag="b">采样方法</el-text> <el-text tag="b">采样方法</el-text>
</div> </div>
<el-space wrap class="group-item-body"> <el-space wrap class="mt-15px w-full">
<el-select v-model="sampler" placeholder="Select" size="large" class="!w-350px"> <el-select v-model="sampler" placeholder="Select" size="large" class="!w-350px">
<el-option <el-option
v-for="item in StableDiffusionSamplers" v-for="item in StableDiffusionSamplers"
@ -46,11 +46,11 @@
</el-select> </el-select>
</el-space> </el-space>
</div> </div>
<div class="group-item"> <div class="mt-30px">
<div> <div>
<el-text tag="b">CLIP</el-text> <el-text tag="b">CLIP</el-text>
</div> </div>
<el-space wrap class="group-item-body"> <el-space wrap class="mt-15px w-full">
<el-select v-model="clipGuidancePreset" placeholder="Select" size="large" class="!w-350px"> <el-select v-model="clipGuidancePreset" placeholder="Select" size="large" class="!w-350px">
<el-option <el-option
v-for="item in StableDiffusionClipGuidancePresets" v-for="item in StableDiffusionClipGuidancePresets"
@ -61,11 +61,11 @@
</el-select> </el-select>
</el-space> </el-space>
</div> </div>
<div class="group-item"> <div class="mt-30px">
<div> <div>
<el-text tag="b">风格</el-text> <el-text tag="b">风格</el-text>
</div> </div>
<el-space wrap class="group-item-body"> <el-space wrap class="mt-15px w-full">
<el-select v-model="stylePreset" placeholder="Select" size="large" class="!w-350px"> <el-select v-model="stylePreset" placeholder="Select" size="large" class="!w-350px">
<el-option <el-option
v-for="item in StableDiffusionStylePresets" v-for="item in StableDiffusionStylePresets"
@ -76,20 +76,20 @@
</el-select> </el-select>
</el-space> </el-space>
</div> </div>
<div class="group-item"> <div class="mt-30px">
<div> <div>
<el-text tag="b">图片尺寸</el-text> <el-text tag="b">图片尺寸</el-text>
</div> </div>
<el-space wrap class="group-item-body"> <el-space wrap class="mt-15px w-full">
<el-input v-model="width" class="w-170px" placeholder="图片宽度" /> <el-input v-model="width" class="w-170px" placeholder="图片宽度" />
<el-input v-model="height" class="w-170px" placeholder="图片高度" /> <el-input v-model="height" class="w-170px" placeholder="图片高度" />
</el-space> </el-space>
</div> </div>
<div class="group-item"> <div class="mt-30px">
<div> <div>
<el-text tag="b">迭代步数</el-text> <el-text tag="b">迭代步数</el-text>
</div> </div>
<el-space wrap class="group-item-body"> <el-space wrap class="mt-15px w-full">
<el-input <el-input
v-model="steps" v-model="steps"
type="number" type="number"
@ -99,11 +99,11 @@
/> />
</el-space> </el-space>
</div> </div>
<div class="group-item"> <div class="mt-30px">
<div> <div>
<el-text tag="b">引导系数</el-text> <el-text tag="b">引导系数</el-text>
</div> </div>
<el-space wrap class="group-item-body"> <el-space wrap class="mt-15px w-full">
<el-input <el-input
v-model="scale" v-model="scale"
type="number" type="number"
@ -113,11 +113,11 @@
/> />
</el-space> </el-space>
</div> </div>
<div class="group-item"> <div class="mt-30px">
<div> <div>
<el-text tag="b">随机因子</el-text> <el-text tag="b">随机因子</el-text>
</div> </div>
<el-space wrap class="group-item-body"> <el-space wrap class="mt-15px w-full">
<el-input <el-input
v-model="seed" v-model="seed"
type="number" type="number"
@ -127,7 +127,7 @@
/> />
</el-space> </el-space>
</div> </div>
<div class="btns"> <div class="flex justify-center mt-50px">
<el-button <el-button
type="primary" type="primary"
size="large" size="large"
@ -254,43 +254,4 @@ const settingValues = async (detail: ImageVO) => {
/** 暴露组件方法 */ /** 暴露组件方法 */
defineExpose({ settingValues }) defineExpose({ settingValues })
</script> </script>
<style scoped lang="scss">
//
.prompt {
}
//
.hot-words {
display: flex;
flex-direction: column;
margin-top: 30px;
.word-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: start;
margin-top: 15px;
.btn {
margin: 0;
}
}
}
//
.group-item {
margin-top: 30px;
.group-item-body {
margin-top: 15px;
width: 100%;
}
}
.btns {
display: flex;
justify-content: center;
margin-top: 50px;
}
</style>

View File

@ -1,11 +1,15 @@
<!-- image --> <!-- image -->
<template> <template>
<div class="ai-image"> <div class="absolute inset-0 flex flex-row wh-full">
<div class="left"> <div class="flex flex-col p-5 w-[390px]">
<div class="segmented"> <div class="mb-[30px]">
<el-segmented v-model="selectPlatform" :options="platformOptions" /> <el-segmented
v-model="selectPlatform"
:options="platformOptions"
class="w-[350px] !bg-[#ececec] [--el-border-radius-base:16px] [--el-segmented-item-selected-color:#fff]"
/>
</div> </div>
<div class="modal-switch-container"> <div class="h-full overflow-y-auto">
<Common <Common
v-if="selectPlatform === 'common'" v-if="selectPlatform === 'common'"
ref="commonRef" ref="commonRef"
@ -32,7 +36,7 @@
/> />
</div> </div>
</div> </div>
<div class="main"> <div class="flex-1 bg-white">
<ImageList ref="imageListRef" @on-regeneration="handleRegeneration" /> <ImageList ref="imageListRef" @on-regeneration="handleRegeneration" />
</div> </div>
</div> </div>
@ -79,10 +83,10 @@ const platformOptions = [
const models = ref<ModelVO[]>([]) // const models = ref<ModelVO[]>([]) //
/** 绘画 start */ /** 绘画 start */
const handleDrawStart = async (platform: string) => {} const handleDrawStart = async (_platform: string) => {}
/** 绘画 complete */ /** 绘画 complete */
const handleDrawComplete = async (platform: string) => { const handleDrawComplete = async (_platform: string) => {
await imageListRef.value.getImageList() await imageListRef.value.getImageList()
} }
@ -108,48 +112,3 @@ onMounted(async () => {
models.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.IMAGE) models.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.IMAGE)
}) })
</script> </script>
<style scoped lang="scss">
.ai-image {
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
display: flex;
flex-direction: row;
height: 100%;
width: 100%;
.left {
display: flex;
flex-direction: column;
padding: 20px;
width: 390px;
.segmented .el-segmented {
--el-border-radius-base: 16px;
--el-segmented-item-selected-color: #fff;
background-color: #ececec;
width: 350px;
}
.modal-switch-container {
height: 100%;
overflow-y: auto;
margin-top: 30px;
}
}
.main {
flex: 1;
background-color: #fff;
}
.right {
width: 350px;
background-color: #f7f8fa;
}
}
</style>

View File

@ -1,19 +1,19 @@
<template> <template>
<div class="square-container"> <div class="bg-white p-20px">
<!-- TODO @fanstyle 建议换成 unocss --> <!-- TODO @fanstyle 建议换成 unocss -->
<!-- TODO @fanSearch 可以换成 Icon 组件么 --> <!-- TODO @fanSearch 可以换成 Icon 组件么 -->
<el-input <el-input
v-model="queryParams.prompt" v-model="queryParams.prompt"
style="width: 100%; margin-bottom: 20px" class="!w-full !mb-20px"
size="large" size="large"
placeholder="请输入要搜索的内容" placeholder="请输入要搜索的内容"
:suffix-icon="Search" :suffix-icon="Search"
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
/> />
<div class="gallery"> <div class="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-10px bg-white shadow-[0_0_10px_rgba(0,0,0,0.1)]">
<!-- TODO @fan这个图片的风格要不和 ImageCard.vue 界面一致只有卡片没有操作因为看着更有相框的感觉~~~ --> <!-- TODO @fan这个图片的风格要不和 ImageCard.vue 界面一致只有卡片没有操作因为看着更有相框的感觉~~~ -->
<div v-for="item in list" :key="item.id" class="gallery-item"> <div v-for="item in list" :key="item.id" class="relative overflow-hidden bg-gray-100 cursor-pointer transition-transform duration-300 hover:scale-105">
<img :src="item.picUrl" class="img" /> <img :src="item.picUrl" class="w-full h-auto block transition-transform duration-300 hover:scale-110" />
</div> </div>
</div> </div>
<!-- TODO @fan缺少翻页 --> <!-- TODO @fan缺少翻页 -->
@ -64,41 +64,4 @@ onMounted(async () => {
await getList() await getList()
}) })
</script> </script>
<style scoped lang="scss">
.square-container {
background-color: #fff;
padding: 20px;
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px;
//max-width: 1000px;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.gallery-item {
position: relative;
overflow: hidden;
background: #f0f0f0;
cursor: pointer;
transition: transform 0.3s;
}
.gallery-item img {
width: 100%;
height: auto;
display: block;
transition: transform 0.3s;
}
.gallery-item:hover img {
transform: scale(1.1);
}
.gallery-item:hover {
transform: scale(1.05);
}
}
</style>

View File

@ -29,4 +29,3 @@ const emits = defineEmits<{
(e: 'update:modelValue', value: string): void (e: 'update:modelValue', value: string): void
}>() }>()
</script> </script>
<style scoped></style>

View File

@ -510,10 +510,15 @@ const isManagerUser = (row: any) => {
/** 处理模型的排序 **/ /** 处理模型的排序 **/
const handleModelSort = () => { const handleModelSort = () => {
// if (isModelSorting.value) {
originalData.value = cloneDeep(props.categoryInfo.modelList) //
isModelSorting.value = true handleModelSortCancel()
initSort() } else {
//
originalData.value = cloneDeep(props.categoryInfo.modelList)
isModelSorting.value = true
initSort()
}
} }
/** 处理模型的排序提交 */ /** 处理模型的排序提交 */

View File

@ -102,7 +102,7 @@
</div> </div>
<div v-if="modelData.startUserType === 2" class="mt-2 flex flex-wrap gap-2"> <div v-if="modelData.startUserType === 2" class="mt-2 flex flex-wrap gap-2">
<div <div
v-for="dept in selectedStartDepts" v-for="dept in selectedStartDepts"
:key="dept.id" :key="dept.id"
class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative" class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
> >
@ -186,7 +186,23 @@ const currentSelectType = ref<'start' | 'manager'>('start')
const rules = { const rules = {
name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }], name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }],
key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }], key: [
{ required: true, message: '流程标识不能为空', trigger: 'blur' },
{
validator: (_rule: any, value: string, callback: any) => {
if (!value) {
callback()
return
}
if (!/^[a-zA-Z_][\-_.0-9_a-zA-Z$]*$/.test(value)) {
callback(new Error('只能包含字母、数字、下划线、连字符和点号,且必须以字母或下划线开头'))
return
}
callback()
},
trigger: 'blur'
}
],
category: [{ required: true, message: '流程分类不能为空', trigger: 'blur' }], category: [{ required: true, message: '流程分类不能为空', trigger: 'blur' }],
type: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }], type: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }], visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],

View File

@ -232,6 +232,34 @@ import { ProcessVariableEnum } from '@/components/SimpleProcessDesignerV2/src/co
import HttpRequestSetting from '@/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestSetting.vue' import HttpRequestSetting from '@/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestSetting.vue'
const modelData = defineModel<any>() const modelData = defineModel<any>()
const formFields = ref<string[]>([])
const props = defineProps({
// ID
modelFormId: {
type: Number,
required: false,
default: undefined,
}
})
// modelFormId
watch(
() => props.modelFormId,
async (newVal) => {
if (newVal) {
const form = await FormApi.getForm(newVal);
formFields.value = form?.fields;
} else {
// modelFormId
formFields.value = [];
}
},
{ immediate: true },
);
// 使
provide('formFields', formFields)
/** 自定义 ID 流程编码 */ /** 自定义 ID 流程编码 */
const timeOptions = ref([ const timeOptions = ref([

View File

@ -14,9 +14,9 @@
<template v-else> <template v-else>
<SimpleModelDesign <SimpleModelDesign
v-if="showDesigner" v-if="showDesigner"
:model-id="modelData.id"
:model-key="modelData.key"
:model-name="modelData.name" :model-name="modelData.name"
:model-form-id="modelData.formId"
:model-form-type="modelData.formType"
:start-user-ids="modelData.startUserIds" :start-user-ids="modelData.startUserIds"
:start-dept-ids="modelData.startDeptIds" :start-dept-ids="modelData.startDeptIds"
@success="handleDesignSuccess" @success="handleDesignSuccess"

View File

@ -77,7 +77,10 @@
<!-- 第四步更多设置 --> <!-- 第四步更多设置 -->
<div v-show="currentStep === 3" class="mx-auto w-700px"> <div v-show="currentStep === 3" class="mx-auto w-700px">
<ExtraSettings v-model="formData" ref="extraSettingsRef" /> <ExtraSettings
ref="extraSettingsRef"
v-model="formData"
:model-form-id="formData.formId"/>
</div> </div>
</div> </div>
</div> </div>
@ -216,6 +219,16 @@ const initData = async () => {
// //
if (route.params.type === 'copy') { if (route.params.type === 'copy') {
delete formData.value.id delete formData.value.id
if (formData.value.bpmnXml) {
formData.value.bpmnXml = formData.value.bpmnXml.replaceAll(
formData.value.name,
formData.value.name + '副本'
)
formData.value.bpmnXml = formData.value.bpmnXml.replaceAll(
formData.value.key,
formData.value.key + '_copy'
)
}
formData.value.name += '副本' formData.value.name += '副本'
formData.value.key += '_copy' formData.value.key += '_copy'
tagsView.setTitle('复制流程') tagsView.setTitle('复制流程')

View File

@ -209,15 +209,18 @@ onActivated(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
:deep() { :deep() {
.el-table--fit .el-table__inner-wrapper:before { .el-table--fit .el-table__inner-wrapper::before {
height: 0; height: 0;
} }
.el-card { .el-card {
border-radius: 8px; border-radius: 8px;
} }
.el-form--inline .el-form-item { .el-form--inline .el-form-item {
margin-right: 10px; margin-right: 10px;
} }
.el-divider--horizontal { .el-divider--horizontal {
margin-top: 6px; margin-top: 6px;
} }

View File

@ -231,8 +231,25 @@ const getApprovalDetail = async (row: any) => {
*/ */
const setFieldPermission = (field: string, permission: string) => { const setFieldPermission = (field: string, permission: string) => {
if (permission === FieldPermissionType.READ) { if (permission === FieldPermissionType.READ) {
// 1.
//@ts-ignore //@ts-ignore
fApi.value?.disabled(true, field) fApi.value?.disabled(true, field)
// 2.
// fApi.value?.updateValidate(field, []);
try {
//@ts-ignore
const rule = fApi.value?.getRule(field)
if (rule) {
// false
rule.$required = false
//
if (rule.validate) {
rule.validate = []
}
}
} catch (error) {
console.warn('修改字段验证规则失败:', error)
}
} }
if (permission === FieldPermissionType.WRITE) { if (permission === FieldPermissionType.WRITE) {
//@ts-ignore //@ts-ignore
@ -249,8 +266,15 @@ const submitForm = async () => {
if (!fApi.value || !props.selectProcessDefinition) { if (!fApi.value || !props.selectProcessDefinition) {
return return
} }
//
await fApi.value.validate() try {
//
await fApi.value.validate()
} catch (error) {
//
console.warn('表单验证失败:', error)
return
}
// //
if (startUserSelectTasks.value?.length > 0) { if (startUserSelectTasks.value?.length > 0) {
for (const userTask of startUserSelectTasks.value) { for (const userTask of startUserSelectTasks.value) {

View File

@ -685,6 +685,7 @@ watch(
/** 弹出气泡卡 */ /** 弹出气泡卡 */
const openPopover = async (type: string) => { const openPopover = async (type: string) => {
if (popOverVisible.value[type] === true) return
if (type === 'approve') { if (type === 'approve') {
// //
const valid = await validateNormalForm() const valid = await validateNormalForm()

View File

@ -28,7 +28,9 @@
<div class="flex flex-col items-start gap2" :id="`activity-task-${activity.id}-${index}`"> <div class="flex flex-col items-start gap2" :id="`activity-task-${activity.id}-${index}`">
<!-- 第一行节点名称时间 --> <!-- 第一行节点名称时间 -->
<div class="flex w-full"> <div class="flex w-full">
<div class="font-bold"> {{ activity.name }}</div> <div class="font-bold">
{{ activity.name }} <span v-if="activity.status === TaskStatusEnum.SKIP"></span>
</div>
<!-- 信息时间 --> <!-- 信息时间 -->
<div <div
v-if="activity.status !== TaskStatusEnum.NOT_START" v-if="activity.status !== TaskStatusEnum.NOT_START"
@ -38,7 +40,13 @@
</div> </div>
</div> </div>
<div v-if="activity.nodeType === NodeType.CHILD_PROCESS_NODE"> <div v-if="activity.nodeType === NodeType.CHILD_PROCESS_NODE">
<el-button type="primary" plain size="small" @click="handleChildProcess(activity)"> <el-button
type="primary"
plain
size="small"
@click="handleChildProcess(activity)"
:disabled="!activity.processInstanceId"
>
查看子流程 查看子流程
</el-button> </el-button>
</div> </div>
@ -179,7 +187,7 @@ import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import { TaskStatusEnum } from '@/api/bpm/task' import { TaskStatusEnum } from '@/api/bpm/task'
import { NodeType, CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts' import { NodeType, CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts'
import { isEmpty } from '@/utils/is' import { isEmpty } from '@/utils/is'
import { Check, Close, Loading, Clock, Minus, Delete } from '@element-plus/icons-vue' import { Check, Close, Loading, Clock, Minus, Delete, ArrowDown } from '@element-plus/icons-vue'
import starterSvg from '@/assets/svgs/bpm/starter.svg' import starterSvg from '@/assets/svgs/bpm/starter.svg'
import auditorSvg from '@/assets/svgs/bpm/auditor.svg' import auditorSvg from '@/assets/svgs/bpm/auditor.svg'
import copySvg from '@/assets/svgs/bpm/copy.svg' import copySvg from '@/assets/svgs/bpm/copy.svg'
@ -203,6 +211,8 @@ const { push } = useRouter() // 路由
// //
const statusIconMap2 = { const statusIconMap2 = {
//
'-2': { color: '#cccccc', icon: 'ep:arrow-down' },
// //
'-1': { color: '#909398', icon: 'ep-clock' }, '-1': { color: '#909398', icon: 'ep-clock' },
// //
@ -224,6 +234,8 @@ const statusIconMap2 = {
} }
const statusIconMap = { const statusIconMap = {
//
'-2': { color: '#909398', icon: ArrowDown },
// //
'-1': { color: '#909398', icon: Clock }, '-1': { color: '#909398', icon: Clock },
'0': { color: '#00b32a', icon: Clock }, '0': { color: '#00b32a', icon: Clock },
@ -319,7 +331,9 @@ const handleUserSelectConfirm = (activityId: string, userList: any[]) => {
/** 跳转子流程 */ /** 跳转子流程 */
const handleChildProcess = (activity: any) => { const handleChildProcess = (activity: any) => {
// TODO @lesan if (!activity.processInstanceId) {
return
}
push({ push({
name: 'BpmProcessInstanceDetail', name: 'BpmProcessInstanceDetail',
query: { query: {

View File

@ -1,12 +1,11 @@
<template> <template>
<ContentWrap :bodyStyle="{ padding: '20px 16px' }"> <ContentWrap :bodyStyle="{ padding: '20px 16px' }">
<SimpleProcessDesigner <SimpleProcessDesigner
:model-id="modelId" :model-form-id="modelFormId"
:model-key="modelKey" :model-form-type="modelFormType"
:model-name="modelName"
@success="handleSuccess"
:start-user-ids="startUserIds" :start-user-ids="startUserIds"
:start-dept-ids="startDeptIds" :start-dept-ids="startDeptIds"
@success="handleSuccess"
ref="designerRef" ref="designerRef"
/> />
</ContentWrap> </ContentWrap>
@ -19,9 +18,9 @@ defineOptions({
}) })
defineProps<{ defineProps<{
modelId?: string
modelKey?: string
modelName?: string modelName?: string
modelFormId?: number
modelFormType?: number
startUserIds?: number[] startUserIds?: number[]
startDeptIds?: number[] startDeptIds?: number[]
}>() }>()

View File

@ -56,7 +56,7 @@ import type { UploadUserFile } from 'element-plus'
import * as UserApi from '@/api/system/user' import * as UserApi from '@/api/system/user'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
defineOptions({ name: 'SystemUserImportForm' }) defineOptions({ name: 'CrmCustomerImportForm' })
const message = useMessage() // const message = useMessage() //

View File

@ -24,6 +24,38 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="跟进内容" prop="content" /> <el-table-column align="center" label="跟进内容" prop="content" />
<el-table-column label="图片" align="center">
<template #default="scope">
<div v-if="scope.row.picUrls && scope.row.picUrls.length > 0" class="flex">
<el-image
v-for="(url, index) in scope.row.picUrls"
:key="index"
:src="url"
:preview-src-list="scope.row.picUrls"
class="w-10 h-10 mr-1"
:initial-index="index"
fit="cover"
preview-teleported
/>
</div>
</template>
</el-table-column>
<el-table-column label="附件" align="center">
<template #default="scope">
<div v-if="scope.row.fileUrls && scope.row.fileUrls.length > 0" class="flex flex-col">
<el-link
v-for="(url, index) in scope.row.fileUrls"
:key="index"
:href="url"
type="primary"
target="_blank"
download
>
{{ getFileName(url) }}
</el-link>
</div>
</template>
</el-table-column>
<el-table-column <el-table-column
:formatter="dateFormatter" :formatter="dateFormatter"
align="center" align="center"
@ -97,6 +129,14 @@ import { BizTypeEnum } from '@/api/crm/permission'
/** 跟进记录列表 */ /** 跟进记录列表 */
defineOptions({ name: 'FollowUpRecord' }) defineOptions({ name: 'FollowUpRecord' })
const getFileName = (url: string) => {
if (!url) {
return ''
}
return url.substring(url.lastIndexOf('/') + 1)
}
const props = defineProps<{ const props = defineProps<{
bizType: number bizType: number
bizId: number bizId: number

View File

@ -55,13 +55,23 @@
<Icon class="mr-5px" icon="ep:zoom-in" /> <Icon class="mr-5px" icon="ep:zoom-in" />
导入 导入
</el-button> </el-button>
<el-button
v-hasPermi="['infra:codegen:delete']"
type="danger"
:disabled="checkedIds.length === 0"
@click="handleDeleteBatch"
>
<Icon class="mr-5px" icon="ep:delete" />
批量删除
</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</ContentWrap> </ContentWrap>
<!-- 列表 --> <!-- 列表 -->
<ContentWrap> <ContentWrap>
<el-table v-loading="loading" :data="list"> <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
<el-table-column type="selection" width="55" />
<el-table-column align="center" label="数据源"> <el-table-column align="center" label="数据源">
<template #default="scope"> <template #default="scope">
{{ {{
@ -232,6 +242,24 @@ const handleDelete = async (id: number) => {
} catch {} } catch {}
} }
/** 批量删除操作 */
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (rows: CodegenApi.CodegenTableVO[]) => {
checkedIds.value = rows.map((row) => row.id)
}
const handleDeleteBatch = async () => {
try {
//
await message.delConfirm()
//
await CodegenApi.deleteCodegenTableList(checkedIds.value)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 同步操作 */ /** 同步操作 */
const handleSyncDB = async (row: CodegenApi.CodegenTableVO) => { const handleSyncDB = async (row: CodegenApi.CodegenTableVO) => {
// DB // DB

View File

@ -65,6 +65,15 @@
> >
<Icon icon="ep:plus" class="mr-5px" /> 新增 <Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button> </el-button>
<el-button
type="danger"
plain
:disabled="checkedIds.length === 0"
@click="handleDeleteBatch"
v-hasPermi="['infra:config:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
<el-button <el-button
type="success" type="success"
plain plain
@ -80,7 +89,8 @@
<!-- 列表 --> <!-- 列表 -->
<ContentWrap> <ContentWrap>
<el-table v-loading="loading" :data="list"> <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
<el-table-column type="selection" width="55" />
<el-table-column label="参数主键" align="center" prop="id" /> <el-table-column label="参数主键" align="center" prop="id" />
<el-table-column label="参数分类" align="center" prop="category" /> <el-table-column label="参数分类" align="center" prop="category" />
<el-table-column label="参数名称" align="center" prop="name" :show-overflow-tooltip="true" /> <el-table-column label="参数名称" align="center" prop="name" :show-overflow-tooltip="true" />
@ -206,6 +216,24 @@ const handleDelete = async (id: number) => {
} catch {} } catch {}
} }
/** 批量删除按钮操作 */
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (rows: ConfigApi.ConfigVO[]) => {
checkedIds.value = rows.map((row) => row.id!).filter(Boolean)
}
const handleDeleteBatch = async () => {
try {
//
await message.delConfirm()
//
await ConfigApi.deleteConfigList(checkedIds.value)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 导出按钮操作 */ /** 导出按钮操作 */
const handleExport = async () => { const handleExport = async () => {
try { try {

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