feat(user): 新增站内消息功能

- 添加聊天消息相关API和模型定义
- 实现消息列表、消息详情和发送消息页面
- 集成消息功能到首页和团队页面
-优化用户模型,增加别名字段
This commit is contained in:
2025-09-16 17:42:49 +08:00
parent 411e867fd6
commit cb40ed7cb7
23 changed files with 1060 additions and 31 deletions

View File

@@ -0,0 +1,101 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopChatConversation, ShopChatConversationParam } from './model';
/**
* 分页查询聊天会话表
*/
export async function pageShopChatConversation(params: ShopChatConversationParam) {
const res = await request.get<ApiResult<PageResult<ShopChatConversation>>>(
'/shop/shop-chat-conversation/page',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询聊天会话表列表
*/
export async function listShopChatConversation(params?: ShopChatConversationParam) {
const res = await request.get<ApiResult<ShopChatConversation[]>>(
'/shop/shop-chat-conversation',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加聊天会话表
*/
export async function addShopChatConversation(data: ShopChatConversation) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-chat-conversation',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改聊天会话表
*/
export async function updateShopChatConversation(data: ShopChatConversation) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-chat-conversation',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除聊天会话表
*/
export async function removeShopChatConversation(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-chat-conversation/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除聊天会话表
*/
export async function removeShopBatchChatConversation(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-chat-conversation/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询聊天会话表
*/
export async function getShopChatConversation(id: number) {
const res = await request.get<ApiResult<ShopChatConversation>>(
'/shop/shop-chat-conversation/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -0,0 +1,37 @@
import type { PageParam } from '@/api';
/**
* 聊天消息表
*/
export interface ShopChatConversation {
// 自增ID
id?: number;
// 用户ID
userId?: number;
// 好友ID
friendId?: number;
// 消息类型
type?: number;
// 消息内容
content?: string;
// 未读消息
unRead?: number;
// 状态, 0未读, 1已读
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 聊天消息表搜索条件
*/
export interface ShopChatConversationParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -0,0 +1,115 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopChatMessage, ShopChatMessageParam } from './model';
/**
* 分页查询聊天消息表
*/
export async function pageShopChatMessage(params: ShopChatMessageParam) {
const res = await request.get<ApiResult<PageResult<ShopChatMessage>>>(
'/shop/shop-chat-message/page',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询聊天消息表列表
*/
export async function listShopChatMessage(params?: ShopChatMessageParam) {
const res = await request.get<ApiResult<ShopChatMessage[]>>(
'/shop/shop-chat-message',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加聊天消息表
*/
export async function addShopChatMessage(data: ShopChatMessage) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-chat-message',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加聊天消息表
*/
export async function addShopBatchChatMessage(data: ShopChatMessage[]) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-chat-message/batch',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改聊天消息表
*/
export async function updateShopChatMessage(data: ShopChatMessage) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-chat-message',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除聊天消息表
*/
export async function removeShopChatMessage(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-chat-message/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除聊天消息表
*/
export async function removeShopBatchChatMessage(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-chat-message/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询聊天消息表
*/
export async function getShopChatMessage(id: number) {
const res = await request.get<ApiResult<ShopChatMessage>>(
'/shop/shop-chat-message/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -0,0 +1,63 @@
import type { PageParam } from '@/api';
/**
* 聊天消息表
*/
export interface ShopChatMessage {
// 自增ID
id?: number;
// 发送人ID
formUserId?: number;
// 发送人名称
formUserName?: string;
// 发送人头像
formUserAvatar?: string;
// 发送人手机号
formUserPhone?: string;
// 发送人别名
formUserAlias?: string;
// 接收人ID
toUserId?: number;
// 接收人名称
toUserName?: string;
// 接收人头像
toUserAvatar?: string;
// 接收人手机号
toUserPhone?: string;
// 接收人别名
toUserAlias?: string;
// 消息类型
type?: string;
// 消息内容
content?: string;
// 屏蔽接收方
sideTo?: number;
// 屏蔽发送方
sideFrom?: number;
// 是否撤回
withdraw?: number;
// 文件信息
fileInfo?: string;
// 批量发送
toUserIds?: any[];
// 存在联系方式
hasContact?: number;
// 状态, 0未读, 1已读
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 聊天消息表搜索条件
*/
export interface ShopChatMessageParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -1,4 +1,4 @@
import type { PageParam } from '@/api/index'; import type { PageParam } from '@/api';
/** /**
* 优惠券 * 优惠券

View File

@@ -18,6 +18,8 @@ export interface ShopDealerReferee {
avatar?: string; avatar?: string;
// 用户昵称 // 用户昵称
nickname?: string; nickname?: string;
// 用户别名
alias?: string;
// 用户手机号 // 用户手机号
phone?: string; phone?: string;
// 推荐关系层级(1,2,3) // 推荐关系层级(1,2,3)

View File

@@ -22,7 +22,8 @@ export interface ChatMessage {
withdraw?: number; withdraw?: number;
// 文件信息 // 文件信息
fileInfo?: string; fileInfo?: string;
toUserName?: any; //
toUserName?: string;
formUserName?: string; formUserName?: string;
// 批量发送 // 批量发送
toUserIds?: any[]; toUserIds?: any[];

View File

@@ -22,7 +22,7 @@ export async function pageUsers(params: UserParam) {
*/ */
export async function listUsers(params?: UserParam) { export async function listUsers(params?: UserParam) {
const res = await request.get<ApiResult<User[]>>( const res = await request.get<ApiResult<User[]>>(
'/system/user', SERVER_API_URL + '/system/user',
params params
); );
if (res.code === 0 && res.data) { if (res.code === 0 && res.data) {
@@ -36,7 +36,7 @@ export async function listUsers(params?: UserParam) {
*/ */
export async function getStaffs(params?: UserParam) { export async function getStaffs(params?: UserParam) {
const res = await request.get<ApiResult<User[]>>( const res = await request.get<ApiResult<User[]>>(
'/system/user', SERVER_API_URL + '/system/user',
{ {
params params
} }
@@ -52,7 +52,7 @@ export async function getStaffs(params?: UserParam) {
*/ */
export async function getCompanyList(params?: UserParam) { export async function getCompanyList(params?: UserParam) {
const res = await request.get<ApiResult<User[]>>( const res = await request.get<ApiResult<User[]>>(
'/system/user', SERVER_API_URL + '/system/user',
{ {
params params
} }
@@ -68,7 +68,7 @@ export async function getCompanyList(params?: UserParam) {
*/ */
export async function getUser(id: number) { export async function getUser(id: number) {
const res = await request.get<ApiResult<User>>( const res = await request.get<ApiResult<User>>(
'/system/user/' + id, SERVER_API_URL + '/system/user/' + id,
{} {}
); );
if (res.code === 0 && res.data) { if (res.code === 0 && res.data) {
@@ -82,7 +82,7 @@ export async function getUser(id: number) {
*/ */
export async function addUser(data: User) { export async function addUser(data: User) {
const res = await request.post<ApiResult<unknown>>( const res = await request.post<ApiResult<unknown>>(
'/system/user', SERVER_API_URL + '/system/user',
data data
); );
if (res.code === 0) { if (res.code === 0) {
@@ -110,7 +110,7 @@ export async function updateUser(data: User) {
*/ */
export async function removeUser(id?: number) { export async function removeUser(id?: number) {
const res = await request.del<ApiResult<unknown>>( const res = await request.del<ApiResult<unknown>>(
'/system/user/' + id SERVER_API_URL + '/system/user/' + id
); );
if (res.code === 0) { if (res.code === 0) {
return res.message; return res.message;
@@ -123,7 +123,7 @@ export async function removeUser(id?: number) {
*/ */
export async function removeUsers(data: (number | undefined)[]) { export async function removeUsers(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>( const res = await request.del<ApiResult<unknown>>(
'/system/user/batch', SERVER_API_URL + '/system/user/batch',
{ {
data data
} }
@@ -139,7 +139,7 @@ export async function removeUsers(data: (number | undefined)[]) {
*/ */
export async function updateUserStatus(userId?: number, status?: number) { export async function updateUserStatus(userId?: number, status?: number) {
const res = await request.put<ApiResult<unknown>>( const res = await request.put<ApiResult<unknown>>(
'/system/user/status', SERVER_API_URL + '/system/user/status',
{ {
userId, userId,
status status
@@ -156,7 +156,7 @@ export async function updateUserStatus(userId?: number, status?: number) {
*/ */
export async function updateUserRecommend(form:any) { export async function updateUserRecommend(form:any) {
const res = await request.put<ApiResult<unknown>>( const res = await request.put<ApiResult<unknown>>(
'/system/user/recommend', SERVER_API_URL + '/system/user/recommend',
form form
); );
if (res.code === 0) { if (res.code === 0) {
@@ -170,7 +170,7 @@ export async function updateUserRecommend(form:any) {
*/ */
export async function updateUserPassword(userId?: number, password = '123456') { export async function updateUserPassword(userId?: number, password = '123456') {
const res = await request.put<ApiResult<unknown>>( const res = await request.put<ApiResult<unknown>>(
'/system/user/password', SERVER_API_URL + '/system/user/password',
{ {
userId, userId,
password password
@@ -189,7 +189,7 @@ export async function importUsers(file: File) {
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
const res = await request.post<ApiResult<unknown>>( const res = await request.post<ApiResult<unknown>>(
'/system/user/import', SERVER_API_URL + '/system/user/import',
formData formData
); );
if (res.code === 0) { if (res.code === 0) {
@@ -207,7 +207,7 @@ export async function checkExistence(
id?: number id?: number
) { ) {
const res = await request.get<ApiResult<unknown>>( const res = await request.get<ApiResult<unknown>>(
'/system/user/existence', SERVER_API_URL + '/system/user/existence',
{ {
params: {field, value, id} params: {field, value, id}
} }
@@ -223,7 +223,7 @@ export async function checkExistence(
*/ */
export async function countUserBalance(params?: UserParam) { export async function countUserBalance(params?: UserParam) {
const res = await request.get<ApiResult<unknown>>( const res = await request.get<ApiResult<unknown>>(
'/system/user/countUserBalance', SERVER_API_URL + '/system/user/countUserBalance',
{ {
params params
} }
@@ -241,7 +241,7 @@ export async function countUserBalance(params?: UserParam) {
*/ */
export async function listAdminsByPhoneAll(params?: UserParam) { export async function listAdminsByPhoneAll(params?: UserParam) {
const res = await request.get<ApiResult<User[]>>( const res = await request.get<ApiResult<User[]>>(
'/system/user/listAdminsByPhoneAll', SERVER_API_URL + '/system/user/listAdminsByPhoneAll',
params params
); );
if (res.code === 0 && res.data) { if (res.code === 0 && res.data) {

View File

@@ -44,7 +44,11 @@ export default defineAppConfig({
"gift/redeem", "gift/redeem",
"gift/detail", "gift/detail",
"store/verification", "store/verification",
"theme/index" "theme/index",
"chat/conversation/index",
"chat/message/index",
"chat/message/add",
"chat/message/detail"
] ]
}, },
{ {

View File

@@ -1,6 +1,6 @@
import React, {useState, useEffect, useCallback} from 'react' import React, {useState, useEffect, useCallback} from 'react'
import {View, Text} from '@tarojs/components' import {View, Text} from '@tarojs/components'
import {Phone} from '@nutui/icons-react-taro' import {Phone, Edit, Message} from '@nutui/icons-react-taro'
import {Space, Empty, Avatar, Button} from '@nutui/nutui-react-taro' import {Space, Empty, Avatar, Button} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import {useDealerUser} from '@/hooks/useDealerUser' import {useDealerUser} from '@/hooks/useDealerUser'
@@ -9,11 +9,13 @@ import {pageShopDealerOrder} from '@/api/shop/shopDealerOrder'
import type {ShopDealerReferee} from '@/api/shop/shopDealerReferee/model' import type {ShopDealerReferee} from '@/api/shop/shopDealerReferee/model'
import FixedButton from "@/components/FixedButton"; import FixedButton from "@/components/FixedButton";
import navTo from "@/utils/common"; import navTo from "@/utils/common";
import {updateUser} from "@/api/system/user";
interface TeamMemberWithStats extends ShopDealerReferee { interface TeamMemberWithStats extends ShopDealerReferee {
name?: string name?: string
avatar?: string avatar?: string
nickname?: string; nickname?: string;
alias?: string;
phone?: string; phone?: string;
orderCount?: number orderCount?: number
commission?: string commission?: string
@@ -219,6 +221,54 @@ const DealerTeam: React.FC = () => {
} }
} }
// 一键拨打
const makePhoneCall = (phone: string) => {
Taro.makePhoneCall({
phoneNumber: phone,
fail: () => {
Taro.showToast({
title: '拨打取消',
icon: 'error'
});
}
});
};
// 别名备注
const editAlias = (item: any, index: number) => {
Taro.showModal({
title: '备注',
// @ts-ignore
editable: true,
placeholderText: '真实姓名',
content: item.alias || '',
success: async (res: any) => {
if (res.confirm && res.content !== undefined) {
try {
// 更新跟进情况
await updateUser({
userId: item.userId,
alias: res.content.trim()
});
teamMembers[index].alias = res.content.trim()
setTeamMembers(teamMembers)
} catch (error) {
console.error('备注失败:', error);
Taro.showToast({
title: '备注失败,请重试',
icon: 'error'
});
}
}
}
});
};
// 发送消息
const sendMessage = (item: TeamMemberWithStats) => {
return navTo(`/user/chat/message/add?id=${item.userId}`, true)
}
// 监听数据变化,获取团队数据 // 监听数据变化,获取团队数据
useEffect(() => { useEffect(() => {
if (dealerUser?.userId || dealerId) { if (dealerUser?.userId || dealerId) {
@@ -233,7 +283,7 @@ const DealerTeam: React.FC = () => {
} }
}, [dealerUser, dealerId, currentDealerName]) }, [dealerUser, dealerId, currentDealerName])
const renderMemberItem = (member: TeamMemberWithStats) => { const renderMemberItem = (member: TeamMemberWithStats, index: number) => {
// 判断是否可以点击:有下级成员且未达到层级限制 // 判断是否可以点击:有下级成员且未达到层级限制
const canClick = member.subMembers && member.subMembers > 0 && levelStack.length < 1 const canClick = member.subMembers && member.subMembers > 0 && levelStack.length < 1
// 判断是否显示手机号只有本级levelStack.length === 0才显示 // 判断是否显示手机号只有本级levelStack.length === 0才显示
@@ -258,13 +308,27 @@ const DealerTeam: React.FC = () => {
<View className="flex-1"> <View className="flex-1">
<View className="flex items-center justify-between mb-1"> <View className="flex items-center justify-between mb-1">
<View className="flex items-center"> <View className="flex items-center">
<Text className="font-semibold text-gray-800 mr-2"> <Space>
{member.nickname} {member.alias ? <Text className="font-semibold text-blue-700 mr-2">{member.alias}</Text> :
</Text> <Text className="font-semibold text-gray-800 mr-2">{member.nickname}</Text>}
{/*别名备注*/}
<Edit size={14} className={'text-blue-500 mr-2'} onClick={(e) => {
e.stopPropagation()
editAlias(member, index)
}}/>
{/*发送消息*/}
<Message size={14} className={'text-orange-500 mr-2'} onClick={(e) => {
e.stopPropagation()
sendMessage(member)
}}/>
</Space>
</View> </View>
{/* 显示手机号(仅本级可见) */} {/* 显示手机号(仅本级可见) */}
{showPhone && member.phone && ( {showPhone && member.phone && (
<Text className="text-sm text-gray-500"> <Text className="text-sm text-gray-500" onClick={(e) => {
e.stopPropagation();
makePhoneCall(member.phone || '');
}}>
{member.phone} {member.phone}
<Phone size={12} className="ml-1 text-green-500"/> <Phone size={12} className="ml-1 text-green-500"/>
</Text> </Text>
@@ -304,6 +368,11 @@ const DealerTeam: React.FC = () => {
const renderOverview = () => ( const renderOverview = () => (
<View className="rounded-xl p-4"> <View className="rounded-xl p-4">
<View
className={'bg-white rounded-lg py-2 px-4 mb-3 shadow-sm text-right text-sm font-medium flex justify-between items-center'}>
<Text className="text-lg font-semibold"></Text>
<Text className={'text-gray-500 '}>{teamMembers.length}</Text>
</View>
{teamMembers.map(renderMemberItem)} {teamMembers.map(renderMemberItem)}
</View> </View>
) )

View File

@@ -21,6 +21,7 @@ import {pageShopDealerWithdraw, addShopDealerWithdraw} from '@/api/shop/shopDeal
import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model' import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model'
import {ShopDealerBank} from "@/api/shop/shopDealerBank/model"; import {ShopDealerBank} from "@/api/shop/shopDealerBank/model";
import {listShopDealerBank} from "@/api/shop/shopDealerBank"; import {listShopDealerBank} from "@/api/shop/shopDealerBank";
import {listCmsWebsiteField} from "@/api/cms/cmsWebsiteField";
interface WithdrawRecordWithDetails extends ShopDealerWithdraw { interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
accountDisplay?: string accountDisplay?: string
@@ -37,6 +38,7 @@ const DealerWithdraw: React.FC = () => {
const [availableAmount, setAvailableAmount] = useState<string>('0.00') const [availableAmount, setAvailableAmount] = useState<string>('0.00')
const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([]) const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([])
const [withdrawAmount, setWithdrawAmount] = useState<string>('') const [withdrawAmount, setWithdrawAmount] = useState<string>('')
const [withdrawValue, setWithdrawValue] = useState<string>('')
const {dealerUser} = useDealerUser() const {dealerUser} = useDealerUser()
@@ -135,12 +137,22 @@ const DealerWithdraw: React.FC = () => {
setIsVisible(false) setIsVisible(false)
} }
function fetchCmsField() {
listCmsWebsiteField({ name: 'WithdrawValue'}).then(res => {
if(res && res.length > 0){
const text = res[0].value;
setWithdrawValue(text || '')
}
})
}
// 初始化加载数据 // 初始化加载数据
useEffect(() => { useEffect(() => {
if (dealerUser?.userId) { if (dealerUser?.userId) {
fetchBalance().then() fetchBalance().then()
fetchWithdrawRecords().then() fetchWithdrawRecords().then()
fetchBanks() fetchBanks()
fetchCmsField()
} }
}, [fetchBalance, fetchWithdrawRecords]) }, [fetchBalance, fetchWithdrawRecords])
@@ -353,6 +365,7 @@ const DealerWithdraw: React.FC = () => {
<Text className={'text-orange-500 px-2 text-lg'}>¥{calculateExpectedAmount(withdrawAmount)}</Text> <Text className={'text-orange-500 px-2 text-lg'}>¥{calculateExpectedAmount(withdrawAmount)}</Text>
</View> </View>
}/> }/>
<Cell title={<Text className={'text-gray-400'}>{withdrawValue}</Text>}/>
</CellGroup> </CellGroup>
<View className="mt-6 px-4"> <View className="mt-6 px-4">

View File

@@ -9,9 +9,14 @@ import navTo from "@/utils/common";
const MyGrid = () => { const MyGrid = () => {
const [list, setList] = useState<CmsNavigation[]>([]) const [list, setList] = useState<CmsNavigation[]>([])
const reload = async () => { const reload = async () => {
const menu = await listCmsNavigation({home: 0}) // 读取首页菜单
const home = await listCmsNavigation({model: 'index'});
const homeId = home[0].navigationId;
if(homeId){
const menu = await listCmsNavigation({home: 0, parentId: homeId})
setList(menu) setList(menu)
} }
}
useEffect(() => { useEffect(() => {
reload().then() reload().then()
@@ -24,10 +29,7 @@ const MyGrid = () => {
// @ts-ignore // @ts-ignore
return ( return (
<> <>
<View className={'p-4'} <View className={'p-4'}>
style={{
marginTop: '5vh'
}}>
<View className={' bg-white rounded-2xl py-4'}> <View className={' bg-white rounded-2xl py-4'}>
<View className={'title font-medium px-4'}></View> <View className={'title font-medium px-4'}></View>
<Divider /> <Divider />

View File

@@ -14,9 +14,10 @@ const Page = () => {
const reload = async () => { const reload = async () => {
// 读取首页菜单 // 读取首页菜单
const home = await listCmsNavigation({model: 'index'}); const home = await listCmsNavigation({model: 'index'});
if (home && home.length > 0) { const homeId = home[0].navigationId;
if (homeId) {
// 读取首页导航条 // 读取首页导航条
const menus = await listCmsNavigation({parentId: home[0].navigationId, hide: 0}); const menus = await listCmsNavigation({parentId: homeId, hide: 0});
setNavItems(menus || []) setNavItems(menus || [])
} }
}; };

View File

@@ -0,0 +1,47 @@
import {useEffect, useState} from 'react'
import { Dialog } from '@nutui/nutui-react-taro'
import {getCmsNavigation} from "@/api/cms/cmsNavigation";
import {CmsNavigation} from "@/api/cms/cmsNavigation/model";
import {RichText} from '@tarojs/components'
const PopUpAd = () => {
const [visible, setVisible] = useState(false)
const [item, setItem] = useState<CmsNavigation>()
const reload = async () => {
const navigation = await getCmsNavigation(4426)
if(navigation && navigation.hide == 0){
setItem(navigation)
setVisible(true)
}
}
useEffect(() => {
reload().then()
}, [])
return (
<>
<Dialog
title={
<div className={'font-bold mb-3'}></div>
}
footer={null}
closeIcon
closeIconPosition="top-right"
style={{
// @ts-ignore
'--nutui-dialog-close-color': '#8c8c8c',
}}
onConfirm={() => setVisible(false)}
onCancel={() => setVisible(false)}
visible={visible}
onClose={() => {
setVisible(false)
}}
>
<RichText nodes={item?.design?.content}/>
</Dialog>
</>
)
}
export default PopUpAd

View File

@@ -10,6 +10,7 @@ import Menu from "./Menu";
import Banner from "./Banner"; import Banner from "./Banner";
import './index.scss' import './index.scss'
import Grid from "@/pages/index/Grid"; import Grid from "@/pages/index/Grid";
import PopUpAd from "@/pages/index/PopUpAd";
function Home() { function Home() {
// 吸顶状态 // 吸顶状态
@@ -117,6 +118,7 @@ function Home() {
<BestSellers/> <BestSellers/>
<Grid /> <Grid />
</View> </View>
<PopUpAd />
</> </>
) )
} }

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '站内消息'
})

View File

@@ -0,0 +1,167 @@
import {useState, useCallback, useEffect} from 'react'
import {View, Text} from '@tarojs/components'
import Taro from '@tarojs/taro'
import {Loading, InfiniteLoading, Empty, Space, Tag} from '@nutui/nutui-react-taro'
import {pageShopChatConversation} from "@/api/shop/shopChatConversation";
import FixedButton from "@/components/FixedButton";
const Index = () => {
const [list, setList] = useState<any[]>([])
const [loading, setLoading] = useState<boolean>(false)
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
// 获取消息数据
const fetchMessageData = useCallback(async (resetPage = false, targetPage?: number, searchKeyword?: string) => {
setLoading(true);
try {
const currentPage = resetPage ? 1 : (targetPage || page);
// 构建API参数根据状态筛选
const params: any = {
page: currentPage
};
// 添加搜索关键词
if (searchKeyword && searchKeyword.trim()) {
params.keywords = searchKeyword.trim();
}
const res = await pageShopChatConversation(params);
if (res?.list && res.list.length > 0) {
// 正确映射状态
const mappedList = res.list.map(customer => ({
...customer
}));
// 如果是重置页面或第一页,直接设置新数据;否则追加数据
if (resetPage || currentPage === 1) {
setList(mappedList);
} else {
setList(prevList => prevList.concat(mappedList));
}
// 正确判断是否还有更多数据
const hasMoreData = res.list.length >= 10; // 假设每页10条数据
setHasMore(hasMoreData);
} else {
if (resetPage || currentPage === 1) {
setList([]);
}
setHasMore(false);
}
setPage(currentPage);
} catch (error) {
console.error('获取消息数据失败:', error);
Taro.showToast({
title: '加载失败,请重试',
icon: 'none'
});
} finally {
setLoading(false);
}
}, [page]);
const reloadMore = async () => {
if (loading || !hasMore) return; // 防止重复加载
const nextPage = page + 1;
await fetchMessageData(false, nextPage);
}
// 获取列表数据(现在使用服务端搜索,不需要消息端过滤)
const getFilteredList = () => {
return list;
};
useEffect(() => {
// 初始化时加载数据
fetchMessageData(true, 1, '');
}, []);
// 渲染消息项
const renderMessageItem = (customer: any) => (
<View key={customer.userId} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
<View className="flex items-center">
<View className="flex-1">
<View className="flex items-center justify-between mb-1">
<Text className="font-semibold text-gray-800 mr-2">
XXXX的通知
</Text>
<Tag type={'warning'}></Tag>
{/*<Tag type={'success'}>已读</Tag>*/}
</View>
<Space direction={'vertical'}>
{/*<Text className="text-xs text-gray-500">统一代码:{customer.dealerCode}</Text>*/}
<Text className="text-xs text-gray-500">
{customer.createTime}
</Text>
</Space>
</View>
</View>
</View>
);
// 渲染消息列表
const renderMessageList = () => {
const filteredList = getFilteredList();
return (
<View className="p-4" style={{
height: '90vh',
overflowY: 'auto',
overflowX: 'hidden'
}}>
<InfiniteLoading
target="scroll"
hasMore={hasMore}
onLoadMore={reloadMore}
onScroll={() => {
// 滚动事件处理
}}
onScrollToUpper={() => {
// 滚动到顶部事件处理
}}
loadingText={
<>
...
</>
}
loadMoreText={
filteredList.length === 0 ? (
<Empty
style={{backgroundColor: 'transparent'}}
description={loading ? "加载中..." : "暂无消息数据"}
/>
) : (
<View className={'h-12 flex items-center justify-center'}>
<Text className="text-gray-500 text-sm"></Text>
</View>
)
}
>
{loading && filteredList.length === 0 ? (
<View className="flex items-center justify-center py-8">
<Loading/>
<Text className="text-gray-500 mt-2 ml-2">...</Text>
</View>
) : (
filteredList.map(renderMessageItem)
)}
</InfiniteLoading>
</View>
);
};
return (
<View className="min-h-screen bg-gray-50">
{/* 消息列表 */}
{renderMessageList()}
<FixedButton />
</View>
);
};
export default Index;

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '发送消息',
navigationBarTextStyle: 'black'
})

View File

@@ -0,0 +1,135 @@
import {useEffect, useState, useRef} from "react";
import {useRouter} from '@tarojs/taro'
import {Loading, CellGroup, Input, Form, Cell, Avatar} from '@nutui/nutui-react-taro'
import {ArrowRight} from '@nutui/icons-react-taro'
import {View, Text} from '@tarojs/components'
import Taro from '@tarojs/taro'
import FixedButton from "@/components/FixedButton";
import {addShopChatMessage} from "@/api/shop/shopChatMessage";
import {ShopChatMessage} from "@/api/shop/shopChatMessage/model";
import navTo from "@/utils/common";
import {getUser} from "@/api/system/user";
import {User} from "@/api/system/user/model";
const AddMessage = () => {
const {params} = useRouter();
const [toUser, setToUser] = useState<User>()
const [loading, setLoading] = useState<boolean>(true)
const [FormData, _] = useState<ShopChatMessage>()
const formRef = useRef<any>(null)
// 判断是编辑还是新增模式
const isEditMode = !!params.id
const toUserId = params.id ? Number(params.id) : undefined
const reload = async () => {
if(toUserId){
getUser(Number(toUserId)).then(data => {
setToUser(data)
})
}
}
// 提交表单
const submitSucceed = async (values: any) => {
try {
// 准备提交的数据
const submitData = {
...values
};
console.log('提交数据:', submitData)
// 参数校验
if(!toUser){
Taro.showToast({
title: `请选择发送对象`,
icon: 'error'
});
return false;
}
// 判断内容是否为空
if (!values.content) {
Taro.showToast({
title: `请输入内容`,
icon: 'error'
});
return false;
}
// 执行新增或更新操作
await addShopChatMessage({
toUserId: toUserId,
formUserId: Taro.getStorageSync('UserId'),
type: 'text',
content: values.content
});
Taro.showToast({
title: `发送成功`,
icon: 'success'
});
setTimeout(() => {
Taro.navigateBack();
}, 1000);
} catch (error) {
console.error('发送失败:', error);
Taro.showToast({
title: `发送失败`,
icon: 'error'
});
}
}
const submitFailed = (error: any) => {
console.log(error, 'err...')
}
useEffect(() => {
reload().then(() => {
setLoading(false)
})
}, [isEditMode]);
if (loading) {
return <Loading className={'px-2'}></Loading>
}
return (
<>
<Cell title={toUser ? (
<View className={'flex items-center'}>
<Avatar src={toUser.avatar}/>
<View className={'ml-2 flex flex-col'}>
<Text>{toUser.alias || toUser.nickname}</Text>
<Text className={'text-gray-300'}>{toUser.mobile}</Text>
</View>
</View>
) : '选择发送对象'} extra={(
<ArrowRight color="#cccccc" className={toUser ? 'mt-2' : ''} size={toUser ? 20 : 18}/>
)}
onClick={() => navTo(`/dealer/team/index`, true)}/>
<Form
ref={formRef}
divider
initialValues={FormData}
labelPosition="left"
onFinish={(values) => submitSucceed(values)}
onFinishFailed={(errors) => submitFailed(errors)}
>
<CellGroup style={{padding: '4px 0'}}>
<Form.Item name="content" initialValue={FormData?.content} required>
<Input placeholder="发消息" maxLength={300}/>
</Form.Item>
</CellGroup>
</Form>
{/* 底部浮动按钮 */}
<FixedButton text={isEditMode ? '立即发送' : '立即发送'} onClick={() => formRef.current?.submit()}/>
</>
);
};
export default AddMessage;

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '查看消息',
navigationBarTextStyle: 'black'
})

View File

@@ -0,0 +1,77 @@
import {useEffect, useState} from "react";
import {useRouter} from '@tarojs/taro'
import {CellGroup, Cell, Loading, Avatar} from '@nutui/nutui-react-taro'
import {View,Text} from '@tarojs/components'
import {ArrowRight} from '@nutui/icons-react-taro'
import {getShopChatMessage, updateShopChatMessage} from "@/api/shop/shopChatMessage";
import {ShopChatMessage} from "@/api/shop/shopChatMessage/model";
import navTo from "@/utils/common";
const AddMessageDetail = () => {
const {params} = useRouter();
const [loading, setLoading] = useState<boolean>(true)
const [item, setItem] = useState<ShopChatMessage>()
const reload = () => {
const id = params.id ? Number(params.id) : undefined
if (id) {
getShopChatMessage(id).then(data => {
setItem(data)
setLoading(false)
updateShopChatMessage({
...data,
status: 1
}).then(() => {
console.log('设为已读')
})
})
}
}
useEffect(() => {
reload()
}, []);
if (loading) {
return <Loading className={'px-2'}></Loading>
}
return (
<>
<Cell style={{
display: 'none'
}} title={item?.formUserId ? (
<View className={'flex items-center'}>
<Avatar src={item.formUserAvatar}/>
<View className={'ml-2 flex flex-col'}>
<Text>{item.formUserAlias || item.formUserName}</Text>
<Text className={'text-gray-300'}>{item.formUserPhone}</Text>
</View>
</View>
) : '选择发送对象'} extra={(
<ArrowRight color="#cccccc" className={item ? 'mt-2' : ''} size={item ? 20 : 18}/>
)}
onClick={() => navTo(`/dealer/team/index`, true)}/>
<CellGroup>
<Cell title={'发布人'} extra={item?.formUserAlias || item?.formUserName}/>
<Cell title={'创建时间'} extra={item?.createTime}/>
<Cell title={'状态'} extra={
item?.status === 0 ? '未读' : '已读'
}/>
{/*<Cell title={(*/}
{/* <>*/}
{/* <Text>{'消息内容:'}</Text>*/}
{/* <Text>{item?.content}</Text>*/}
{/* </>*/}
{/*)} />*/}
</CellGroup>
<CellGroup>
<Cell title={(
<Text>{item?.content}</Text>
)} />
</CellGroup>
</>
);
};
export default AddMessageDetail;

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '我的消息'
})

View File

@@ -0,0 +1,179 @@
import {useState, useCallback, useEffect} from 'react'
import {View, Text} from '@tarojs/components'
import Taro from '@tarojs/taro'
import {Loading, InfiniteLoading, Empty, Avatar, Badge} from '@nutui/nutui-react-taro'
import FixedButton from "@/components/FixedButton";
import {ShopChatMessage} from "@/api/shop/shopChatMessage/model";
import {pageShopChatMessage} from "@/api/shop/shopChatMessage";
import navTo from "@/utils/common";
const MessageIndex = () => {
const [list, setList] = useState<ShopChatMessage[]>([])
const [loading, setLoading] = useState<boolean>(false)
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
// 获取消息数据
const fetchMessageData = useCallback(async (resetPage = false, targetPage?: number, searchKeyword?: string) => {
setLoading(true);
try {
const currentPage = resetPage ? 1 : (targetPage || page);
// 构建API参数根据状态筛选
const params: any = {
type: 'text',
page: currentPage,
toUserId: Taro.getStorageSync('UserId')
};
// 添加搜索关键词
if (searchKeyword && searchKeyword.trim()) {
params.keywords = searchKeyword.trim();
}
const res = await pageShopChatMessage(params);
if (res?.list && res.list.length > 0) {
// 正确映射状态
const mappedList = res.list.map(customer => ({
...customer
}));
// 如果是重置页面或第一页,直接设置新数据;否则追加数据
if (resetPage || currentPage === 1) {
setList(mappedList);
} else {
setList(prevList => prevList.concat(mappedList));
}
// 正确判断是否还有更多数据
const hasMoreData = res.list.length >= 10; // 假设每页10条数据
setHasMore(hasMoreData);
} else {
if (resetPage || currentPage === 1) {
setList([]);
}
setHasMore(false);
}
setPage(currentPage);
} catch (error) {
console.error('获取消息数据失败:', error);
Taro.showToast({
title: '加载失败,请重试',
icon: 'none'
});
} finally {
setLoading(false);
}
}, [page]);
const reloadMore = async () => {
if (loading || !hasMore) return; // 防止重复加载
const nextPage = page + 1;
await fetchMessageData(false, nextPage);
}
// 获取列表数据(现在使用服务端搜索,不需要消息端过滤)
const getFilteredList = () => {
return list;
};
useEffect(() => {
// 初始化时加载数据
fetchMessageData(true, 1, '');
}, []);
// 渲染消息项
const renderMessageItem = (item: any) => (
<View key={item.userId} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
<View className="flex items-center" onClick={() => navTo(`/user/chat/message/detail?id=${item.id}`,true)}>
<View className="flex-1">
<View className="flex items-center justify-between mb-1">
<View className={'flex w-full'}>
<Badge style={{marginInlineEnd: '10px'}} dot={item.status === 0} top="2" right="4">
<Avatar
size="40"
src={item.formUserAvatar}
/>
</Badge>
<View className="flex flex-col w-full">
<View className="flex items-center w-full justify-between">
<Text className="font-semibold text-gray-800 mr-2">{item.formUserAlias || item.formUserName}</Text>
<Text className="text-xs text-gray-500">
{item.createTime}
</Text>
</View>
<Text className="text-gray-500 mt-2 mr-2">
{item.content}
</Text>
</View>
</View>
</View>
</View>
</View>
</View>
);
// 渲染消息列表
const renderMessageList = () => {
const filteredList = getFilteredList();
return (
<View className="p-4" style={{
height: '90vh',
overflowY: 'auto',
overflowX: 'hidden'
}}>
<InfiniteLoading
target="scroll"
hasMore={hasMore}
onLoadMore={reloadMore}
onScroll={() => {
// 滚动事件处理
}}
onScrollToUpper={() => {
// 滚动到顶部事件处理
}}
loadingText={
<>
...
</>
}
loadMoreText={
filteredList.length === 0 ? (
<Empty
style={{backgroundColor: 'transparent'}}
description={loading ? "加载中..." : "暂无消息数据"}
/>
) : (
<View className={'h-12 flex items-center justify-center'}>
<Text className="text-gray-500 text-sm"></Text>
</View>
)
}
>
{loading && filteredList.length === 0 ? (
<View className="flex items-center justify-center py-8">
<Loading/>
<Text className="text-gray-500 mt-2 ml-2">...</Text>
</View>
) : (
filteredList.map(renderMessageItem)
)}
</InfiniteLoading>
</View>
);
};
return (
<View className="min-h-screen bg-gray-50">
{/* 消息列表 */}
{renderMessageList()}
<FixedButton text={'发送消息'} onClick={() => navTo(`/user/chat/message/add`,true)}/>
</View>
);
};
export default MessageIndex;