style: 使用 Prettier 格式化源码

master-bpm-bug-fix
YunaiV 2026-06-20 07:01:34 -07:00
parent 3208a76868
commit 18ed1cdfed
221 changed files with 1426 additions and 908 deletions

View File

@ -39,4 +39,4 @@ export const ProcessExpressionApi = {
exportProcessExpression: async (params) => {
return await request.download({ url: `/bpm/process-expression/export-excel`, params })
}
}
}

View File

@ -1,6 +1,5 @@
import request from '@/config/axios'
export const updateBpmSimpleModel = async (data) => {
return await request.post({
url: '/bpm/model/simple/update',

View File

@ -34,7 +34,11 @@ export const getMyFriendList = () => {
}
// 增量拉取当前用户的好友关系(重连 / 离线补偿)
export const pullMyFriendList = (params: { lastUpdateTime?: number; lastId?: number; limit: number }) => {
export const pullMyFriendList = (params: {
lastUpdateTime?: number
lastId?: number
limit: number
}) => {
return request.get<ImFriendRespVO[]>({ url: '/im/friend/pull', params })
}
@ -65,4 +69,3 @@ export const blockFriend = (friendUserId: number | string) => {
export const unblockFriend = (friendUserId: number | string) => {
return request.put<boolean>({ url: '/im/friend/unblock', params: { friendUserId } })
}

View File

@ -72,6 +72,10 @@ export const getMyGroupRequest = (id: number) => {
}
// 增量拉取我管理的所有群下加群申请变更(重连 / 离线补偿)
export const pullMyGroupRequestList = (params: { lastUpdateTime?: number; lastId?: number; limit: number }) => {
export const pullMyGroupRequestList = (params: {
lastUpdateTime?: number
lastId?: number
limit: number
}) => {
return request.get<ImGroupRequestRespVO[]>({ url: '/im/group-request/pull', params })
}

View File

@ -42,12 +42,18 @@ export const getStatisticsOverview = (): Promise<ImStatisticsOverviewVO> => {
// 获得消息趋势(私聊 + 群聊双线)
export const getMessageTrend = (days: number): Promise<ImStatisticsTrendVO> => {
return request.get<ImStatisticsTrendVO>({ url: '/im/manager/statistics/message-trend', params: { days } })
return request.get<ImStatisticsTrendVO>({
url: '/im/manager/statistics/message-trend',
params: { days }
})
}
// 获得用户趋势(新增注册 + 日活双线)
export const getUserTrend = (days: number): Promise<ImStatisticsTrendVO> => {
return request.get<ImStatisticsTrendVO>({ url: '/im/manager/statistics/user-trend', params: { days } })
return request.get<ImStatisticsTrendVO>({
url: '/im/manager/statistics/user-trend',
params: { days }
})
}
// 获得内容类型分布(最近 30 天)

View File

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

View File

@ -1,29 +1,29 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
import type { Dayjs } from 'dayjs'
/** 学生课程信息 */
export interface Demo03Course {
id: number; // 编号
studentId?: number; // 学生编号
name?: string; // 名字
score?: number; // 分数
id: number // 编号
studentId?: number // 学生编号
name?: string // 名字
score?: number // 分数
}
/** 学生班级信息 */
export interface Demo03Grade {
id: number; // 编号
studentId?: number; // 学生编号
name?: string; // 名字
teacher?: string; // 班主任
id: number // 编号
studentId?: number // 学生编号
name?: string // 名字
teacher?: string // 班主任
}
/** 学生信息 */
export interface Demo03Student {
id: number; // 编号
name?: string; // 名字
sex?: number; // 性别
birthday?: string | Dayjs; // 出生日期
description?: string; // 简介
id: number // 编号
name?: string // 名字
sex?: number // 性别
birthday?: string | Dayjs // 出生日期
description?: string // 简介
}
// 学生 API
@ -55,7 +55,9 @@ export const Demo03StudentApi = {
/** 批量删除学生 */
deleteDemo03StudentList: async (ids: number[]) => {
return await request.delete({ url: `/infra/demo03-student-erp/delete-list?ids=${ids.join(',')}` })
return await request.delete({
url: `/infra/demo03-student-erp/delete-list?ids=${ids.join(',')}`
})
},
// 导出学生 Excel
@ -63,7 +65,7 @@ export const Demo03StudentApi = {
return await request.download({ url: `/infra/demo03-student-erp/export-excel`, params })
},
// ==================== 子表(学生课程) ====================
// ==================== 子表(学生课程) ====================
// 获得学生课程分页
getDemo03CoursePage: async (params) => {
@ -86,7 +88,9 @@ export const Demo03StudentApi = {
/** 批量删除学生课程 */
deleteDemo03CourseList: async (ids: number[]) => {
return await request.delete({ url: `/infra/demo03-student-erp/demo03-course/delete-list?ids=${ids.join(',')}` })
return await request.delete({
url: `/infra/demo03-student-erp/demo03-course/delete-list?ids=${ids.join(',')}`
})
},
// 获得学生课程
@ -94,7 +98,7 @@ export const Demo03StudentApi = {
return await request.get({ url: `/infra/demo03-student-erp/demo03-course/get?id=` + id })
},
// ==================== 子表(学生班级) ====================
// ==================== 子表(学生班级) ====================
// 获得学生班级分页
getDemo03GradePage: async (params) => {
@ -117,11 +121,13 @@ export const Demo03StudentApi = {
/** 批量删除学生班级 */
deleteDemo03GradeList: async (ids: number[]) => {
return await request.delete({ url: `/infra/demo03-student-erp/demo03-grade/delete-list?ids=${ids.join(',')}` })
return await request.delete({
url: `/infra/demo03-student-erp/demo03-grade/delete-list?ids=${ids.join(',')}`
})
},
// 获得学生班级
getDemo03Grade: async (id: number) => {
return await request.get({ url: `/infra/demo03-student-erp/demo03-grade/get?id=` + id })
},
}
}

View File

@ -1,29 +1,29 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
import type { Dayjs } from 'dayjs'
/** 学生课程信息 */
export interface Demo03Course {
id: number; // 编号
studentId?: number; // 学生编号
name?: string; // 名字
score?: number; // 分数
id: number // 编号
studentId?: number // 学生编号
name?: string // 名字
score?: number // 分数
}
/** 学生班级信息 */
export interface Demo03Grade {
id: number; // 编号
studentId?: number; // 学生编号
name?: string; // 名字
teacher?: string; // 班主任
id: number // 编号
studentId?: number // 学生编号
name?: string // 名字
teacher?: string // 班主任
}
/** 学生信息 */
export interface Demo03Student {
id: number; // 编号
name?: string; // 名字
sex?: number; // 性别
birthday?: string | Dayjs; // 出生日期
description?: string; // 简介
id: number // 编号
name?: string // 名字
sex?: number // 性别
birthday?: string | Dayjs // 出生日期
description?: string // 简介
demo03courses?: Demo03Course[]
demo03grade?: Demo03Grade
}
@ -57,7 +57,9 @@ export const Demo03StudentApi = {
/** 批量删除学生 */
deleteDemo03StudentList: async (ids: number[]) => {
return await request.delete({ url: `/infra/demo03-student-inner/delete-list?ids=${ids.join(',')}` })
return await request.delete({
url: `/infra/demo03-student-inner/delete-list?ids=${ids.join(',')}`
})
},
// 导出学生 Excel
@ -65,17 +67,21 @@ export const Demo03StudentApi = {
return await request.download({ url: `/infra/demo03-student-inner/export-excel`, params })
},
// ==================== 子表(学生课程) ====================
// ==================== 子表(学生课程) ====================
// 获得学生课程列表
getDemo03CourseListByStudentId: async (studentId) => {
return await request.get({ url: `/infra/demo03-student-inner/demo03-course/list-by-student-id?studentId=` + studentId })
return await request.get({
url: `/infra/demo03-student-inner/demo03-course/list-by-student-id?studentId=` + studentId
})
},
// ==================== 子表(学生班级) ====================
// ==================== 子表(学生班级) ====================
// 获得学生班级
getDemo03GradeByStudentId: async (studentId) => {
return await request.get({ url: `/infra/demo03-student-inner/demo03-grade/get-by-student-id?studentId=` + studentId })
},
return await request.get({
url: `/infra/demo03-student-inner/demo03-grade/get-by-student-id?studentId=` + studentId
})
}
}

View File

@ -1,29 +1,29 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
import type { Dayjs } from 'dayjs'
/** 学生课程信息 */
export interface Demo03Course {
id: number; // 编号
studentId?: number; // 学生编号
name?: string; // 名字
score?: number; // 分数
id: number // 编号
studentId?: number // 学生编号
name?: string // 名字
score?: number // 分数
}
/** 学生班级信息 */
export interface Demo03Grade {
id: number; // 编号
studentId?: number; // 学生编号
name?: string; // 名字
teacher?: string; // 班主任
id: number // 编号
studentId?: number // 学生编号
name?: string // 名字
teacher?: string // 班主任
}
/** 学生信息 */
export interface Demo03Student {
id: number; // 编号
name?: string; // 名字
sex?: number; // 性别
birthday?: string | Dayjs; // 出生日期
description?: string; // 简介
id: number // 编号
name?: string // 名字
sex?: number // 性别
birthday?: string | Dayjs // 出生日期
description?: string // 简介
demo03courses?: Demo03Course[]
demo03grade?: Demo03Grade
}
@ -57,7 +57,9 @@ export const Demo03StudentApi = {
/** 批量删除学生 */
deleteDemo03StudentList: async (ids: number[]) => {
return await request.delete({ url: `/infra/demo03-student-normal/delete-list?ids=${ids.join(',')}` })
return await request.delete({
url: `/infra/demo03-student-normal/delete-list?ids=${ids.join(',')}`
})
},
// 导出学生 Excel
@ -65,17 +67,21 @@ export const Demo03StudentApi = {
return await request.download({ url: `/infra/demo03-student-normal/export-excel`, params })
},
// ==================== 子表(学生课程) ====================
// ==================== 子表(学生课程) ====================
// 获得学生课程列表
getDemo03CourseListByStudentId: async (studentId) => {
return await request.get({ url: `/infra/demo03-student-normal/demo03-course/list-by-student-id?studentId=` + studentId })
return await request.get({
url: `/infra/demo03-student-normal/demo03-course/list-by-student-id?studentId=` + studentId
})
},
// ==================== 子表(学生班级) ====================
// ==================== 子表(学生班级) ====================
// 获得学生班级
getDemo03GradeByStudentId: async (studentId) => {
return await request.get({ url: `/infra/demo03-student-normal/demo03-grade/get-by-student-id?studentId=` + studentId })
},
return await request.get({
url: `/infra/demo03-student-normal/demo03-grade/get-by-student-id?studentId=` + studentId
})
}
}

View File

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

View File

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

View File

@ -39,7 +39,10 @@ export const deleteTenantPackage = (id: number) => {
// 批量删除租户套餐
export const deleteTenantPackageList = (ids: number[]) => {
return request.delete({ url: '/system/tenant-package/delete-list', params: { ids: ids.join(',') } })
return request.delete({
url: '/system/tenant-package/delete-list',
params: { ids: ids.join(',') }
})
}
// 获取租户套餐精简信息列表

View File

@ -17,7 +17,12 @@
</el-radio-group>
</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-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-radio-group v-model="formData.indicator">

View File

@ -1,4 +1,4 @@
import {ComponentStyle, DiyComponent} from '@/components/DiyEditor/util'
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
/** 积分商城属性 */
export interface PromotionPointProperty {

View File

@ -31,13 +31,7 @@
/>
</el-form-item>
<el-form-item label="高度" prop="height" label-width="70px">
<el-slider
v-model="formData.height"
:max="200"
:min="20"
show-input
input-size="small"
/>
<el-slider v-model="formData.height" :max="200" :min="20" show-input input-size="small" />
</el-form-item>
</el-card>
<el-card header="主标题" class="property-group" shadow="never">

View File

@ -34,7 +34,7 @@ interface AreaVO {
interface Props {
modelValue?: number[] | string[]
level?: typeof AreaLevelEnum[keyof typeof AreaLevelEnum]
level?: (typeof AreaLevelEnum)[keyof typeof AreaLevelEnum]
disabled?: boolean
placeholder?: string
clearable?: boolean

View File

@ -155,32 +155,32 @@ const hasValidPresetValue = (): boolean => {
//
const setDefaultValue = () => {
console.log('[DeptSelect] setDefaultValue called, defaultCurrentDept:', props.defaultCurrentDept)
// defaultCurrentDept true
if (!props.defaultCurrentDept) {
console.log('[DeptSelect] defaultCurrentDept is false, skip')
return
}
//
if (hasValidPresetValue()) {
console.log('[DeptSelect] has preset value, skip:', props.modelValue)
return
}
// ID
const userStore = useUserStoreWithOut()
const user = userStore.getUser
const deptId = user?.deptId
console.log('[DeptSelect] current user:', user, 'deptId:', deptId)
// deptId 0
if (!deptId || deptId === 0) {
console.log('[DeptSelect] deptId is invalid, skip')
return
}
//
const defaultValue = props.multiple ? [deptId] : deptId
console.log('[DeptSelect] setting default value:', defaultValue)

View File

@ -40,7 +40,14 @@ export const useFormCreateDesigner = async (designer: Ref) => {
designer.value?.removeMenuItem('fcEditor')
const iframeRule = useIframeRule()
const areaSelectRule = useAreaSelectRule()
const components = [editorRule, uploadFileRule, uploadImgRule, uploadImgsRule, iframeRule, areaSelectRule]
const components = [
editorRule,
uploadFileRule,
uploadImgRule,
uploadImgsRule,
iframeRule,
areaSelectRule
]
components.forEach((component) => {
// 插入组件规则
designer.value?.addComponent(component)

View File

@ -19,7 +19,7 @@ export const localeProps = (t, prefix, rules) => {
/**
* field, title
*
*
* @param rule https://www.form-create.com/v3/guide/rule
* @param fields
* @param parentTitle

View File

@ -125,7 +125,12 @@ watch(
<template>
<div class="selector">
<ElInput v-model="inputValue" @click="visible = !visible" :clearable="props.clearable" @clear="clearIcon">
<ElInput
v-model="inputValue"
@click="visible = !visible"
:clearable="props.clearable"
@clear="clearIcon"
>
<template #append>
<ElPopover
:popper-options="{

View File

@ -5,43 +5,43 @@ export interface JsonEditorProps {
* JSON
*/
modelValue: any
/**
*
* @default 'tree'
*/
mode?: JSONEditorMode
/**
*
* @default '400px'
*/
height?: string
/**
*
* @default false
*/
showModeSelection?: boolean
/**
*
* @default false
*/
showNavigationBar?: boolean
/**
*
* @default true
*/
showStatusBar?: boolean
/**
*
* @default true
*/
showMainMenuBar?: boolean
/**
* JSONEditor
* @see https://github.com/josdejong/jsoneditor/blob/develop/docs/api.md
@ -57,12 +57,12 @@ export interface JsonEditorEmits {
*
*/
(e: 'update:modelValue', value: any): void
/**
*
*/
(e: 'change', value: any): void
/**
*
*/
@ -77,4 +77,4 @@ export interface JsonEditorExpose {
* JSONEditor
*/
getEditor: () => any
}
}

View File

@ -50,13 +50,13 @@ const props = defineProps({
modelFormId: {
type: Number,
required: false,
default: undefined,
default: undefined
},
//
//
modelFormType: {
type: Number,
required: false,
default: BpmModelFormType.NORMAL,
default: BpmModelFormType.NORMAL
},
//
startUserIds: {
@ -73,30 +73,30 @@ const props = defineProps({
const processData = inject('processData') as Ref
const loading = ref(false)
const formFields = ref<string[]>([])
const formType = ref(props.modelFormType);
const formType = ref(props.modelFormType)
// modelFormType
watch(
() => props.modelFormType,
(newVal) => {
formType.value = newVal;
},
);
formType.value = newVal
}
)
// modelFormId
watch(
() => props.modelFormId,
async (newVal) => {
if (newVal) {
const form = await getForm(newVal);
formFields.value = form?.fields;
const form = await getForm(newVal)
formFields.value = form?.fields
} else {
// modelFormId
formFields.value = [];
formFields.value = []
}
},
{ immediate: true },
);
{ immediate: true }
)
const roleOptions = ref<RoleApi.RoleVO[]>([]) //
const postOptions = ref<PostApi.PostVO[]>([]) //
@ -118,7 +118,6 @@ provide('startDeptIds', props.startDeptIds)
provide('tasks', [])
provide('processInstance', {})
const message = useMessage() //
const processNodeTree = ref<SimpleFlowNode | undefined>()
provide('processNodeTree', processNodeTree)

View File

@ -2,4 +2,4 @@ import SimpleProcessDesigner from './SimpleProcessDesigner.vue'
import SimpleProcessViewer from './SimpleProcessViewer.vue'
import '../theme/simple-process-designer.scss'
export { SimpleProcessDesigner, SimpleProcessViewer}
export { SimpleProcessDesigner, SimpleProcessViewer }

View File

@ -295,10 +295,20 @@
/>
</el-select>
</el-form-item>
<el-form-item v-if="configForm.multiInstanceSourceType === ChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY">
<el-form-item
v-if="
configForm.multiInstanceSourceType ===
ChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY
"
>
<el-input-number v-model="configForm.multiInstanceSource" :min="1" />
</el-form-item>
<el-form-item v-if="configForm.multiInstanceSourceType === ChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM">
<el-form-item
v-if="
configForm.multiInstanceSourceType ===
ChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM
"
>
<el-select class="w-200px!" v-model="configForm.multiInstanceSource">
<el-option
v-for="(field, fIdx) in digitalFormFieldOptions"
@ -308,7 +318,12 @@
/>
</el-select>
</el-form-item>
<el-form-item v-if="configForm.multiInstanceSourceType === ChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM">
<el-form-item
v-if="
configForm.multiInstanceSourceType ===
ChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM
"
>
<el-select class="w-200px!" v-model="configForm.multiInstanceSource">
<el-option
v-for="(field, fIdx) in multiFormFieldOptions"
@ -519,7 +534,9 @@ const showChildProcessNodeConfig = (node: SimpleFlowNode) => {
configForm.value.outVariables = node.childProcessSetting.outVariables
// 6.
configForm.value.startUserType = node.childProcessSetting.startUserSetting.type
configForm.value.startUserEmptyType = node.childProcessSetting.startUserSetting.emptyType ?? ChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER
configForm.value.startUserEmptyType =
node.childProcessSetting.startUserSetting.emptyType ??
ChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER
configForm.value.startUserFormField = node.childProcessSetting.startUserSetting.formField ?? ''
// 7.
configForm.value.timeoutEnable = node.childProcessSetting.timeoutSetting.enable ?? false

View File

@ -1,7 +1,10 @@
import { TimeUnitType, ApproveType, APPROVE_TYPE } from './consts'
// 获取条件节点默认的名称
export const getDefaultConditionNodeName = (index: number, defaultFlow: boolean | undefined): string => {
export const getDefaultConditionNodeName = (
index: number,
defaultFlow: boolean | undefined
): string => {
if (defaultFlow) {
return '其它情况'
}
@ -9,7 +12,10 @@ export const getDefaultConditionNodeName = (index: number, defaultFlow: boolean
}
// 获取包容分支条件节点默认的名称
export const getDefaultInclusiveConditionNodeName = (index: number, defaultFlow: boolean | undefined): string => {
export const getDefaultInclusiveConditionNodeName = (
index: number,
defaultFlow: boolean | undefined
): string => {
if (defaultFlow) {
return '其它情况'
}

View File

@ -174,7 +174,7 @@
}
.router {
color: #ca3a31
color: #ca3a31;
}
.transactor {
@ -303,7 +303,7 @@
}
&.router-node {
color: #ca3a31
color: #ca3a31;
}
&.transactor-task {
@ -762,14 +762,15 @@
// iconfont
@font-face {
font-family: "iconfont"; /* Project id 4495938 */
src: url('iconfont.woff2?t=1737639517142') format('woff2'),
url('iconfont.woff?t=1737639517142') format('woff'),
url('iconfont.ttf?t=1737639517142') format('truetype');
font-family: 'iconfont'; /* Project id 4495938 */
src:
url('iconfont.woff2?t=1737639517142') format('woff2'),
url('iconfont.woff?t=1737639517142') format('woff'),
url('iconfont.ttf?t=1737639517142') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-family: 'iconfont' !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
@ -777,49 +778,49 @@
}
.icon-trigger:before {
content: "\e6d3";
content: '\e6d3';
}
.icon-router:before {
content: "\e6b2";
content: '\e6b2';
}
.icon-delay:before {
content: "\e600";
content: '\e600';
}
.icon-start-user:before {
content: "\e679";
content: '\e679';
}
.icon-inclusive:before {
content: "\e602";
content: '\e602';
}
.icon-copy:before {
content: "\e7eb";
content: '\e7eb';
}
.icon-transactor:before {
content: "\e61c";
content: '\e61c';
}
.icon-exclusive:before {
content: "\e717";
content: '\e717';
}
.icon-approve:before {
content: "\e715";
content: '\e715';
}
.icon-parallel:before {
content: "\e688";
content: '\e688';
}
.icon-async-child-process:before {
content: "\e6f2";
content: '\e6f2';
}
.icon-child-process:before {
content: "\e6c1";
content: '\e6c1';
}

View File

@ -36,7 +36,7 @@
* Verify 验证码组件
* @description 分发验证码使用
* */
import {VerifyPictureWord, VerifyPoints, VerifySlide} from './Verify'
import { VerifyPictureWord, VerifyPoints, VerifySlide } from './Verify'
import { computed, ref, toRefs, watchEffect } from 'vue'
export default {
@ -443,4 +443,4 @@ export default {
content: ' ';
inset: 0;
}
</style>
</style>

View File

@ -2,4 +2,4 @@ import VerifySlide from './VerifySlide.vue'
import VerifyPoints from './VerifyPoints.vue'
import VerifyPictureWord from './VerifyPictureWord.vue'
export { VerifySlide, VerifyPoints, VerifyPictureWord }
export { VerifySlide, VerifyPoints, VerifyPictureWord }

View File

@ -1254,11 +1254,11 @@
"allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"]
},
"properties": [
{
"name": "value",
"type": "Integer",
"isBody": true
}
{
"name": "value",
"type": "Integer",
"isBody": true
}
]
},
{

View File

@ -23,20 +23,20 @@
export default function customTranslate(translations) {
return function (template, replacements) {
replacements = replacements || {};
replacements = replacements || {}
// 将模板和翻译字典的键统一转换为小写进行匹配
const lowerTemplate = template.toLowerCase();
const translation = Object.keys(translations).find(key => key.toLowerCase() === lowerTemplate);
const lowerTemplate = template.toLowerCase()
const translation = Object.keys(translations).find((key) => key.toLowerCase() === lowerTemplate)
// 如果找到匹配的翻译,使用翻译后的模板
if (translation) {
template = translations[translation];
template = translations[translation]
}
// 替换模板中的占位符
return template.replace(/{([^}]+)}/g, function (_, key) {
// 如果替换值存在,返回替换值;否则返回原始占位符
return replacements[key] !== undefined ? replacements[key] : `{${key}}`;
});
};
}
return replacements[key] !== undefined ? replacements[key] : `{${key}}`
})
}
}

View File

@ -2,14 +2,15 @@
@use './process-panel.scss';
$success-color: #4eb819;
$primary-color: #409EFF;
$danger-color: #F56C6C;
$primary-color: #409eff;
$danger-color: #f56c6c;
$cancel-color: #909399;
.process-viewer {
position: relative;
border: 1px solid #EFEFEF;
background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+') repeat!important;
border: 1px solid #efefef;
background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+')
repeat !important;
.success-arrow {
fill: $success-color;
@ -23,7 +24,7 @@ $cancel-color: #909399;
.success.djs-connection {
.djs-visual path {
stroke: $success-color!important;
stroke: $success-color !important;
//marker-end: url(#sequenceflow-end-white-success)!important;
}
}
@ -36,82 +37,84 @@ $cancel-color: #909399;
.success.djs-shape {
.djs-visual rect {
stroke: $success-color!important;
fill: $success-color!important;
fill-opacity: 0.15!important;
stroke: $success-color !important;
fill: $success-color !important;
fill-opacity: 0.15 !important;
}
.djs-visual polygon {
stroke: $success-color!important;
stroke: $success-color !important;
}
.djs-visual path:nth-child(2) {
stroke: $success-color!important;
fill: $success-color!important;
stroke: $success-color !important;
fill: $success-color !important;
}
.djs-visual circle {
stroke: $success-color!important;
fill: $success-color!important;
fill-opacity: 0.15!important;
stroke: $success-color !important;
fill: $success-color !important;
fill-opacity: 0.15 !important;
}
}
.primary.djs-shape {
.djs-visual rect {
stroke: $primary-color!important;
fill: $primary-color!important;
fill-opacity: 0.15!important;
stroke: $primary-color !important;
fill: $primary-color !important;
fill-opacity: 0.15 !important;
}
.djs-visual polygon {
stroke: $primary-color!important;
stroke: $primary-color !important;
}
.djs-visual circle {
stroke: $primary-color!important;
fill: $primary-color!important;
fill-opacity: 0.15!important;
stroke: $primary-color !important;
fill: $primary-color !important;
fill-opacity: 0.15 !important;
}
}
.danger.djs-shape {
.djs-visual rect {
stroke: $danger-color!important;
fill: $danger-color!important;
fill-opacity: 0.15!important;
stroke: $danger-color !important;
fill: $danger-color !important;
fill-opacity: 0.15 !important;
}
.djs-visual polygon {
stroke: $danger-color!important;
stroke: $danger-color !important;
}
.djs-visual circle {
stroke: $danger-color!important;
fill: $danger-color!important;
fill-opacity: 0.15!important;
stroke: $danger-color !important;
fill: $danger-color !important;
fill-opacity: 0.15 !important;
}
}
.cancel.djs-shape {
.djs-visual rect {
stroke: $cancel-color!important;
fill: $cancel-color!important;
fill-opacity: 0.15!important;
stroke: $cancel-color !important;
fill: $cancel-color !important;
fill-opacity: 0.15 !important;
}
.djs-visual polygon {
stroke: $cancel-color!important;
stroke: $cancel-color !important;
}
.djs-visual circle {
stroke: $cancel-color!important;
fill: $cancel-color!important;
fill-opacity: 0.15!important;
stroke: $cancel-color !important;
fill: $cancel-color !important;
fill-opacity: 0.15 !important;
}
}
}
.process-viewer .djs-tooltip-container, .process-viewer .djs-overlay-container, .process-viewer .djs-palette {
.process-viewer .djs-tooltip-container,
.process-viewer .djs-overlay-container,
.process-viewer .djs-palette {
display: none;
}

View File

@ -167,8 +167,8 @@ service.interceptors.response.use(
cb()
})
requestList = []
if ((config!.headers || {}).isEncrypt){
(config!.headers || {}).isEncrypted = true
if ((config!.headers || {}).isEncrypt) {
;(config!.headers || {}).isEncrypted = true
}
return service(config)
} catch (e) {

View File

@ -8,7 +8,7 @@ export function useWatermark(appendEl: HTMLElement | null = document.body) {
const id = domSymbol.toString()
const appStore = useAppStore()
let watermarkStr = ''
const clear = () => {
const domId = document.getElementById(id)
if (domId) {

View File

@ -27,7 +27,10 @@ onMounted(() => {
watch(
() => collapse.value,
(collapse: boolean) => {
if (getLayoutRenderMode(unref(layout)) === 'topLeft' || getLayoutRenderMode(unref(layout)) === 'cutMenu') {
if (
getLayoutRenderMode(unref(layout)) === 'topLeft' ||
getLayoutRenderMode(unref(layout)) === 'cutMenu'
) {
show.value = true
return
}

View File

@ -8,11 +8,7 @@ import { hasOneShowingChild } from './helper'
import { isUrl } from '@/utils/is'
import { useDesign } from '@/hooks/web/useDesign'
import { createRouteLocation, resolveDynamicPath } from '@/utils/routeParams'
import {
isHeaderNavLayout,
isHorizontalMenuLayout,
isTwoColumnLayout
} from '@/utils/layout'
import { isHeaderNavLayout, isHorizontalMenuLayout, isTwoColumnLayout } from '@/utils/layout'
import { cloneDeep } from 'lodash-es'
import { pathResolve } from '@/utils/routerHelper'
import {
@ -69,8 +65,9 @@ export default defineComponent({
const menuRef = ref<InstanceType<typeof ElMenu>>()
const menuMode = computed((): 'vertical' | 'horizontal' =>
props.mode || (isHorizontalMenuLayout(unref(layout)) ? 'horizontal' : 'vertical')
const menuMode = computed(
(): 'vertical' | 'horizontal' =>
props.mode || (isHorizontalMenuLayout(unref(layout)) ? 'horizontal' : 'vertical')
)
const collapse = computed(() => appStore.getCollapse)
@ -102,7 +99,8 @@ export default defineComponent({
const routers = computed(() => {
const sourceRouters =
props.menus || (props.split ? permissionStore.getMenuTabRouters : permissionStore.getRouters)
props.menus ||
(props.split ? permissionStore.getMenuTabRouters : permissionStore.getRouters)
if (!props.rootOnly) {
return sourceRouters
}
@ -113,7 +111,10 @@ export default defineComponent({
const { meta, path } = unref(currentRoute)
const currentPath = (meta.activeMenu as string) || path
if (props.rootOnly) {
return permissionStore.getMenuRootPath || getRootMenuActivePath(permissionStore.getRouters, currentPath)
return (
permissionStore.getMenuRootPath ||
getRootMenuActivePath(permissionStore.getRouters, currentPath)
)
}
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
@ -134,7 +135,10 @@ export default defineComponent({
props.theme === 'header' ? 'var(--el-color-primary)' : 'var(--left-menu-text-active-color)'
)
const getFirstChildPath = (route: AppRouteRecordRaw, parentPath: string): string | undefined => {
const getFirstChildPath = (
route: AppRouteRecordRaw,
parentPath: string
): string | undefined => {
const firstChild = route.children?.find((child) => !child.meta?.hidden)
if (!firstChild) {
return undefined
@ -218,7 +222,8 @@ export default defineComponent({
const setSplitMenus = (targetPath: string) => {
const rootInfo = getRootMenuRoute(permissionStore.getRouters, targetPath)
const rootPath = rootInfo?.fullPath || getRootMenuActivePath(permissionStore.getRouters, targetPath)
const rootPath =
rootInfo?.fullPath || getRootMenuActivePath(permissionStore.getRouters, targetPath)
const rootRoute = rootInfo?.route
const children = rootRoute?.children?.length
? cloneDeep(rootRoute.children).map((child) => {
@ -386,7 +391,8 @@ export default defineComponent({
}
const targetPath =
props.rootOnly && routeInfo?.route.children?.length
? ((routeInfo.route.redirect as string) || getFirstChildPath(routeInfo.route, routeInfo.fullPath))
? (routeInfo.route.redirect as string) ||
getFirstChildPath(routeInfo.route, routeInfo.fullPath)
: index
if (targetPath) {
@ -541,9 +547,13 @@ export default defineComponent({
'h-[100%] overflow-hidden flex-col bg-[var(--left-menu-bg-color)]',
{
'w-[var(--left-menu-min-width)]':
unref(collapse) && !isTwoColumnLayout(unref(layout)) && unref(menuMode) !== 'horizontal',
unref(collapse) &&
!isTwoColumnLayout(unref(layout)) &&
unref(menuMode) !== 'horizontal',
'w-[var(--left-menu-max-width)]':
!unref(collapse) && !isTwoColumnLayout(unref(layout)) && unref(menuMode) !== 'horizontal'
!unref(collapse) &&
!isTwoColumnLayout(unref(layout)) &&
unref(menuMode) !== 'horizontal'
}
]}
style={{
@ -642,13 +652,12 @@ $prefix-cls: #{$namespace}-menu;
height: calc(var(--top-tool-height)) !important;
min-width: 0;
flex-shrink: 1;
overflow-x: hidden;
overflow-y: hidden;
overflow: hidden;
:deep(.#{$elNamespace}-menu--horizontal) {
height: calc(var(--top-tool-height));
border-bottom: none;
min-width: 100%;
border-bottom: none;
//
& > .#{$elNamespace}-sub-menu.is-active {
.#{$elNamespace}-sub-menu__title {

View File

@ -33,14 +33,12 @@ export const useRenderMenuItem = () =>
if (
!children.length ||
oneShowingChild &&
(!onlyOneChild?.children || onlyOneChild?.noShowingChildren) &&
!meta?.alwaysShow
(oneShowingChild &&
(!onlyOneChild?.children || onlyOneChild?.noShowingChildren) &&
!meta?.alwaysShow)
) {
return (
<ElMenuItem
index={resolvedOnlyOneChildPath}
>
<ElMenuItem index={resolvedOnlyOneChildPath}>
{{
default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta)
}}

View File

@ -58,9 +58,9 @@ export const getRootMenuRoute = (
}
}
export const getRootMenuActivePath = (
routes: AppRouteRecordRaw[],
targetPath: string
): string => {
return getRootMenuRoute(routes, targetPath)?.fullPath || getRootPath(normalizeMenuTargetPath(targetPath))
export const getRootMenuActivePath = (routes: AppRouteRecordRaw[], targetPath: string): string => {
return (
getRootMenuRoute(routes, targetPath)?.fullPath ||
getRootPath(normalizeMenuTargetPath(targetPath))
)
}

View File

@ -12,10 +12,7 @@ import { useDesign } from '@/hooks/web/useDesign'
import { isUrl } from '@/utils/is'
import { createRouteLocation } from '@/utils/routeParams'
import { getRootPath, isHeaderMixedNavLayout, isTwoColumnLayout } from '@/utils/layout'
import {
getRootMenuRoute,
normalizeMenuTargetPath
} from '@/layout/components/Menu/src/menuRoute'
import { getRootMenuRoute, normalizeMenuTargetPath } from '@/layout/components/Menu/src/menuRoute'
const { getPrefixCls, variables } = useDesign()
@ -58,7 +55,9 @@ export default defineComponent({
return getRootMenuRoute(unref(routers), targetPath)
})
const rootPath = computed(() => unref(activeRootInfo)?.fullPath || getRootPath(unref(currentMenuPath)))
const rootPath = computed(
() => unref(activeRootInfo)?.fullPath || getRootPath(unref(currentMenuPath))
)
const activeRootRoute = computed(() => unref(activeRootInfo)?.route)
@ -125,7 +124,9 @@ export default defineComponent({
const isSameMenuRouters = (left: AppRouteRecordRaw[], right: AppRouteRecordRaw[]): boolean => {
return (
left.length === right.length &&
left.every((route, index) => route.path === right[index]?.path && route.name === right[index]?.name)
left.every(
(route, index) => route.path === right[index]?.path && route.name === right[index]?.name
)
)
}
@ -219,7 +220,8 @@ export default defineComponent({
const children = getVisibleChildren(item)
if (children.length) {
if (newPath === oldPath || !unref(showMenu)) {
showMenu.value = unref(fixedMenu) || unref(headerMixed) || unref(twoColumn) ? true : !unref(showMenu)
showMenu.value =
unref(fixedMenu) || unref(headerMixed) || unref(twoColumn) ? true : !unref(showMenu)
}
if (unref(showMenu)) {
setExtraMenuRouters(buildExtraMenuRouters(children, unref(tabActive)))

View File

@ -171,7 +171,8 @@ const moveToTarget = (currentTag: RouteLocationNormalizedLoaded) => {
} else {
// find preTag and nextTag
const currentIndex: number = tagList.findIndex(
(item) => (item?.to as RouteLocationNormalizedLoaded | undefined)?.fullPath === currentTag.fullPath
(item) =>
(item?.to as RouteLocationNormalizedLoaded | undefined)?.fullPath === currentTag.fullPath
)
if (currentIndex < 0) return
const tgsRefs = document.getElementsByClassName(`${prefixCls}__item`)
@ -376,10 +377,10 @@ watch(
<Icon
v-if="
tagsViewIcon &&
(item?.meta?.icon ||
(item?.matched &&
item.matched[0] &&
item.matched[item.matched.length - 1].meta?.icon))
(item?.meta?.icon ||
(item?.matched &&
item.matched[0] &&
item.matched[item.matched.length - 1].meta?.icon))
"
:icon="item?.meta?.icon || item.matched[item.matched.length - 1].meta.icon"
:size="12"
@ -387,7 +388,7 @@ watch(
/>
{{
t(item?.meta?.title as string) +
(item?.meta?.titleSuffix ? ` (${item?.meta?.titleSuffix})` : '')
(item?.meta?.titleSuffix ? ` (${item?.meta?.titleSuffix})` : '')
}}
<Icon
:class="`${prefixCls}__item--close`"

View File

@ -94,7 +94,11 @@ export default defineComponent({
) : undefined}
<div class="h-full flex items-center">
{hasTenantVisitPermission.value ? <TenantVisit /> : undefined}
<div class="v-setting custom-hover" title={t('setting.projectSetting')} onClick={openSetting}>
<div
class="v-setting custom-hover"
title={t('setting.projectSetting')}
onClick={openSetting}
>
<Icon color="var(--top-header-text-color)" size={18} icon="ep:setting" />
</div>
{screenfull.value ? (

View File

@ -91,7 +91,7 @@ const handleLock = async () => {
<style lang="scss" scoped>
:global(.v-lock-dialog) {
@media (max-width: 767px) {
@media (width <= 767px) {
max-width: calc(100vw - 16px);
}
}

View File

@ -205,6 +205,7 @@ $error-color: #ed6f6f;
font-size: 90px;
}
}
@media screen and (min-width: $screen-lg) {
span:not(.meridiem) {
font-size: 220px;
@ -216,6 +217,7 @@ $error-color: #ed6f6f;
font-size: 260px;
}
}
@media screen and (min-width: $screen-2xl) {
span:not(.meridiem) {
font-size: 320px;
@ -230,7 +232,7 @@ $error-color: #ed6f6f;
display: flex;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
background-color: rgb(0 0 0 / 50%);
backdrop-filter: blur(8px);
justify-content: center;
align-items: center;

View File

@ -9,11 +9,7 @@ import AppView from './AppView.vue'
import ToolHeader from './ToolHeader.vue'
import { ElScrollbar } from 'element-plus'
import { useDesign } from '@/hooks/web/useDesign'
import {
isHeaderMixedNavLayout,
isMixedNavLayout,
isTwoColumnLayout
} from '@/utils/layout'
import { isHeaderMixedNavLayout, isMixedNavLayout, isTwoColumnLayout } from '@/utils/layout'
const { getPrefixCls } = useDesign()
@ -251,7 +247,8 @@ export const useRenderLayout = () => {
const renderCutMenu = () => {
const showHeaderMenu = isHeaderMixedNavLayout(layout.value)
const fixedTwoColumnMenu = fixedMenu.value || isTwoColumnLayout(layout.value)
const showTwoColumnExtraMenu = fixedTwoColumnMenu && permissionStore.getMenuTabRouters.length > 0
const showTwoColumnExtraMenu =
fixedTwoColumnMenu && permissionStore.getMenuTabRouters.length > 0
return (
<>
<div class="relative flex items-center bg-[var(--top-header-bg-color)] layout-border__bottom">

View File

@ -173,7 +173,6 @@ export const PREDEFINE_COLORS = [
'#711f57'
]
/**
* Mixes two colors.
*

View File

@ -172,43 +172,43 @@ export const once = function (el: HTMLElement, event: string, fn: EventListener)
export const getStyle =
ieVersion < 9
? function (element: Element | any, styleName: string) {
if (isServer) return
if (!element || !styleName) return null
styleName = camelCase(styleName)
if (styleName === 'float') {
styleName = 'styleFloat'
}
try {
switch (styleName) {
case 'opacity':
try {
return element.filters.item('alpha').opacity / 100
} catch (e) {
return 1.0
}
default:
return element.style[styleName] || element.currentStyle
? element.currentStyle[styleName]
: null
if (isServer) return
if (!element || !styleName) return null
styleName = camelCase(styleName)
if (styleName === 'float') {
styleName = 'styleFloat'
}
try {
switch (styleName) {
case 'opacity':
try {
return element.filters.item('alpha').opacity / 100
} catch (e) {
return 1.0
}
default:
return element.style[styleName] || element.currentStyle
? element.currentStyle[styleName]
: null
}
} catch (e) {
return element.style[styleName]
}
} catch (e) {
return element.style[styleName]
}
}
: function (element: Element | any, styleName: string) {
if (isServer) return
if (!element || !styleName) return null
styleName = camelCase(styleName)
if (styleName === 'float') {
styleName = 'cssFloat'
if (isServer) return
if (!element || !styleName) return null
styleName = camelCase(styleName)
if (styleName === 'float') {
styleName = 'cssFloat'
}
try {
const computed = (document as any).defaultView.getComputedStyle(element, '')
return element.style[styleName] || computed ? computed[styleName] : null
} catch (e) {
return element.style[styleName]
}
}
try {
const computed = (document as any).defaultView.getComputedStyle(element, '')
return element.style[styleName] || computed ? computed[styleName] : null
} catch (e) {
return element.style[styleName]
}
}
/* istanbul ignore next */
export function setStyle(element: Element | any, styleName: any, value: any) {
@ -287,4 +287,3 @@ export const isInContainer = (el: Element, container: any) => {
elRect.left < containerRect.right
)
}

View File

@ -216,7 +216,9 @@ export function formatPast2(ms: number): string {
*/
export function formatSeconds(seconds: number): string {
const s = Math.max(0, Math.floor(seconds || 0))
const mm = Math.floor(s / 60).toString().padStart(2, '0')
const mm = Math.floor(s / 60)
.toString()
.padStart(2, '0')
const ss = (s % 60).toString().padStart(2, '0')
return `${mm}:${ss}`
}

View File

@ -215,10 +215,10 @@ export const generateUUID = () => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
let random = Math.random() * 16
if (timestamp > 0) {
random = (timestamp + random) % 16 | 0
random = ((timestamp + random) % 16) | 0
timestamp = Math.floor(timestamp / 16)
} else {
random = (performanceNow + random) % 16 | 0
random = ((performanceNow + random) % 16) | 0
performanceNow = Math.floor(performanceNow / 16)
}
return (c === 'x' ? random : (random & 0x3) | 0x8).toString(16)

View File

@ -100,7 +100,7 @@ export const isClient = !isServer
export const isUrl = (path: string): boolean => {
// fix:修复hash路由无法跳转的问题
const reg =
/(((^https?:(?:\/\/)?)(?:[-:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%#\/.\w-_]*)?\??(?:[-\+=&%@.\w_]*)#?(?:[\w]*))?)$/
/(((^https?:(?:\/\/)?)(?:[-:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%#\/.\w-_]*)?\??(?:[-\+=&%@.\w_]*)#?(?:[\w]*))?)$/
return reg.test(path)
}

View File

@ -40,7 +40,9 @@ export const isHeaderNavLayout = (layout?: LayoutType | string | null): boolean
export const isHorizontalMenuLayout = (layout?: LayoutType | string | null): boolean => {
const normalized = normalizeLayout(layout)
return normalized === 'header-nav' || normalized === 'mixed-nav' || normalized === 'header-mixed-nav'
return (
normalized === 'header-nav' || normalized === 'mixed-nav' || normalized === 'header-mixed-nav'
)
}
export const isMixedNavLayout = (layout?: LayoutType | string | null): boolean => {

View File

@ -1,6 +1,5 @@
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import {hasPermission} from "@/directives/permission/hasPermi";
import { hasPermission } from '@/directives/permission/hasPermi'
const { t } = useI18n() // 国际化

View File

@ -23,9 +23,7 @@ export const parseQueryString = (queryString = ''): Record<string, any> => {
return qs.parse(queryString.replace(/^\?/, '')) as Record<string, any>
}
export const splitRoutePath = (
rawPath: string | null | undefined
): ParsedRouteLocation => {
export const splitRoutePath = (rawPath: string | null | undefined): ParsedRouteLocation => {
if (!rawPath) {
return { path: '' }
}
@ -113,7 +111,8 @@ export const splitDynamicRouteParams = (
}
}
const isRepeatableParam = (matched: string): boolean => matched.endsWith('*') || matched.endsWith('+')
const isRepeatableParam = (matched: string): boolean =>
matched.endsWith('*') || matched.endsWith('+')
const encodeRouteParam = (matched: string, value: any): string => {
if (Array.isArray(value)) {

View File

@ -113,7 +113,7 @@ const iconHouse = useIcon({ icon: 'ep:house' })
const iconAvatar = useIcon({ icon: 'ep:avatar' })
const iconLock = useIcon({ icon: 'ep:lock' })
const formLogin = ref()
const {validForm} = useFormValid(formLogin)
const { validForm } = useFormValid(formLogin)
const { handleBackLogin, getLoginState } = useLoginState()
const { currentRoute, push } = useRouter()
const permissionStore = usePermissionStore()

View File

@ -6,4 +6,12 @@ import QrCodeForm from './QrCodeForm.vue'
import SSOLoginVue from './SSOLogin.vue'
import ForgetPasswordForm from './ForgetPasswordForm.vue'
export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue, ForgetPasswordForm }
export {
LoginForm,
MobileForm,
LoginFormTitle,
RegisterForm,
QrCodeForm,
SSOLoginVue,
ForgetPasswordForm
}

View File

@ -1,8 +1,5 @@
<template>
<el-card
body-class=""
class="!w-80 !h-auto !rounded-10px !relative !flex !flex-col"
>
<el-card body-class="" class="!w-80 !h-auto !rounded-10px !relative !flex !flex-col">
<div class="!flex !flex-row !justify-between">
<div>
<el-button type="primary" text bg v-if="detail?.status === AiImageStatusEnum.IN_PROGRESS">

View File

@ -37,7 +37,11 @@
</div>
<el-space wrap class="mt-15px">
<div
: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'"
: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"
:key="model.key"
>
@ -52,7 +56,11 @@
</div>
<el-space wrap class="mt-15px">
<div
: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'"
: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"
:key="imageStyle.key"
>
@ -73,7 +81,11 @@
@click="handleSizeClick(imageSize)"
>
<div
: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'"
: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>
@ -229,4 +241,3 @@ const settingValues = async (detail: ImageVO) => {
/** 暴露组件方法 */
defineExpose({ settingValues })
</script>

View File

@ -43,7 +43,11 @@
@click="handleSizeClick(imageSize)"
>
<div
: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'"
: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>
@ -57,7 +61,11 @@
</div>
<el-space wrap class="mt-15px">
<div
: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'"
: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"
:key="model.key"
>
@ -71,12 +79,7 @@
<el-text tag="b">版本</el-text>
</div>
<el-space wrap class="mt-20px w-full">
<el-select
v-model="selectVersion"
class="!w-350px"
clearable
placeholder="请选择版本"
>
<el-select v-model="selectVersion" class="!w-350px" clearable placeholder="请选择版本">
<el-option
v-for="item in versionList"
:key="item.value"
@ -233,4 +236,3 @@ const settingValues = async (detail: ImageVO) => {
/** 暴露组件方法 */
defineExpose({ settingValues })
</script>

View File

@ -254,4 +254,3 @@ const settingValues = async (detail: ImageVO) => {
/** 暴露组件方法 */
defineExpose({ settingValues })
</script>

View File

@ -10,10 +10,19 @@
:suffix-icon="Search"
@keyup.enter="handleQuery"
/>
<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)]">
<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 界面一致只有卡片没有操作因为看着更有相框的感觉~~~ -->
<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="w-full h-auto block transition-transform duration-300 hover:scale-110" />
<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="w-full h-auto block transition-transform duration-300 hover:scale-110"
/>
</div>
</div>
<!-- TODO @fan缺少翻页 -->
@ -64,4 +73,3 @@ onMounted(async () => {
await getList()
})
</script>

View File

@ -11,12 +11,20 @@
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="modelData.formType === BpmModelFormType.NORMAL" label="流程表单" prop="formId">
<el-form-item
v-if="modelData.formType === BpmModelFormType.NORMAL"
label="流程表单"
prop="formId"
>
<el-select v-model="modelData.formId" clearable style="width: 100%">
<el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" />
</el-select>
</el-form-item>
<el-form-item v-if="modelData.formType === BpmModelFormType.CUSTOM" label="表单提交路由" prop="formCustomCreatePath">
<el-form-item
v-if="modelData.formType === BpmModelFormType.CUSTOM"
label="表单提交路由"
prop="formCustomCreatePath"
>
<el-input
v-model="modelData.formCustomCreatePath"
placeholder="请输入表单提交路由"
@ -31,7 +39,11 @@
<Icon icon="ep:question" class="ml-5px" />
</el-tooltip>
</el-form-item>
<el-form-item v-if="modelData.formType === BpmModelFormType.CUSTOM" label="表单查看地址" prop="formCustomViewPath">
<el-form-item
v-if="modelData.formType === BpmModelFormType.CUSTOM"
label="表单查看地址"
prop="formCustomViewPath"
>
<el-input
v-model="modelData.formCustomViewPath"
placeholder="请输入表单查看的组件地址"
@ -48,7 +60,11 @@
</el-form-item>
<!-- 表单预览 -->
<div
v-if="modelData.formType === BpmModelFormType.NORMAL && modelData.formId && formPreview.rule.length > 0"
v-if="
modelData.formType === BpmModelFormType.NORMAL &&
modelData.formId &&
formPreview.rule.length > 0
"
class="mt-20px"
>
<div class="flex items-center mb-15px">

View File

@ -86,7 +86,7 @@ onBeforeUnmount(() => {
/>
</div>
<!-- TODO @unocss 简化 style -->
<div style=" margin: 10px;border: 1px solid #ccc">
<div style="margin: 10px; border: 1px solid #ccc">
<Toolbar
style="border-bottom: 1px solid #ccc"
:editor="editorRef"
@ -106,7 +106,7 @@ onBeforeUnmount(() => {
@insert-mention="insertMention"
/>
</div>
<div style=" float: right;margin-right: 10px">
<div style="float: right; margin-right: 10px">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="handleConfirm"> </el-button>
</div>

View File

@ -15,11 +15,10 @@ function parseHtml(
): SlateElement {
// TS 语法
// 生成“流程记录”元素(按照此前约定的数据结构)
const processRecord = {
type: 'process-record',
children: [{ text: '' }], // void node 必须有 children ,其中有一个空字符串,重要!!!
children: [{ text: '' }] // void node 必须有 children ,其中有一个空字符串,重要!!!
}
return processRecord

View File

@ -220,7 +220,7 @@ onActivated(() => {
.el-form--inline .el-form-item {
margin-right: 10px;
}
.el-divider--horizontal {
margin-top: 6px;
}

View File

@ -268,7 +268,7 @@ const submitForm = async () => {
if (!fApi.value || !props.selectProcessDefinition) {
return
}
try {
//
await fApi.value.validate()

View File

@ -252,11 +252,7 @@ const flattenAreaTree = (list: AreaNode[] = [], map: Map<string, string> = new M
return map
}
const mapValueWithLabelMap = (
value: unknown,
labelMap: Map<string, string>,
separator = ', '
) => {
const mapValueWithLabelMap = (value: unknown, labelMap: Map<string, string>, separator = ', ') => {
const values = toValueArray(value)
const labels = values
.map((item) => escapeHtml(labelMap.get(String(item)) ?? String(item)))
@ -300,11 +296,7 @@ const loadPrintLookupMaps = async (formFieldsObj: FormFieldRule[]) => {
} satisfies PrintLookupMaps
}
const formatPrintField = (
rule: FormFieldRule,
value: unknown,
lookupMaps: PrintLookupMaps
) => {
const formatPrintField = (rule: FormFieldRule, value: unknown, lookupMaps: PrintLookupMaps) => {
const type = String(rule.type ?? '')
switch (type) {
@ -356,7 +348,9 @@ const formatPrintField = (
return renderFileListHtml(value)
case 'IframeComponent': {
const propsObj = rule.props
const propsUrl = isPrintableRecord(propsObj) ? String(getRecordValue(propsObj, 'url') ?? '') : ''
const propsUrl = isPrintableRecord(propsObj)
? String(getRecordValue(propsObj, 'url') ?? '')
: ''
const iframeUrl = isEmptyValue(value) ? propsUrl : String(value ?? '')
return iframeUrl ? createFileLinkHtml(iframeUrl) : ''
}
@ -395,10 +389,10 @@ const initPrintDataMap = () => {
printDataMap.value['processNum'] = String(printData.value.processInstance.id ?? '')
printDataMap.value['startTime'] = formatDate(printData.value.processInstance.startTime)
printDataMap.value['endTime'] = formatDate(printData.value.processInstance.endTime)
printDataMap.value['processStatus'] = String(getDictLabel(
DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS,
printData.value.processInstance.status
) ?? '')
printDataMap.value['processStatus'] = String(
getDictLabel(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS, printData.value.processInstance.status) ??
''
)
printDataMap.value['printUser'] = userName.value
printDataMap.value['printTime'] = printTime.value
}

View File

@ -87,7 +87,12 @@
{{ formatPast2(scope.row.durationInMillis) }}
</template>
</el-table-column>
<el-table-column align="center" label="流程编号" prop="processInstanceId" :show-overflow-tooltip="true" />
<el-table-column
align="center"
label="流程编号"
prop="processInstanceId"
:show-overflow-tooltip="true"
/>
<el-table-column align="center" label="任务编号" prop="id" :show-overflow-tooltip="true" />
<el-table-column align="center" label="操作" fixed="right" width="80">
<template #default="scope">

View File

@ -77,8 +77,7 @@ const getList = async () => {
const currentUserId = getCurrentUserId()
const permission = list.value.find(
(item) =>
item.userId === currentUserId &&
item.level === PermissionApi.PermissionLevelEnum.OWNER
item.userId === currentUserId && item.level === PermissionApi.PermissionLevelEnum.OWNER
)
if (permission) {
formData.value.ownerUserId = currentUserId

View File

@ -46,7 +46,7 @@
class="!w-240px"
node-key="id"
placeholder="请选择归属部门"
@change="(queryParams.userId = undefined), handleQuery()"
@change="((queryParams.userId = undefined), handleQuery())"
/>
</el-form-item>
<el-form-item label="员工" prop="userId">

View File

@ -46,7 +46,7 @@
class="!w-240px"
node-key="id"
placeholder="请选择归属部门"
@change="(queryParams.userId = undefined), handleQuery()"
@change="((queryParams.userId = undefined), handleQuery())"
/>
</el-form-item>
<el-form-item label="员工" prop="userId">

View File

@ -11,10 +11,7 @@
:index="idx"
:key="resolveItemKey(item, idx)"
></slot>
<div
v-if="showFooter"
class="py-3 text-xs text-center text-[var(--el-text-color-secondary)]"
>
<div v-if="showFooter" class="py-3 text-xs text-center text-[var(--el-text-color-secondary)]">
已到底部
</div>
</el-scrollbar>

View File

@ -15,7 +15,9 @@
title="拖拽调整宽度"
@mousedown="startResize"
>
<div class="im-resizable-aside__line w-0.5 h-full rounded-0.5 bg-transparent transition-all"></div>
<div
class="im-resizable-aside__line w-0.5 h-full rounded-0.5 bg-transparent transition-all"
></div>
</div>
</aside>
</template>

View File

@ -42,11 +42,7 @@ import { computed } from 'vue'
import UserAvatar from '../user/UserAvatar.vue'
import { isPrivateConversation } from '@/views/im/utils/constants'
import {
getCardLabelInfo,
type CardMessage,
type CardTarget
} from '@/views/im/utils/message'
import { getCardLabelInfo, type CardMessage, type CardTarget } from '@/views/im/utils/message'
defineOptions({ name: 'ImCardBubble' })

View File

@ -168,9 +168,7 @@ const message = useMessage()
const currentUserId = computed(() => getCurrentUserId())
/** 搜索结果过滤掉自己;用 v-if 而非 v-show避免 DOM 占位 + 头像无效请求 */
const visibleUsers = computed(() =>
users.value.filter((user) => user.id !== currentUserId.value)
)
const visibleUsers = computed(() => users.value.filter((user) => user.id !== currentUserId.value))
const keyword = ref('')
const users = ref<UserVO[]>([])
const searched = ref(false)

View File

@ -17,11 +17,7 @@ import { computed, ref, watch } from 'vue'
import UserAvatar from '../user/UserAvatar.vue'
import { useFriendStore } from '../../store/friendStore'
import { useGroupStore } from '../../store/groupStore'
import {
buildGroupAvatar,
getCachedGroupAvatar,
setCachedGroupAvatar
} from '../../../utils/group'
import { buildGroupAvatar, getCachedGroupAvatar, setCachedGroupAvatar } from '../../../utils/group'
import { getMemberDisplayName } from '../../../utils/user'
import type { GroupMember } from '../../types'

View File

@ -60,9 +60,7 @@ onUnmounted(() => window.removeEventListener('keydown', handleKeydown))
function handleChat(group: GroupLite) {
const cached = groupStore.getGroup(group.id)
// cached getGroupDisplayName contact / cached showGroupName /
const displayName = cached
? getGroupDisplayName(cached)
: group.showGroupName || group.name || ''
const displayName = cached ? getGroupDisplayName(cached) : group.showGroupName || group.name || ''
//
conversationStore.openConversation(
group.id,

View File

@ -3,13 +3,9 @@
<el-dialog v-model="visible" title="设置禁言" width="560px" :close-on-click-modal="false">
<div class="flex flex-col gap-4">
<!-- 成员信息卡 FriendAddDialog user 卡保持一致的浅色背景 -->
<div
class="flex items-center gap-2 px-3 py-2.5 rounded-md bg-[var(--el-fill-color-light)]"
>
<div class="flex items-center gap-2 px-3 py-2.5 rounded-md bg-[var(--el-fill-color-light)]">
<span class="text-13px text-[var(--el-text-color-secondary)]">禁言成员</span>
<span
class="text-sm font-medium text-[var(--el-text-color-primary)] truncate"
>
<span class="text-sm font-medium text-[var(--el-text-color-primary)] truncate">
{{ memberName }}
</span>
</div>

View File

@ -313,22 +313,23 @@ function updateLocalResult(id: number, handleResult: number) {
<style scoped>
/* 自绘按钮:贴近微信小药丸样式;与 :disabled、:hover:not(:disabled) 等伪类叠加 modifier 类的组合选择器写在 class 里成本高,留 SCSS */
.im-group-request-list__btn {
flex-shrink: 0;
min-width: 56px;
height: 28px;
min-width: 56px;
padding: 0 12px;
font-size: 13px;
border-radius: 4px;
cursor: pointer;
border: 1px solid transparent;
border-radius: 4px;
transition:
background-color 0.15s,
border-color 0.15s,
color 0.15s;
flex-shrink: 0;
}
.im-group-request-list__btn:disabled {
opacity: 0.6;
cursor: not-allowed;
opacity: 0.6;
}
.im-group-request-list__btn--primary {
@ -336,6 +337,7 @@ function updateLocalResult(id: number, handleResult: number) {
background-color: var(--el-color-primary);
border-color: var(--el-color-primary);
}
.im-group-request-list__btn--primary:hover:not(:disabled) {
background-color: var(--el-color-primary-light-3);
border-color: var(--el-color-primary-light-3);
@ -346,6 +348,7 @@ function updateLocalResult(id: number, handleResult: number) {
background-color: var(--el-bg-color);
border-color: var(--el-border-color);
}
.im-group-request-list__btn--ghost:hover:not(:disabled) {
color: var(--el-color-primary);
border-color: var(--el-color-primary);

View File

@ -363,6 +363,7 @@ function handleToggle(conversation: Conversation) {
.im-conversation-picker__recent::-webkit-scrollbar {
height: 4px;
}
.im-conversation-picker__recent::-webkit-scrollbar-thumb {
background-color: var(--el-border-color);
border-radius: 2px;

View File

@ -181,9 +181,7 @@ const disabledSet = computed(() => new Set(props.disabledIds))
const selectedSet = computed(() => new Set(props.selectedIds))
/** 候选好友:剔除 hideIdshide 优先级最高) */
const candidates = computed(() =>
props.friends.filter((friend) => !hideSet.value.has(friend.id))
)
const candidates = computed(() => props.friends.filter((friend) => !hideSet.value.has(friend.id)))
/** 委托 useFriendBuckets搜索规则复用左侧列表按滚动分页渲染 */
const { filtered } = useFriendBuckets(candidates, keyword)

View File

@ -24,7 +24,7 @@
<PagedScroller :items="shownMembers" :page-size="30" item-key="userId">
<template #default="{ item }">
<GroupMemberItem
:member="(item as GroupMemberLite)"
:member="item as GroupMemberLite"
:height="46"
@click="handleToggle(item as GroupMemberLite)"
>
@ -86,11 +86,7 @@
<!-- grid 形态宫格预览 locked 成员右上角叠加 × 移除locked 不渲染 -->
<div v-else class="flex flex-wrap p-2.5">
<GroupMemberGrid
v-for="member in selectedMembers"
:key="member.userId"
:member="member"
>
<GroupMemberGrid v-for="member in selectedMembers" :key="member.userId" :member="member">
<Icon
v-if="!isLocked(member)"
icon="ant-design:close-circle-filled"
@ -240,4 +236,3 @@ function handleToggle(member: GroupMemberLite) {
emit('update:selectedIds', next)
}
</script>

View File

@ -71,11 +71,7 @@ import {
inviteCall,
noAnswerCallCheck
} from '@/api/im/rtc'
import {
ImRtcCallMediaType,
ImRtcCallStage,
ImConversationType
} from '@/views/im/utils/constants'
import { ImRtcCallMediaType, ImRtcCallStage, ImConversationType } from '@/views/im/utils/constants'
import { RTC_NO_ANSWER_CALL_CHECK_INTERVAL_MS } from '@/views/im/utils/config'
import { getCurrentUserId } from '@/utils/auth'
import { getSenderAvatar, getSenderDisplayName } from '@/views/im/utils/user'
@ -104,17 +100,15 @@ const hangingUp = ref(false)
/** 当前是否视频通话 */
const isVideo = computed(() => {
const t =
rtcStore.call?.mediaType ||
rtcStore.incomingPayload?.mediaType ||
ImRtcCallMediaType.VOICE
rtcStore.call?.mediaType || rtcStore.incomingPayload?.mediaType || ImRtcCallMediaType.VOICE
return t === ImRtcCallMediaType.VIDEO
})
/** 当前是否群通话;决定浮动窗大小 */
const isGroup = computed(
() =>
(rtcStore.call?.conversationType ??
rtcStore.incomingPayload?.conversationType) === ImConversationType.GROUP
(rtcStore.call?.conversationType ?? rtcStore.incomingPayload?.conversationType) ===
ImConversationType.GROUP
)
/** 初始摄像头是否打开;群通话默认全部关闭,进入后用户主动开 */
@ -261,11 +255,7 @@ function maybeEnterRunning() {
watch(
() => rtcStore.stage,
async (stage) => {
if (
stage === ImRtcCallStage.INVITING &&
rtcStore.call?.token &&
rtcStore.call?.livekitUrl
) {
if (stage === ImRtcCallStage.INVITING && rtcStore.call?.token && rtcStore.call?.livekitUrl) {
try {
await connectLiveKit(rtcStore.call.livekitUrl, rtcStore.call.token)
} catch (e) {
@ -417,7 +407,9 @@ function handlePeerDisconnected() {
/** 通话存活期间INVITING / INCOMING / RUNNING周期性触发后端扫该 room 的超时 INVITING保持 timer 是为了 inviteCall 追加新人后也能覆盖;阈值由后端配置决定,前端只负责 trigger */
const { resume: resumeNoAnswerTimer, pause: pauseNoAnswerTimer } = useIntervalFn(
triggerNoAnswerCallCheck, RTC_NO_ANSWER_CALL_CHECK_INTERVAL_MS, { immediate: false }
triggerNoAnswerCallCheck,
RTC_NO_ANSWER_CALL_CHECK_INTERVAL_MS,
{ immediate: false }
)
watch(
() => rtcStore.isActive,

View File

@ -97,12 +97,14 @@ const audioRef = useMediaStreamElement<HTMLAudioElement>(() => props.participant
.tile-dot {
animation: tile-dot 1.4s infinite ease-in-out both;
}
@keyframes tile-dot {
0%,
80%,
100% {
opacity: 0.3;
}
40% {
opacity: 1;
}

View File

@ -85,9 +85,7 @@
</div>
<!-- 底部操作区麦克风 / 扬声器 / 摄像头 / (群聊共享屏幕 / 添加成员) / 挂断 -->
<div
class="flex flex-shrink-0 gap-3 justify-around items-center pt-4 px-4 pb-5 bg-black/20"
>
<div class="flex flex-shrink-0 gap-3 justify-around items-center pt-4 px-4 pb-5 bg-black/20">
<div
class="flex flex-col gap-2 items-center cursor-pointer select-none min-w-[64px]"
@click="$emit('toggle-mic')"
@ -164,7 +162,9 @@
class="flex flex-col gap-2 items-center cursor-pointer select-none min-w-[64px]"
@click="$emit('add-member')"
>
<span class="flex justify-center items-center w-[52px] h-[52px] text-white rounded-full bg-white/15">
<span
class="flex justify-center items-center w-[52px] h-[52px] text-white rounded-full bg-white/15"
>
<Icon icon="ant-design:plus-outlined" :size="22" />
</span>
<span class="text-xs text-white/70 whitespace-nowrap">添加成员</span>
@ -175,7 +175,9 @@
:class="{ 'opacity-60 pointer-events-none': hangingUp }"
@click="$emit('hangup')"
>
<span class="flex justify-center items-center w-[52px] h-[52px] text-white rounded-full bg-[#f04a4a]">
<span
class="flex justify-center items-center w-[52px] h-[52px] text-white rounded-full bg-[#f04a4a]"
>
<Icon icon="ant-design:phone-outlined" :size="22" class="rotate-[135deg]" />
</span>
<span class="text-xs text-white/70 whitespace-nowrap">挂断</span>
@ -277,11 +279,13 @@ const formattedDuration = computed(() =>
.reconnect-dot {
animation: reconnect-pulse 1s ease-in-out infinite;
}
@keyframes reconnect-pulse {
0%,
100% {
opacity: 0.3;
}
50% {
opacity: 1;
}

View File

@ -231,7 +231,9 @@ async function handleSend() {
const results = await Promise.all(tasks)
const failedNames = results.filter((r) => !r.ok).map((r) => r.conversation.name || '未命名会话')
// ""
conversationStore.pushRecentForwardConversationKeyList(targets.map((c) => getConversationKey(c)))
conversationStore.pushRecentForwardConversationKeyList(
targets.map((c) => getConversationKey(c))
)
if (failedNames.length === 0) {
message.success('已转发')
} else if (failedNames.length === targets.length) {

View File

@ -101,9 +101,7 @@ function handleSendMessage() {
}
// friendStore /
const friend = friendStore.getFriend(user.value.id)
const conversationName = friend
? getFriendDisplayName(friend)
: user.value.nickname || ''
const conversationName = friend ? getFriendDisplayName(friend) : user.value.nickname || ''
conversationStore.openConversation(
user.value.id,
ImConversationType.PRIVATE,

View File

@ -81,12 +81,12 @@ export function useLiveKitRoom() {
_room.value = r
r.on(RoomEvent.ParticipantConnected, (rp) => {
syncRemotes(r)
const userId = parseUserId(rp.identity)
if (userId != null) {
participantConnectedHandlers.forEach((cb) => cb(userId))
}
})
syncRemotes(r)
const userId = parseUserId(rp.identity)
if (userId != null) {
participantConnectedHandlers.forEach((cb) => cb(userId))
}
})
.on(RoomEvent.ParticipantDisconnected, (rp) => {
syncRemotes(r)
// 离开的参与者缓存清掉,避免下次同 sid 重连命中失效引用

View File

@ -198,15 +198,17 @@ export const useMediaUploader = () => {
uploadProgress: 0,
_localFile: opts.file
}
void messageStore.insertMessage(
{
type: conversation.type,
targetId: conversation.targetId,
name: conversation.name || String(conversation.targetId),
avatar: conversation.avatar || ''
},
placeholder
).catch(() => undefined)
void messageStore
.insertMessage(
{
type: conversation.type,
targetId: conversation.targetId,
name: conversation.name || String(conversation.targetId),
avatar: conversation.avatar || ''
},
placeholder
)
.catch(() => undefined)
return { clientMessageId, blobUrl }
}

View File

@ -113,14 +113,10 @@ const currentUserId = computed(() => getCurrentUserId())
const iSentIt = computed(() => props.request.fromUserId === currentUserId.value)
/** 是否「已拒绝」态模板里多处用到computed 一次省得到处写枚举比对 */
const refused = computed(
() => props.request.handleResult === ImFriendRequestHandleResult.REFUSED
)
const refused = computed(() => props.request.handleResult === ImFriendRequestHandleResult.REFUSED)
/** 是否「已通过」态:转走 UserInfo 好友详情入口 */
const agreed = computed(
() => props.request.handleResult === ImFriendRequestHandleResult.AGREED
)
const agreed = computed(() => props.request.handleResult === ImFriendRequestHandleResult.AGREED)
/** 对端的用户编号 / 昵称 / 头像 */
const peerUserId = computed(() =>

View File

@ -39,10 +39,15 @@
title="邀请好友入群"
@click="handleOpenInvite"
>
<div class="im-conversation-group-side__icon-tile flex items-center justify-center w-[50px] h-[50px] text-20px text-[var(--el-text-color-regular)] bg-[var(--el-fill-color-lighter)] border border-dashed border-[var(--el-border-color)] rounded-md transition-colors duration-200">
<div
class="im-conversation-group-side__icon-tile flex items-center justify-center w-[50px] h-[50px] text-20px text-[var(--el-text-color-regular)] bg-[var(--el-fill-color-lighter)] border border-dashed border-[var(--el-border-color)] rounded-md transition-colors duration-200"
>
<Icon icon="ant-design:plus-outlined" />
</div>
<div class="mt-1.5 text-12px leading-[1.5] text-[var(--el-text-color-regular)] text-center">添加</div>
<div
class="mt-1.5 text-12px leading-[1.5] text-[var(--el-text-color-regular)] text-center"
>添加</div
>
</div>
<!-- 移出群主或管理员管理员只能移出普通成员由后端校验 -->
@ -52,10 +57,15 @@
title="移出群成员"
@click="handleOpenRemove"
>
<div class="im-conversation-group-side__icon-tile flex items-center justify-center w-[50px] h-[50px] text-20px text-[var(--el-text-color-regular)] bg-[var(--el-fill-color-lighter)] border border-dashed border-[var(--el-border-color)] rounded-md transition-colors duration-200">
<div
class="im-conversation-group-side__icon-tile flex items-center justify-center w-[50px] h-[50px] text-20px text-[var(--el-text-color-regular)] bg-[var(--el-fill-color-lighter)] border border-dashed border-[var(--el-border-color)] rounded-md transition-colors duration-200"
>
<Icon icon="ant-design:minus-outlined" />
</div>
<div class="mt-1.5 text-12px leading-[1.5] text-[var(--el-text-color-regular)] text-center">移出</div>
<div
class="mt-1.5 text-12px leading-[1.5] text-[var(--el-text-color-regular)] text-center"
>移出</div
>
</div>
</div>
@ -87,8 +97,13 @@
<div
class="im-conversation-group-side__row flex flex-col items-stretch gap-1.5 px-4 py-[14px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--el-fill-color-lighter)]"
>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">群聊名称</span>
<span class="text-13px text-[var(--el-text-color-regular)] break-all leading-[1.6] truncate">{{ group.name }}</span>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]"
>群聊名称</span
>
<span
class="text-13px text-[var(--el-text-color-regular)] break-all leading-[1.6] truncate"
>{{ group.name }}</span
>
</div>
</template>
<div class="flex flex-col gap-2">
@ -103,8 +118,13 @@
v-else
class="im-conversation-group-side__row flex flex-col items-stretch gap-1.5 px-4 py-[14px] text-14px min-h-6 transition-colors duration-150"
>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">群聊名称</span>
<span class="text-13px text-[var(--el-text-color-regular)] break-all leading-[1.6] truncate">{{ group.name }}</span>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]"
>群聊名称</span
>
<span
class="text-13px text-[var(--el-text-color-regular)] break-all leading-[1.6] truncate"
>{{ group.name }}</span
>
</div>
<!-- 群公告群主可改内容可能很长 > chevron 表示可展开编辑 -->
@ -120,7 +140,9 @@
class="im-conversation-group-side__row flex flex-col items-stretch gap-1.5 px-4 py-[14px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--el-fill-color-lighter)]"
>
<div class="flex items-center justify-between gap-2">
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">群公告</span>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]"
>群公告</span
>
<Icon
icon="ant-design:right-outlined"
:size="11"
@ -133,7 +155,9 @@
>
{{ group.notice }}
</span>
<span v-else class="text-13px text-[var(--el-text-color-placeholder)] leading-[1.6]">未设置</span>
<span v-else class="text-13px text-[var(--el-text-color-placeholder)] leading-[1.6]"
>未设置</span
>
</div>
</template>
<div class="flex flex-col gap-2">
@ -162,7 +186,9 @@
>
{{ group.notice }}
</span>
<span v-else class="text-13px text-[var(--el-text-color-placeholder)] leading-[1.6]">未设置</span>
<span v-else class="text-13px text-[var(--el-text-color-placeholder)] leading-[1.6]"
>未设置</span
>
</div>
<!-- 备注仅自己可见保存后会替换会话列表 / 顶部群名展示历史退群群隐藏改备注走 updateGroupMember已退群会被后端拒 -->
@ -177,14 +203,19 @@
<div
class="im-conversation-group-side__row flex flex-col items-stretch gap-1.5 px-4 py-[14px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--el-fill-color-lighter)]"
>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">备注</span>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]"
>备注</span
>
<span
v-if="group.groupRemark"
class="text-13px text-[var(--el-text-color-regular)] break-all leading-[1.6] line-clamp-2"
>
{{ group.groupRemark }}
</span>
<span v-else class="text-13px text-[var(--el-text-color-placeholder)] leading-[1.6]">
<span
v-else
class="text-13px text-[var(--el-text-color-placeholder)] leading-[1.6]"
>
群聊的备注仅自己可见
</span>
</div>
@ -217,14 +248,18 @@
<div
class="im-conversation-group-side__row flex flex-col items-stretch gap-1.5 px-4 py-[14px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--el-fill-color-lighter)]"
>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">我在本群的昵称</span>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]"
>我在本群的昵称</span
>
<span
v-if="group.remarkNickName"
class="text-13px text-[var(--el-text-color-regular)] break-all leading-[1.6] truncate"
>
{{ group.remarkNickName }}
</span>
<span v-else class="text-13px text-[var(--el-text-color-placeholder)] leading-[1.6]">点击设置</span>
<span v-else class="text-13px text-[var(--el-text-color-placeholder)] leading-[1.6]"
>点击设置</span
>
</div>
</template>
<div class="flex flex-col gap-2">
@ -249,7 +284,9 @@
class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--el-fill-color-lighter)]"
@click="emit('open-history')"
>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">查找聊天内容</span>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]"
>查找聊天内容</span
>
<Icon
icon="ant-design:right-outlined"
:size="11"
@ -262,7 +299,9 @@
class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--el-fill-color-lighter)]"
@click="handleShareGroupCard"
>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">分享群名片</span>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]"
>分享群名片</span
>
<Icon
icon="ant-design:right-outlined"
:size="11"
@ -275,17 +314,30 @@
<!-- ==================== 开关项 ==================== -->
<div class="bg-[var(--el-bg-color)]">
<div class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 transition-colors duration-150">
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">消息免打扰</span>
<div
class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 transition-colors duration-150"
>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]"
>消息免打扰</span
>
<el-switch :model-value="!!conversation?.silent" @change="onMutedChange" />
</div>
<div class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 transition-colors duration-150">
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">置顶聊天</span>
<div
class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 transition-colors duration-150"
>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]"
>置顶聊天</span
>
<el-switch :model-value="!!conversation?.top" @change="onTopChange" />
</div>
<!-- 全群禁言仅群主或管理员可操作 -->
<div v-if="isOwnerOrAdmin" class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 transition-colors duration-150">
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">全群禁言</span>
<div
v-if="isOwnerOrAdmin"
class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 transition-colors duration-150"
>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]"
>全群禁言</span
>
<el-switch :model-value="!!currentMutedAll" @change="onMuteAllChange" />
</div>
</div>
@ -296,8 +348,13 @@
<div class="flex-shrink-0 h-[10px]"></div>
<div class="bg-[var(--el-bg-color)]">
<!-- 进群审批仅群主可操作开启后普通成员的申请邀请路径都需群主 / 管理员同意群主 / 管理员邀请直进 -->
<div v-if="isOwner" class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 transition-colors duration-150">
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">进群需要群主 / 群管理确认</span>
<div
v-if="isOwner"
class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 transition-colors duration-150"
>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]"
>进群需要群主 / 群管理确认</span
>
<el-switch :model-value="!!group.joinApproval" @change="handleJoinApprovalChange" />
</div>
<!-- 进群申请子项仅当开启审批 + 当前用户是 owner / admin 时出现点击进列表 dialog -->
@ -306,7 +363,9 @@
class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--el-fill-color-lighter)]"
@click="handleOpenRequestList"
>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">- 进群申请</span>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]"
>- 进群申请</span
>
<Icon
icon="ant-design:right-outlined"
:size="11"
@ -325,7 +384,9 @@
class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--el-fill-color-lighter)]"
@click="handleOpenAdminSet"
>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">群管理员</span>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]"
>群管理员</span
>
<Icon
icon="ant-design:right-outlined"
:size="11"
@ -336,7 +397,9 @@
class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--el-fill-color-lighter)]"
@click="handleOpenTransferOwner"
>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">群主管理权转让</span>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]"
>群主管理权转让</span
>
<Icon
icon="ant-design:right-outlined"
:size="11"
@ -363,13 +426,7 @@
解散群聊
</el-button>
<!-- 非群主退出群聊 -->
<el-button
v-else
class="w-full !h-9 text-14px"
type="danger"
plain
@click="handleQuit"
>
<el-button v-else class="w-full !h-9 text-14px" type="danger" plain @click="handleQuit">
退出群聊
</el-button>
</div>
@ -399,11 +456,7 @@ import { useMessage } from '@/hooks/web/useMessage'
import { getCurrentUserId } from '@/utils/auth'
import { CommonStatusEnum } from '@/utils/constants'
import {
updateGroup,
muteAll,
dissolveGroup
} from '@/api/im/group'
import { updateGroup, muteAll, dissolveGroup } from '@/api/im/group'
import { quitGroup, updateGroupMember } from '@/api/im/group/member'
import { useConversationStore } from '../../../../store/conversationStore'
import { useGroupStore } from '../../../../store/groupStore'
@ -644,7 +697,11 @@ function onTopChange(value: boolean | string | number) {
if (!props.conversation) {
return
}
conversationStore.setConversationTop(props.conversation.type, props.conversation.targetId, !!value)
conversationStore.setConversationTop(
props.conversation.type,
props.conversation.targetId,
!!value
)
}
// ==================== ====================
@ -813,13 +870,13 @@ function handleOpenTransferOwner() {
/* 「添加 / 移出」瓦片hover 时联动内部 icon-tile 走主色,跨子元素的 hover 联动无法用单元素工具类表达 */
.im-conversation-group-side__tile-wrap:hover .im-conversation-group-side__icon-tile {
color: var(--el-color-primary);
border-color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
border-color: var(--el-color-primary);
}
/* :deep 穿透 Icon 内部 svg el-icon 全局 color 在暗色模式下被主题盖过,锁 fill 到当前色 */
.im-conversation-group-side__icon-tile :deep(svg) {
fill: currentColor !important;
fill: currentcolor !important;
}
/* 相邻信息行加分隔线; 相邻兄弟选择器无法用工具类表达 */

View File

@ -232,7 +232,8 @@ const requestText = computed(() => {
if (!isGroup.value) {
return ''
}
const count = groupRequestStore.getUnhandledGroupRequestCountMap.get(props.conversation.targetId) ?? 0
const count =
groupRequestStore.getUnhandledGroupRequestCountMap.get(props.conversation.targetId) ?? 0
return count > 0 ? `[${count}条进群申请]` : ''
})
@ -293,7 +294,6 @@ function handleContextMenu(e: MouseEvent) {
}
)
}
</script>
<style scoped>
@ -304,7 +304,7 @@ function handleContextMenu(e: MouseEvent) {
/* el-icon 的全局 color:var(--color) 在暗色模式下会渲染成白色,这里用 :deep + !important 锁定 */
.conversation-item__silent :deep(svg) {
fill: currentColor !important;
fill: currentcolor !important;
}
.conversation-item__prefix {

View File

@ -32,7 +32,9 @@
:name="friend.nickname"
:size="50"
/>
<div class="w-full mt-1.5 overflow-hidden text-12px leading-[1.5] text-[var(--el-text-color-regular)] text-center truncate">
<div
class="w-full mt-1.5 overflow-hidden text-12px leading-[1.5] text-[var(--el-text-color-regular)] text-center truncate"
>
{{ displayName }}
</div>
</div>
@ -43,10 +45,15 @@
title="发起群聊"
@click="handleOpenCreateGroup"
>
<div class="im-conversation-private-side__icon-tile flex items-center justify-center w-[50px] h-[50px] text-20px text-[var(--el-text-color-regular)] bg-[var(--el-fill-color-lighter)] border border-dashed border-[var(--el-border-color)] rounded-md transition-colors duration-200">
<div
class="im-conversation-private-side__icon-tile flex items-center justify-center w-[50px] h-[50px] text-20px text-[var(--el-text-color-regular)] bg-[var(--el-fill-color-lighter)] border border-dashed border-[var(--el-border-color)] rounded-md transition-colors duration-200"
>
<Icon icon="ant-design:plus-outlined" />
</div>
<div class="w-full mt-1.5 overflow-hidden text-12px leading-[1.5] text-[var(--el-text-color-regular)] text-center truncate">添加</div>
<div
class="w-full mt-1.5 overflow-hidden text-12px leading-[1.5] text-[var(--el-text-color-regular)] text-center truncate"
>添加</div
>
</div>
</div>
@ -64,14 +71,19 @@
<div
class="im-conversation-private-side__row flex flex-col items-stretch gap-1.5 px-4 py-[14px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--el-fill-color-lighter)]"
>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">备注</span>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]"
>备注</span
>
<span
v-if="friend.displayName"
class="text-13px leading-[1.6] text-[var(--el-text-color-regular)] break-all line-clamp-2"
>
{{ friend.displayName }}
</span>
<span v-else class="text-13px leading-[1.6] text-[var(--el-text-color-placeholder)]">
<span
v-else
class="text-13px leading-[1.6] text-[var(--el-text-color-placeholder)]"
>
好友备注仅自己可见
</span>
</div>
@ -101,7 +113,9 @@
class="im-conversation-private-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--el-fill-color-lighter)]"
@click="emit('open-history')"
>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">查找聊天内容</span>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]"
>查找聊天内容</span
>
<Icon
icon="ant-design:right-outlined"
:size="11"
@ -114,12 +128,20 @@
<!-- 开关项 -->
<div class="bg-[var(--el-bg-color)]">
<div class="im-conversation-private-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 transition-colors duration-150">
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">消息免打扰</span>
<div
class="im-conversation-private-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 transition-colors duration-150"
>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]"
>消息免打扰</span
>
<el-switch :model-value="!!conversation?.silent" @change="handleMutedChange" />
</div>
<div class="im-conversation-private-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 transition-colors duration-150">
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">置顶聊天</span>
<div
class="im-conversation-private-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 transition-colors duration-150"
>
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]"
>置顶聊天</span
>
<el-switch :model-value="!!conversation?.top" @change="handleTopChange" />
</div>
</div>
@ -236,7 +258,11 @@ function handleTopChange(value: boolean | string | number) {
if (!props.conversation) {
return
}
conversationStore.setConversationTop(props.conversation.type, props.conversation.targetId, !!value)
conversationStore.setConversationTop(
props.conversation.type,
props.conversation.targetId,
!!value
)
}
/** 群创建成功:跳到新群会话 + 关掉本侧抽屉,让用户专注新群 */
@ -260,13 +286,13 @@ function handleGroupCreated(groupId: number) {
/* 「+」 tile hover 时联动内部 icon-tile 走主色; 跨子元素的 hover 联动无法用单元素工具类表达 */
.im-conversation-private-side__tile-wrap-clickable:hover .im-conversation-private-side__icon-tile {
color: var(--el-color-primary);
border-color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
border-color: var(--el-color-primary);
}
/* :deep 穿透 Icon 内部 svg el-icon 全局 color 在暗色模式下被主题盖过,锁 fill 到当前色 */
.im-conversation-private-side__icon-tile :deep(svg) {
fill: currentColor !important;
fill: currentcolor !important;
}
/* 相邻信息行加分隔线; 相邻兄弟选择器无法用工具类表达 */

View File

@ -342,14 +342,13 @@ onUnmounted(() => {
<style scoped>
/* 底部小三角:指向触发图标,仿微信 PC 气泡指针left 偏移对应表情按钮(工具栏 1st icon */
.im-popover-arrow::after {
content: '';
position: absolute;
top: calc(100% - 1px);
left: 10px;
border-style: solid;
border-width: 6px 6px 0 6px;
border-color: var(--el-bg-color) transparent transparent transparent;
filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.08));
border-style: solid;
border-width: 6px 6px 0;
content: '';
filter: drop-shadow(0 2px 2px rgb(0 0 0 / 8%));
}
</style>

View File

@ -364,7 +364,7 @@ function collectFromEditor(root: HTMLElement): { text: string; atUserIds: number
* 3. 二次防御collectFromEditor trim可能比 syncEditorState 更严格例如全 ZWSP仍空就 return
* 4. 清空 + 同步状态先清 innerHTML syncEditorState placeholder / canSend 一起回归
* 顺序很重要先清后 sync否则 sync 看到旧内容会误判
* 5. 上送atUserIds 非空才传避免发空数组quote clearConversationDraft 前抓取确保引用条立即消失
* 5. 上送atUserIds 非空才传避免发空数组quote clearConversationDraft 前抓取确保引用条立即消失
*/
async function handleSend(options?: { receipt?: boolean }) {
const editor = editorRef.value
@ -1171,8 +1171,9 @@ async function onVideoPicked(e: Event) {
.message-input__tool:deep(svg) {
font-size: 18px !important;
color: var(--el-text-color-regular) !important;
fill: currentColor !important;
fill: currentcolor !important;
}
.message-input__tool:hover,
.message-input__tool:hover:deep(svg) {
color: var(--el-color-primary) !important;
@ -1180,10 +1181,10 @@ async function onVideoPicked(e: Event) {
/* 用 data-empty 而非 :empty浏览器在删空后会留下 <br>:empty 不命中data-empty 由 syncEditorState 维护 */
.message-input__editor[data-empty]::before {
content: attr(data-placeholder);
position: absolute;
color: var(--el-text-color-placeholder);
pointer-events: none;
position: absolute;
content: attr(data-placeholder);
}
/* @ token 走主色高亮contenteditable=false 让 backspace 整段删而不是逐字符 */

View File

@ -54,15 +54,7 @@
</template>
<script lang="ts" setup>
import {
computed,
onBeforeUnmount,
onMounted,
onUnmounted,
ref,
useTemplateRef,
watch
} from 'vue'
import { computed, onBeforeUnmount, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'
import { useMessage } from '@/hooks/web/useMessage'
import { formatSeconds } from '@/utils/formatTime'
@ -292,14 +284,14 @@ onUnmounted(() => {
<style scoped>
/* 底部小三角:指向触发图标,仿微信 PC 气泡指针left 偏移对应语音按钮(工具栏 4th icon */
.im-popover-arrow::after {
content: '';
position: absolute;
top: calc(100% - 1px);
left: 110px;
border-style: solid;
border-width: 6px 6px 0 6px;
border-color: var(--el-bg-color) transparent transparent transparent;
filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.08));
border-style: solid;
border-width: 6px 6px 0;
content: '';
filter: drop-shadow(0 2px 2px rgb(0 0 0 / 8%));
}
/* 录音中的脉冲呼吸动画;@keyframes 必须 CSS 定义 */
@ -309,13 +301,15 @@ onUnmounted(() => {
@keyframes im-voice-pulse {
0% {
box-shadow: 0 0 0 0 rgba(245, 108, 108, 0.6);
box-shadow: 0 0 0 0 rgb(245 108 108 / 60%);
}
70% {
box-shadow: 0 0 0 20px rgba(245, 108, 108, 0);
box-shadow: 0 0 0 20px rgb(245 108 108 / 0%);
}
100% {
box-shadow: 0 0 0 0 rgba(245, 108, 108, 0);
box-shadow: 0 0 0 0 rgb(245 108 108 / 0%);
}
}
</style>

View File

@ -182,15 +182,15 @@ async function handleRemove(pinnedMessage: Message) {
<style scoped>
/* 弹出层朝上的三角箭头;走 ::before + 4 边 border 配色画,颜色跟弹出层 background 一致 */
.im-group-pinned-message__list::before {
content: '';
position: absolute;
top: -8px;
left: 184px;
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-bottom: 8px solid var(--el-bg-color);
filter: drop-shadow(0 -2px 1px rgba(0, 0, 0, 0.04));
border-left: 8px solid transparent;
content: '';
filter: drop-shadow(0 -2px 1px rgb(0 0 0 / 4%));
}
</style>

View File

@ -82,6 +82,6 @@ const pendingCount = computed(() => groupRequestStore.getUnhandledGroupRequestCo
<style scoped>
/* :deep 穿透 Icon 子组件 DOM强制 svg 走 currentColor 应对暗色模式 el-icon 全局色覆盖 */
.im-group-request-pending__icon :deep(svg) {
fill: currentColor !important;
fill: currentcolor !important;
}
</style>

View File

@ -5,8 +5,14 @@
class="material-card cursor-pointer w-full overflow-hidden rounded-lg bg-[var(--el-bg-color)] border border-solid border-[var(--el-border-color-lighter)]"
@click="onClick"
>
<img v-if="payload.coverUrl" class="block w-full h-[200px] object-cover" :src="payload.coverUrl" />
<div class="px-3.5 py-3 text-15px font-600 leading-[1.4] text-[var(--el-text-color-primary)] line-clamp-2">
<img
v-if="payload.coverUrl"
class="block w-full h-[200px] object-cover"
:src="payload.coverUrl"
/>
<div
class="px-3.5 py-3 text-15px font-600 leading-[1.4] text-[var(--el-text-color-primary)] line-clamp-2"
>
{{ payload.title || '(无标题)' }}
</div>
</div>
@ -19,7 +25,9 @@
>
<div class="flex gap-2.5 items-start">
<div class="flex flex-1 flex-col gap-1.5 min-w-0">
<div class="text-15px font-600 leading-[1.4] text-[var(--el-text-color-primary)] line-clamp-2 break-all">
<div
class="text-15px font-600 leading-[1.4] text-[var(--el-text-color-primary)] line-clamp-2 break-all"
>
{{ payload.title || '(无标题)' }}
</div>
<div
@ -35,7 +43,9 @@
:src="payload.coverUrl"
/>
</div>
<div class="flex items-center gap-1.5 mt-2.5 pt-2 border-t border-t-solid border-[var(--el-border-color-lighter)] text-12px text-[var(--el-text-color-secondary)]">
<div
class="flex items-center gap-1.5 mt-2.5 pt-2 border-t border-t-solid border-[var(--el-border-color-lighter)] text-12px text-[var(--el-text-color-secondary)]"
>
<img
v-if="sourceChannel?.avatar"
class="w-4 h-4 rounded-full object-cover flex-shrink-0"
@ -47,13 +57,11 @@
</div>
<!-- 富文本详情全屏弹窗按需挂载destroy-on-close 关闭后释放 v-dompurify-html 解析的 DOM -->
<Dialog
v-model="detailVisible"
:title="payload.title || '详情'"
fullscreen
destroy-on-close
>
<div v-loading="detailLoading" class="material-detail-body max-w-[720px] mx-auto px-5 pt-6 pb-20">
<Dialog v-model="detailVisible" :title="payload.title || '详情'" fullscreen destroy-on-close>
<div
v-loading="detailLoading"
class="material-detail-body max-w-[720px] mx-auto px-5 pt-6 pb-20"
>
<div class="text-[22px] font-600 leading-[1.4] text-[var(--el-text-color-primary)] mb-5">
{{ payload.title || '' }}
</div>
@ -95,9 +103,7 @@ const isChannelView = computed(
)
/** 反序列化 content JSON 为 payload 对象 */
const payload = computed<MaterialMessage>(
() => parseMessage<MaterialMessage>(props.content) ?? {}
)
const payload = computed<MaterialMessage>(() => parseMessage<MaterialMessage>(props.content) ?? {})
/** 来源频道;紧凑卡底部渲染头像 + 名称 */
const sourceChannel = computed(() =>
@ -137,15 +143,15 @@ const onClick = async () => {
transition: box-shadow 0.15s ease;
&:hover {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
box-shadow: 0 2px 12px rgb(0 0 0 / 8%);
}
}
/* :deep 穿透 v-dompurify-html 渲染的内嵌 DOM统一控制富文本里的 img / p / hN 排版 */
.article-content {
:deep(img) {
max-width: 100%;
height: auto;
max-width: 100%;
}
:deep(p) {

View File

@ -355,28 +355,31 @@ onBeforeUnmount(() => {
颜色与气泡背景对应 1px 视觉吃进去省一张图片 */
.message-bubble--other::before,
.message-bubble--self::before {
content: '';
position: absolute;
top: 12px;
width: 0;
height: 0;
border-style: solid;
content: '';
}
.message-bubble--other::before {
left: -5px;
border-width: 5px 6px 5px 0;
border-color: transparent var(--el-fill-color-light) transparent transparent;
border-width: 5px 6px 5px 0;
}
.message-bubble--self::before {
right: -5px;
border-width: 5px 0 5px 6px;
border-color: transparent transparent transparent #95ec69;
border-width: 5px 0 5px 6px;
}
/* :deep 穿透 scoped 子组件 DOMel-icon 在暗色模式下全局 color 被 .el-icon{color:var(--color)} 干扰,把 voice 图标 fill 锁死 */
.message-bubble__voice-icon :deep(svg) {
fill: #606266 !important;
}
.message-bubble__voice-icon.im-voice-playing :deep(svg) {
fill: #409eff !important;
}
@ -391,6 +394,7 @@ onBeforeUnmount(() => {
100% {
transform: scale(1);
}
50% {
transform: scale(1.15);
}

View File

@ -714,8 +714,8 @@ function locateMessage(messageId: number) {
/* :deep 穿透 el-tag 子组件 DOM搜索区 chip 禁掉 hover 颜色过渡 / × 图标动效,避免在搜索区里有抖动感 */
.im-message-history__chip,
.im-message-history__chip :deep(.el-tag__close) {
transition: none !important;
animation: none !important;
transition: none !important;
}
/* 「定位到聊天位置」按钮:父行 hover 才显示,走 .parent:hover .child 跨元素状态联动 */
@ -723,16 +723,18 @@ function locateMessage(messageId: number) {
position: absolute;
top: 100%;
right: 0;
margin-top: 4px;
display: none;
white-space: nowrap;
margin-top: 4px;
color: #1989fa;
white-space: nowrap;
cursor: pointer;
transition: color 0.15s;
}
.im-message-history__row:hover .im-message-history__locate {
display: block;
}
.im-message-history__locate:hover {
color: #146fc7;
}
@ -744,30 +746,35 @@ function locateMessage(messageId: number) {
color: #1989fa;
transition: color 0.15s;
}
.im-message-history__tab:hover {
color: #2f81d4;
}
.im-message-history__tab--active::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
left: 0;
height: 2px;
background: #1989fa;
border-radius: 1px;
content: '';
}
/* :deep 穿透 el-calendar 子组件 DOM默认偏大压一压让它能塞进 320 popover */
.im-message-history__calendar :deep(.el-calendar) {
--el-calendar-cell-width: 36px;
}
.im-message-history__calendar :deep(.el-calendar__header) {
padding: 4px 8px;
}
.im-message-history__calendar :deep(.el-calendar-table) {
font-size: 12px;
}
.im-message-history__calendar :deep(.el-calendar-day) {
height: 36px;
padding: 4px;

View File

@ -1053,6 +1053,7 @@ function handleDelete() {
.im-loading-spin {
animation: im-loading-spin 1s linear infinite;
}
@keyframes im-loading-spin {
to {
transform: rotate(360deg);

View File

@ -755,9 +755,10 @@ watch(
.message-panel__header-icon,
.message-panel__header-icon :deep(svg) {
color: var(--el-text-color-regular) !important;
fill: currentColor !important;
fill: currentcolor !important;
transition: color 0.15s;
}
.message-panel__header-icon:hover,
.message-panel__header-icon:hover :deep(svg) {
color: var(--el-color-primary) !important;
@ -775,6 +776,7 @@ watch(
.message-panel__message-anchor {
transition: background-color 0.6s ease;
}
.message-panel__message-anchor--highlight {
background-color: var(--el-color-warning-light-9);
}

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