feat(im): 继续优化频道的各种代码(v3)

im
YunaiV 2026-05-19 22:06:38 +08:00
parent 2442a01e48
commit 94e5fc00ac
6 changed files with 168 additions and 70 deletions

View File

@ -18,6 +18,13 @@ export const getManagerChannelMaterialPage = (params: PageParam) => {
return request.get({ url: '/im/manager/channel-material/page', params })
}
// 获得指定频道下的素材精简列表
export const getSimpleManagerChannelMaterialList = (channelId: number) => {
return request.get({
url: '/im/manager/channel-material/simple-list?channelId=' + channelId
})
}
// 获得素材详情
export const getManagerChannelMaterial = (id: number) => {
return request.get({ url: '/im/manager/channel-material/get?id=' + id })

View File

@ -1,11 +1,26 @@
<template>
<div class="material-card" @click="onClick">
<!-- 公众号会话内大卡片封面 + 标题 + 摘要 -->
<div v-if="isChannelView" class="material-card" @click="onClick">
<div class="title">{{ payload.title || '(无标题)' }}</div>
<img v-if="payload.coverUrl" class="cover" :src="payload.coverUrl" />
<div v-if="payload.summary" class="summary">{{ payload.summary }}</div>
<span class="link">{{ payload.url ? '外链' : '查看详情' }}</span>
</div>
<!-- 私聊 / 群聊里被转发的素材紧凑卡片标题左 + 小封面右 + 底部频道标识 -->
<!-- TODO @AI转发后的消息无法点击打开 -->
<div v-else class="material-card-forward" @click="onClick">
<div class="forward-body">
<div class="forward-title">{{ payload.title || '(无标题)' }}</div>
<img v-if="payload.coverUrl" class="forward-cover" :src="payload.coverUrl" />
</div>
<div class="forward-footer">
<Icon icon="ep:promotion" :size="12" />
<span>频道消息</span>
</div>
</div>
<!-- TODO @ai需要注释下 -->
<Dialog
v-model="detailVisible"
:title="payload.title || '详情'"
@ -23,8 +38,11 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import type { PropType } from 'vue'
import Icon from '@/components/Icon/src/Icon.vue'
import { getChannelMaterial } from '@/api/im/channel/material'
import { parseMessage, type MaterialMessage } from '@/views/im/utils/message'
import { useConversationStore } from '@/views/im/home/store/conversationStore'
import { ImConversationType } from '@/views/im/utils/constants'
interface MessageInfo {
materialId?: number
@ -35,6 +53,13 @@ const props = defineProps({
message: { type: Object as PropType<MessageInfo>, required: true }
})
const conversationStore = useConversationStore()
/** 当前是否在公众号 / 频道会话里:决定走大卡片还是紧凑转发卡片 */
const isChannelView = computed(
() => conversationStore.activeConversation?.type === ImConversationType.CHANNEL
)
/** 反序列化 content JSON 为 payload 对象 */
const payload = computed<MaterialMessage>(
() => parseMessage<MaterialMessage>(props.message.content) ?? {}
@ -69,6 +94,8 @@ const onClick = async () => {
</script>
<style scoped lang="scss">
/* 公众号会话内大卡片:占父容器全宽,封面 + 标题 + 摘要纵向铺开 */
/** TODO @AI有没可能 unocss 尽量替代掉; */
.material-card {
display: flex;
flex-direction: column;
@ -124,6 +151,62 @@ const onClick = async () => {
}
}
/* 私聊 / 群聊转发紧凑卡片:标题左 + 小封面右 + 底部频道标识 */
.material-card-forward {
display: flex;
flex-direction: column;
width: 280px;
padding: 10px 12px 8px;
background: var(--el-bg-color);
border-radius: 8px;
border: 1px solid var(--el-border-color-lighter);
cursor: pointer;
transition: box-shadow 0.15s ease;
&:hover {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.forward-body {
display: flex;
gap: 10px;
align-items: flex-start;
.forward-title {
flex: 1;
min-width: 0;
font-size: 14px;
font-weight: 500;
line-height: 1.4;
color: var(--el-text-color-primary);
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.forward-cover {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 4px;
background: var(--el-fill-color-light);
flex-shrink: 0;
}
}
.forward-footer {
margin-top: 8px;
padding-top: 6px;
border-top: 1px solid var(--el-border-color-lighter);
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: var(--el-text-color-secondary);
}
}
.material-detail-body {
max-width: 720px;
margin: 0 auto;

View File

@ -64,7 +64,13 @@
</template>
</el-table-column>
<el-table-column label="编码" align="center" prop="code" width="160" show-overflow-tooltip />
<el-table-column label="名称" align="center" prop="name" min-width="120" show-overflow-tooltip />
<el-table-column
label="名称"
align="center"
prop="name"
min-width="120"
show-overflow-tooltip
/>
<el-table-column label="排序" align="center" prop="sort" width="80" />
<el-table-column label="状态" align="center" prop="status" width="80">
<template #default="scope">
@ -111,7 +117,6 @@
</template>
<script lang="ts" setup>
// TOOD @AI system user index
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import * as ChannelApi from '@/api/im/manager/channel'
@ -119,12 +124,12 @@ import ChannelForm from './ChannelForm.vue'
defineOptions({ name: 'ImChannel' })
const message = useMessage()
const { t } = useI18n()
const message = useMessage() //
const { t } = useI18n() //
const loading = ref(true)
const total = ref(0)
const list = ref<ChannelApi.ImManagerChannelVO[]>([])
const loading = ref(true) //
const total = ref(0) //
const list = ref<ChannelApi.ImManagerChannelVO[]>([]) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
@ -132,7 +137,7 @@ const queryParams = reactive({
name: undefined as string | undefined,
status: undefined as number | undefined
})
const queryFormRef = ref()
const queryFormRef = ref() //
/** 查询频道分页 */
const getList = async () => {
@ -176,6 +181,7 @@ const handleDelete = async (id: number) => {
await getList()
}
/** 初始化 **/
onMounted(() => {
getList()
})

View File

@ -8,10 +8,14 @@
v-loading="formLoading"
>
<el-form-item label="所属频道" prop="channelId">
<!-- TODO @AI使用 channelselect 组件 -->
<el-select v-model="formData.channelId" placeholder="请选择频道" class="!w-full">
<el-option v-for="c in props.channelList" :key="c.id" :label="c.name" :value="c.id" />
</el-select>
<ChannelSelect v-model="formData.channelId" placeholder="请选择频道" />
</el-form-item>
<!-- TODO @AI是不是内容类型在考虑优化下1富文本2外链更简介注意需要插入到字典里 -->
<el-form-item label="内容类型" prop="type">
<el-radio-group v-model="formData.type">
<el-radio :value="1">站内富文本</el-radio>
<el-radio :value="2">外链</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input v-model="formData.title" placeholder="图文标题" maxlength="128" show-word-limit />
@ -29,14 +33,8 @@
show-word-limit
/>
</el-form-item>
<!-- TODO @AI需要增加 type -->
<el-form-item label="跳转链接" prop="url">
<el-input
v-model="formData.url"
placeholder="为空走站内详情页渲染 content非空则跳此链接"
/>
</el-form-item>
<el-form-item label="正文" prop="content">
<!-- 内容类型为站内富文本时展示 content 富文本输入外链时展示 url 输入 -->
<el-form-item v-if="formData.type === 1" label="正文" prop="content">
<el-input
v-model="formData.content"
placeholder="富文本 HTML"
@ -44,6 +42,9 @@
:rows="8"
/>
</el-form-item>
<el-form-item v-else label="跳转链接" prop="url">
<el-input v-model="formData.url" placeholder="https://example.com/..." />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
@ -53,26 +54,22 @@
</template>
<script lang="ts" setup>
// TODO @AI system user form
import * as MaterialApi from '@/api/im/manager/channel/material'
import type { ImManagerChannelVO } from '@/api/im/manager/channel'
import ChannelSelect from '../list/components/ChannelSelect.vue'
defineOptions({ name: 'ImChannelMaterialForm' })
const props = defineProps<{ channelList: ImManagerChannelVO[] }>()
const { t } = useI18n() //
const message = useMessage() //
const { t } = useI18n()
const message = useMessage()
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref('')
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined as number | undefined,
channelId: undefined as number | undefined,
// TODO @AI type 1 2
type: 125, // MATERIAL
type: 1, // 1 2 ImChannelMaterialTypeEnum
title: '',
coverUrl: '',
summary: '',
@ -81,15 +78,18 @@ const formData = ref({
})
const formRules = reactive({
channelId: [{ required: true, message: '所属频道不能为空', trigger: 'change' }],
type: [{ required: true, message: '内容类型不能为空', trigger: 'change' }],
title: [{ required: true, message: '标题不能为空', trigger: 'blur' }]
})
const formRef = ref()
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
@ -99,10 +99,11 @@ const open = async (type: string, id?: number) => {
}
}
}
defineExpose({ open })
defineExpose({ open }) // open
const emit = defineEmits(['success'])
const emit = defineEmits(['success']) // success
/** 提交表单 */
const submitForm = async () => {
await formRef.value.validate()
formLoading.value = true
@ -122,11 +123,12 @@ const submitForm = async () => {
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
channelId: undefined,
type: 125,
type: 1,
title: '',
coverUrl: '',
summary: '',

View File

@ -8,9 +8,7 @@
label-width="68px"
>
<el-form-item label="频道" prop="channelId">
<el-select v-model="queryParams.channelId" placeholder="全部" clearable class="!w-200px">
<el-option v-for="c in channelList" :key="c.id" :label="c.name" :value="c.id" />
</el-select>
<ChannelSelect v-model="queryParams.channelId" placeholder="全部" clearable class="!w-200px" />
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input
@ -108,33 +106,32 @@
/>
</ContentWrap>
<ChannelMaterialForm ref="formRef" :channel-list="channelList" @success="getList" />
<ChannelMaterialForm ref="formRef" @success="getList" />
</template>
<script lang="ts" setup>
// TOOD @AI system user index
import { dateFormatter } from '@/utils/formatTime'
import * as MaterialApi from '@/api/im/manager/channel/material'
import * as ChannelApi from '@/api/im/manager/channel'
import ChannelSelect from '../list/components/ChannelSelect.vue'
import ChannelMaterialForm from './ChannelMaterialForm.vue'
defineOptions({ name: 'ImChannelMaterial' })
const message = useMessage()
const { t } = useI18n()
const message = useMessage() //
const { t } = useI18n() //
const loading = ref(true)
const total = ref(0)
const list = ref<MaterialApi.ImManagerChannelMaterialVO[]>([])
const channelList = ref<ChannelApi.ImManagerChannelVO[]>([])
const loading = ref(true) //
const total = ref(0) //
const list = ref<MaterialApi.ImManagerChannelMaterialVO[]>([]) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
channelId: undefined as number | undefined,
title: undefined as string | undefined
})
const queryFormRef = ref()
const queryFormRef = ref() //
/** 查询素材分页 */
const getList = async () => {
loading.value = true
try {
@ -146,21 +143,25 @@ const getList = async () => {
}
}
/** 搜索 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
const formRef = ref()
/** 打开新增 / 编辑弹窗 */
const formRef = ref() // Ref
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除 */
const handleDelete = async (id: number) => {
try {
await message.delConfirm()
@ -172,9 +173,8 @@ const handleDelete = async (id: number) => {
await getList()
}
onMounted(async () => {
// TOOD @AIChannelApi channel/components
channelList.value = await ChannelApi.getEnabledChannelList()
/** 初始化 */
onMounted(() => {
getList()
})
</script>

View File

@ -8,9 +8,7 @@
label-width="68px"
>
<el-form-item label="频道" prop="channelId">
<el-select v-model="queryParams.channelId" placeholder="全部" clearable class="!w-200px">
<el-option v-for="c in channelList" :key="c.id" :label="c.name" :value="c.id" />
</el-select>
<ChannelSelect v-model="queryParams.channelId" placeholder="全部" clearable class="!w-200px" />
</el-form-item>
<el-form-item label="发送时间" prop="sendTime">
<el-date-picker
@ -88,33 +86,32 @@
/>
</ContentWrap>
<ChannelMessageSendForm ref="sendFormRef" :channel-list="channelList" @success="getList" />
<ChannelMessageSendForm ref="sendFormRef" @success="getList" />
</template>
<script lang="ts" setup>
// TODO @AI system user index
import { dateFormatter } from '@/utils/formatTime'
import * as MessageApi from '@/api/im/manager/channel/message'
import * as ChannelApi from '@/api/im/manager/channel'
import ChannelSelect from '../list/components/ChannelSelect.vue'
import ChannelMessageSendForm from './ChannelMessageSendForm.vue'
defineOptions({ name: 'ImChannelMessage' })
const message = useMessage()
const { t } = useI18n()
const message = useMessage() //
const { t } = useI18n() //
const loading = ref(true)
const total = ref(0)
const list = ref<MessageApi.ImManagerChannelMessageVO[]>([])
const channelList = ref<ChannelApi.ImManagerChannelVO[]>([])
const loading = ref(true) //
const total = ref(0) //
const list = ref<MessageApi.ImManagerChannelMessageVO[]>([]) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
channelId: undefined as number | undefined,
sendTime: [] as string[]
})
const queryFormRef = ref()
const queryFormRef = ref() //
/** 查询消息分页 */
const getList = async () => {
loading.value = true
try {
@ -126,21 +123,25 @@ const getList = async () => {
}
}
/** 搜索 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
const sendFormRef = ref()
/** 打开「立即推送」弹窗 */
const sendFormRef = ref() // Ref
const openSendForm = () => {
sendFormRef.value.open()
}
/** 删除 */
const handleDelete = async (id: number) => {
try {
await message.delConfirm()
@ -152,8 +153,7 @@ const handleDelete = async (id: number) => {
await getList()
}
onMounted(async () => {
channelList.value = await ChannelApi.getEnabledChannelList()
onMounted(() => {
getList()
})
</script>