diff --git a/.env.base b/.env.base index fdb26d85..cf433822 100644 --- a/.env.base +++ b/.env.base @@ -4,10 +4,10 @@ NODE_ENV=development VITE_DEV=true # 请求路径 -VITE_BASE_URL='http://localhost:48080' +VITE_BASE_URL='http://127.0.0.1:48080' # 上传路径 -VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload' +VITE_UPLOAD_URL='http://127.0.0.1:48080/admin-api/infra/file/upload' # 接口前缀 VITE_API_BASEPATH=/dev-api diff --git a/src/api/mall/product/favorite.ts b/src/api/mall/product/favorite.ts new file mode 100644 index 00000000..3834eed0 --- /dev/null +++ b/src/api/mall/product/favorite.ts @@ -0,0 +1,12 @@ +import request from '@/config/axios' + +export interface Favorite { + id?: number + userId?: string // 用户编号 + spuId?: number | null // 商品 SPU 编号 +} + +// 获得 ProductFavorite 列表 +export const getFavoritePage = (params: PageParam) => { + return request.get({ url: '/product/favorite/page', params }) +} diff --git a/src/api/system/notice/index.ts b/src/api/system/notice/index.ts index 62bf5259..f6434697 100644 --- a/src/api/system/notice/index.ts +++ b/src/api/system/notice/index.ts @@ -35,3 +35,8 @@ export const updateNotice = (data: NoticeVO) => { export const deleteNotice = (id: number) => { return request.delete({ url: '/system/notice/delete?id=' + id }) } + +// 推送公告 +export const pushNotice = (id: number) => { + return request.post({ url: '/system/notice/push?id=' + id }) +} diff --git a/src/api/system/sms/smsLog/index.ts b/src/api/system/sms/smsLog/index.ts index 3d54fac1..f9891716 100644 --- a/src/api/system/sms/smsLog/index.ts +++ b/src/api/system/sms/smsLog/index.ts @@ -15,8 +15,6 @@ export interface SmsLogVO { userType: number | null sendStatus: number | null sendTime: Date | null - sendCode: number | null - sendMsg: string apiSendCode: string apiSendMsg: string apiRequestId: string diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index 4f95852f..6346a3d3 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -437,5 +437,6 @@ export default { btn_zoom_in: '放大', btn_zoom_out: '缩小', preview: '预览' - } + }, + 'OAuth 2.0': 'OAuth 2.0' // 避免菜单名是 OAuth 2.0 时,一直 warn 报错 } diff --git a/src/views/infra/webSocket/index.vue b/src/views/infra/webSocket/index.vue index ce6db798..0f609213 100644 --- a/src/views/infra/webSocket/index.vue +++ b/src/views/infra/webSocket/index.vue @@ -1,5 +1,6 @@ <template> <div class="flex"> + <!-- 左侧:建立连接、发送消息 --> <el-card :gutter="12" class="w-1/2" shadow="always"> <template #header> <div class="card-header"> @@ -11,28 +12,38 @@ <el-tag :color="getTagColor">{{ status }}</el-tag> </div> <hr class="my-4" /> - <div class="flex"> <el-input v-model="server" disabled> - <template #prepend> 服务地址</template> + <template #prepend>服务地址</template> </el-input> - <el-button :type="getIsOpen ? 'danger' : 'primary'" @click="toggle"> + <el-button :type="getIsOpen ? 'danger' : 'primary'" @click="toggleConnectStatus"> {{ getIsOpen ? '关闭连接' : '开启连接' }} </el-button> </div> - <p class="mt-4 text-lg font-medium">设置</p> + <p class="mt-4 text-lg font-medium">消息输入框</p> <hr class="my-4" /> <el-input - v-model="sendValue" + v-model="sendText" :autosize="{ minRows: 2, maxRows: 4 }" :disabled="!getIsOpen" clearable type="textarea" + placeholder="请输入你要发送的消息" /> - <el-button :disabled="!getIsOpen" block class="mt-4" type="primary" @click="handlerSend"> + <el-select v-model="sendUserId" class="mt-4" placeholder="请选择发送人"> + <el-option key="" label="所有人" value="" /> + <el-option + v-for="user in userList" + :key="user.id" + :label="user.nickname" + :value="user.id" + /> + </el-select> + <el-button :disabled="!getIsOpen" block class="ml-2 mt-4" type="primary" @click="handlerSend"> 发送 </el-button> </el-card> + <!-- 右侧:消息记录 --> <el-card :gutter="12" class="w-1/2" shadow="always"> <template #header> <div class="card-header"> @@ -41,13 +52,13 @@ </template> <div class="max-h-80 overflow-auto"> <ul> - <li v-for="item in getList" :key="item.time" class="mt-2"> + <li v-for="msg in messageList.reverse()" :key="msg.time" class="mt-2"> <div class="flex items-center"> <span class="text-primary mr-2 font-medium">收到消息:</span> - <span>{{ formatDate(item.time) }}</span> + <span>{{ formatDate(msg.time) }}</span> </div> <div> - {{ item.res }} + {{ msg.text }} </div> </li> </ul> @@ -57,62 +68,113 @@ </template> <script lang="ts" setup> import { formatDate } from '@/utils/formatTime' -import { useUserStore } from '@/store/modules/user' import { useWebSocket } from '@vueuse/core' +import { getAccessToken } from '@/utils/auth' +import * as UserApi from '@/api/system/user' defineOptions({ name: 'InfraWebSocket' }) -const userStore = useUserStore() - -const sendValue = ref('') +const message = useMessage() // 消息弹窗 const server = ref( - (import.meta.env.VITE_BASE_URL + '/websocket/message').replace('http', 'ws') + - '?userId=' + - userStore.getUser.id -) - -const state = reactive({ - recordList: [] as { id: number; time: number; res: string }[] -}) + (import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') + '?token=' + getAccessToken() +) // WebSocket 服务地址 +const getIsOpen = computed(() => status.value === 'OPEN') // WebSocket 连接是否打开 +const getTagColor = computed(() => (getIsOpen.value ? 'success' : 'red')) // WebSocket 连接的展示颜色 +/** 发起 WebSocket 连接 */ const { status, data, send, close, open } = useWebSocket(server.value, { autoReconnect: false, heartbeat: true }) +/** 监听接收到的数据 */ +const messageList = ref([] as { time: number; text: string }[]) // 消息列表 watchEffect(() => { - if (data.value) { - try { - const res = JSON.parse(data.value) - state.recordList.push(res) - } catch (error) { - state.recordList.push({ - res: data.value, - id: Math.ceil(Math.random() * 1000), + if (!data.value) { + return + } + try { + // 1. 收到心跳 + if (data.value === 'pong') { + // state.recordList.push({ + // text: '【心跳】', + // time: new Date().getTime() + // }) + return + } + + // 2.1 解析 type 消息类型 + const jsonMessage = JSON.parse(data.value) + const type = jsonMessage.type + const content = JSON.parse(jsonMessage.content) + if (!type) { + message.error('未知的消息类型:' + data.value) + return + } + // 2.2 消息类型:demo-message-receive + if (type === 'demo-message-receive') { + const single = content.single + if (single) { + messageList.value.push({ + text: `【单发】用户编号(${content.fromUserId}):${content.text}`, + time: new Date().getTime() + }) + } else { + messageList.value.push({ + text: `【群发】用户编号(${content.fromUserId}):${content.text}`, + time: new Date().getTime() + }) + } + return + } + // 2.3 消息类型:notice-push + if (type === 'notice-push') { + messageList.value.push({ + text: `【系统通知】:${content.title}`, time: new Date().getTime() }) + return } + message.error('未处理消息:' + data.value) + } catch (error) { + message.error('处理消息发生异常:' + data.value) + console.error(error) } }) -const getIsOpen = computed(() => status.value === 'OPEN') -const getTagColor = computed(() => (getIsOpen.value ? 'success' : 'red')) - -const getList = computed(() => { - return [...state.recordList].reverse() -}) - -function handlerSend() { - send(sendValue.value) - sendValue.value = '' +/** 发送消息 */ +const sendText = ref('') // 发送内容 +const sendUserId = ref('') // 发送人 +const handlerSend = () => { + // 1.1 先 JSON 化 message 消息内容 + const messageContent = JSON.stringify({ + text: sendText.value, + toUserId: sendUserId.value + }) + // 1.2 再 JSON 化整个消息 + const jsonMessage = JSON.stringify({ + type: 'demo-message-send', + content: messageContent + }) + // 2. 最后发送消息 + send(jsonMessage) + sendText.value = '' } -function toggle() { +/** 切换 websocket 连接状态 */ +const toggleConnectStatus = () => { if (getIsOpen.value) { close() } else { open() } } + +/** 初始化 **/ +const userList = ref<any[]>([]) // 用户列表 +onMounted(async () => { + // 获取用户列表 + userList.value = await UserApi.getSimpleUserList() +}) </script> diff --git a/src/views/member/user/detail/UserFavoriteList.vue b/src/views/member/user/detail/UserFavoriteList.vue new file mode 100644 index 00000000..afab9a08 --- /dev/null +++ b/src/views/member/user/detail/UserFavoriteList.vue @@ -0,0 +1,96 @@ +<template> + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list"> + <el-table-column key="id" align="center" label="商品编号" width="180" prop="id" /> + <el-table-column label="商品图" min-width="80"> + <template #default="{ row }"> + <el-image :src="row.picUrl" class="h-30px w-30px" @click="imagePreview(row.picUrl)" /> + </template> + </el-table-column> + <el-table-column :show-overflow-tooltip="true" label="商品名称" min-width="300" prop="name" /> + <el-table-column align="center" label="商品售价" min-width="90" prop="price"> + <template #default="{ row }"> {{ floatToFixed2(row.price) }}元</template> + </el-table-column> + <el-table-column align="center" label="销量" min-width="90" prop="salesCount" /> + <el-table-column + :formatter="dateFormatter" + align="center" + label="收藏时间" + prop="createTime" + width="180" + /> + <el-table-column align="center" label="状态" min-width="80"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.PRODUCT_SPU_STATUS" :value="scope.row.status" /> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> +</template> + +<script lang="ts" setup> +import { DICT_TYPE } from '@/utils/dict' +import { dateFormatter } from '@/utils/formatTime' +import * as FavoriteApi from '@/api/mall/product/favorite' +import { floatToFixed2 } from '@/utils' + +const message = useMessage() // 消息弹窗 +const { t } = useI18n() // 国际化 + +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + name: null, + createTime: [], + userId: NaN +}) +const queryFormRef = ref() // 搜索的表单 + +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + const data = await FavoriteApi.getFavoritePage(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } +} + +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + getList() +} + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + handleQuery() +} + +const { userId } = defineProps({ + userId: { + type: Number, + required: true + } +}) + +/** 初始化 **/ +onMounted(() => { + queryParams.userId = userId + getList() +}) +</script> diff --git a/src/views/member/user/detail/index.vue b/src/views/member/user/detail/index.vue index 1bac010e..6237cca6 100644 --- a/src/views/member/user/detail/index.vue +++ b/src/views/member/user/detail/index.vue @@ -48,7 +48,9 @@ <UserOrderList :user-id="id" /> </el-tab-pane> <el-tab-pane label="售后管理" lazy>售后管理(WIP)</el-tab-pane> - <el-tab-pane label="收藏记录" lazy>收藏记录(WIP)</el-tab-pane> + <el-tab-pane label="收藏记录" lazy> + <UserFavoriteList :user-id="id" /> + </el-tab-pane> <el-tab-pane label="优惠劵" lazy> <UserCouponList :user-id="id" /> </el-tab-pane> @@ -76,6 +78,7 @@ import UserExperienceRecordList from './UserExperienceRecordList.vue' import UserOrderList from './UserOrderList.vue' import UserPointList from './UserPointList.vue' import UserSignList from './UserSignList.vue' +import UserFavoriteList from './UserFavoriteList.vue' import { CardTitle } from '@/components/Card/index' import { ElMessage } from 'element-plus' diff --git a/src/views/system/notice/index.vue b/src/views/system/notice/index.vue index dc776d3a..f482f91c 100644 --- a/src/views/system/notice/index.vue +++ b/src/views/system/notice/index.vue @@ -87,6 +87,9 @@ > 删除 </el-button> + <el-button link @click="handlePush(scope.row.id)" v-hasPermi="['system:notice:update']"> + 推送 + </el-button> </template> </el-table-column> </el-table> @@ -168,6 +171,17 @@ const handleDelete = async (id: number) => { } catch {} } +/** 推送按钮操作 */ +const handlePush = async (id: number) => { + try { + // 推送的二次确认 + await message.confirm('是否推送所选中通知?') + // 发起推送 + await NoticeApi.pushNotice(id) + message.success(t('推送成功')) + } catch {} +} + /** 初始化 **/ onMounted(() => { getList() diff --git a/src/views/system/sms/log/SmsLogDetail.vue b/src/views/system/sms/log/SmsLogDetail.vue index 34c5e58e..b0d22c2d 100644 --- a/src/views/system/sms/log/SmsLogDetail.vue +++ b/src/views/system/sms/log/SmsLogDetail.vue @@ -37,9 +37,6 @@ <el-descriptions-item label="发送时间"> {{ formatDate(detailData.sendTime) }} </el-descriptions-item> - <el-descriptions-item label="发送结果"> - {{ detailData.sendCode }} | {{ detailData.sendMsg }} - </el-descriptions-item> <el-descriptions-item label="API 发送结果"> {{ detailData.apiSendCode }} | {{ detailData.apiSendMsg }} </el-descriptions-item>