From 84795d10cdbc1c67ece0678c9ae0e02c741b6448 Mon Sep 17 00:00:00 2001 From: hw Date: Tue, 4 Nov 2025 16:53:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=87=AA=E5=8A=A8=E5=9B=9E=E5=A4=8D?= =?UTF-8?q?=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-antd/package.json | 2 + apps/web-antd/src/assets/imgs/wechat.png | Bin 0 -> 1881 bytes apps/web-antd/src/store/index.ts | 11 + apps/web-antd/src/store/tagsView.ts | 202 +++++++++++++ apps/web-antd/src/utils/index.ts | 27 ++ apps/web-antd/src/utils/routerHelper.ts | 22 ++ apps/web-antd/src/views/mp/autoReply/data.ts | 88 ++++++ .../web-antd/src/views/mp/autoReply/index.vue | 272 ++++++++++++++++-- .../views/mp/autoReply/modules/ReplyForm.vue | 139 +++++++++ .../views/mp/autoReply/modules/ReplyTable.vue | 55 ++++ .../src/views/mp/autoReply/modules/form.vue | 142 +++++++++ .../src/views/mp/autoReply/modules/types.ts | 7 + .../mp/modules/wx-account-select/main.vue | 123 +++++--- .../modules/wx-reply/components/TabVideo.vue | 73 ++++- .../views/mp/modules/wx-video-play/main.vue | 19 +- apps/web-ele/.env.development | 2 + apps/web-ele/package.json | 2 + apps/web-ele/src/assets/imgs/wechat.png | Bin 0 -> 1881 bytes apps/web-ele/src/views/mp/autoReply/data.ts | 90 ++++++ apps/web-ele/src/views/mp/autoReply/index.vue | 253 ++++++++++++++++ .../views/mp/autoReply/modules/ReplyForm.vue | 128 +++++++++ .../views/mp/autoReply/modules/ReplyTable.vue | 55 ++++ .../src/views/mp/autoReply/modules/form.vue | 142 +++++++++ .../src/views/mp/autoReply/modules/types.ts | 7 + .../mp/modules/wx-account-select/main.vue | 84 +++++- .../views/mp/modules/wx-video-play/main.vue | 5 +- pnpm-lock.yaml | 211 ++++++++++++++ pnpm-workspace.yaml | 2 + 28 files changed, 2060 insertions(+), 103 deletions(-) create mode 100644 apps/web-antd/src/assets/imgs/wechat.png create mode 100644 apps/web-antd/src/store/tagsView.ts create mode 100644 apps/web-antd/src/views/mp/autoReply/data.ts create mode 100644 apps/web-antd/src/views/mp/autoReply/modules/ReplyForm.vue create mode 100644 apps/web-antd/src/views/mp/autoReply/modules/ReplyTable.vue create mode 100644 apps/web-antd/src/views/mp/autoReply/modules/form.vue create mode 100644 apps/web-antd/src/views/mp/autoReply/modules/types.ts create mode 100644 apps/web-ele/src/assets/imgs/wechat.png create mode 100644 apps/web-ele/src/views/mp/autoReply/data.ts create mode 100644 apps/web-ele/src/views/mp/autoReply/index.vue create mode 100644 apps/web-ele/src/views/mp/autoReply/modules/ReplyForm.vue create mode 100644 apps/web-ele/src/views/mp/autoReply/modules/ReplyTable.vue create mode 100644 apps/web-ele/src/views/mp/autoReply/modules/form.vue create mode 100644 apps/web-ele/src/views/mp/autoReply/modules/types.ts diff --git a/apps/web-antd/package.json b/apps/web-antd/package.json index 5f315ad38..ef663925f 100644 --- a/apps/web-antd/package.json +++ b/apps/web-antd/package.json @@ -43,6 +43,7 @@ "@vben/styles": "workspace:*", "@vben/types": "workspace:*", "@vben/utils": "workspace:*", + "@videojs-player/vue": "catalog:", "@vueuse/core": "catalog:", "@vueuse/integrations": "catalog:", "ant-design-vue": "catalog:", @@ -59,6 +60,7 @@ "pinia": "catalog:", "steady-xml": "catalog:", "tinymce": "catalog:", + "video.js": "catalog:", "vue": "catalog:", "vue-dompurify-html": "catalog:", "vue-router": "catalog:", diff --git a/apps/web-antd/src/assets/imgs/wechat.png b/apps/web-antd/src/assets/imgs/wechat.png new file mode 100644 index 0000000000000000000000000000000000000000..6afc5e41cfc29d258e0c6135981a7a09b7bcf126 GIT binary patch literal 1881 zcmV-f2d4OmP)Px+6G=otRA@uxT5F6HRTlovt)4-Yt`>DxqM{h1piEaoG!it+;>Ku9`iG12%)6qSb>G)jC#QP~(>XJx8!Ncf>R3nRXU>6!tR>8fLH z&-BPlcUM(+R|8q^kNH#I`M&erd#dK%bDQux7!;JpamncoOxXzv({Uli+0)aoy&Fxi zgW|7-ry`T%jKJPYsh*xLg z=_mZbD@VDSjI=d2u_EpTV{+vZ?vV_<2cWG_H(#rjMoE)uN^1inQ7hpBUdB=@VJ2&# zSJb3xM4tobrdkMkaJE{yxiV`Dvjn&heT6&y}!$;8dSkH)Ae?JF=Qm5e2{TXFM(B5|Eyoa>Qb zi=~@uyi9mKA2ex1-va0|S1@Cjl`zjd6m$He(YfOA2lTk($P%F!8(B{iu%3c|8#NUDitV7DNwa~y0EA>Nbu?!+#%Zpl`| zS!mMGWT{>nB&i=RX1p&z>&T{cvi!#-r=KUtUPy$~m@yH+X#h_IhzaOoa2J@m2-Ja) zvp%yVz0Z%1#K>~p3*cnG0Ri9!K=e0Rf?*>%ML^7F@KpdVER*{sfL|vCo4pKE#L#v# zaB7(X6~X03!V%dU_cx$N-CK7Lh*wlZbZ8J^7c=hf)6J(!aZw}{eTA9iOxAj=v~;sI zoQhpZoHI8gV8L}x2>16dHn$hNlaZu$7ZV-`Sa1{_zisL1%hhCb1&HpenHHmm34a}Z zHl?tk%_U${&4huEE#16GO=<}c+G?mXNX_VT$eG)(Z{L#FlJDf13 zwT=^8cR1@^18ig2*fT57iXNu0r3`tR}is!F3D zS-LqZVrT~#7+WP*wc$I8wj?7)>{&FwOswejRRN_jn0?GM7iE-*~8LJ*>%_n!r5g92N}TF>amg< z7w8c^%fQRTd_F)^0ge^BuM9CJW(o4j;mRO$E|-g#7nL+QE)|_6m_8UH&4TE8F+eMO zmv7ruN+{Q&tVcr#*}XmAJKu&+2whHunqA3x8Xmpl866NYqE9nuF2FB{tg_0@oST?x zXs;is)g8GVq5Zubcf0@iKm=5XK2^EU%QC`yjQdGD#%*)#Sn>+H(se%MS78C?tDhM*>{ z2+z+#h?%`D+hm7-cpO%fIisoQ3NXoT-I##Qerk*Cy!|bmvX4As9Cnj~MztAWNTY%M z50#sbQj>N>Oy-kOliFX1_$mg?uXksla(-wPS#Ojve@iP`2#b^O0DIsA?r(JL%Hz4@s4x|Xv_L12Fc30Q=P TM8y=K00000NkvXXu0mjf4<3N5 literal 0 HcmV?d00001 diff --git a/apps/web-antd/src/store/index.ts b/apps/web-antd/src/store/index.ts index 269586ee8..af0c4ab6f 100644 --- a/apps/web-antd/src/store/index.ts +++ b/apps/web-antd/src/store/index.ts @@ -1 +1,12 @@ +import type { App } from 'vue'; + +import { createPinia } from 'pinia'; + export * from './auth'; +const store = createPinia(); + +export const setupStore = (app: App) => { + app.use(store); +}; + +export { store }; diff --git a/apps/web-antd/src/store/tagsView.ts b/apps/web-antd/src/store/tagsView.ts new file mode 100644 index 000000000..396f581e8 --- /dev/null +++ b/apps/web-antd/src/store/tagsView.ts @@ -0,0 +1,202 @@ +import type { RouteLocationNormalizedLoaded } from 'vue-router'; + +import { useUserStore } from '@vben/stores'; + +import { defineStore } from 'pinia'; + +import { router } from '#/router'; +import { findIndex } from '#/utils'; +import { getRawRoute } from '#/utils/routerHelper'; + +import { store } from './index'; + +const userStore = useUserStore(); + +export interface TagsViewState { + visitedViews: RouteLocationNormalizedLoaded[]; + cachedViews: Set; + selectedTag?: RouteLocationNormalizedLoaded; +} + +export const useTagsViewStore = defineStore('tagsView', { + state: (): TagsViewState => ({ + visitedViews: [], + cachedViews: new Set(), + selectedTag: undefined, + }), + getters: { + getVisitedViews(): RouteLocationNormalizedLoaded[] { + return this.visitedViews; + }, + getCachedViews(): string[] { + return [...this.cachedViews]; + }, + getSelectedTag(): RouteLocationNormalizedLoaded | undefined { + return this.selectedTag; + }, + }, + actions: { + // 新增缓存和tag + addView(view: RouteLocationNormalizedLoaded): void { + this.addVisitedView(view); + this.addCachedView(); + }, + // 新增tag + addVisitedView(view: RouteLocationNormalizedLoaded) { + if (this.visitedViews.some((v) => v.fullPath === view.fullPath)) return; + if (view.meta?.noTagsView) return; + const visitedView = Object.assign({}, view, { + title: view.meta?.title || 'no-name', + }); + + if (visitedView.meta) { + const titleSuffixList: string[] = []; + this.visitedViews.forEach((v) => { + if ( + v.path === visitedView.path && + v.meta?.title === visitedView.meta?.title + ) { + titleSuffixList.push((v.meta?.titleSuffix as string) || '1'); + } + }); + if (titleSuffixList.length > 0) { + let titleSuffix = 1; + while (titleSuffixList.includes(`${titleSuffix}`)) { + titleSuffix += 1; + } + visitedView.meta.titleSuffix = + titleSuffix === 1 ? undefined : `${titleSuffix}`; + } + } + + this.visitedViews.push(visitedView); + }, + // 新增缓存 + addCachedView() { + const cacheMap: Set = new Set(); + for (const v of this.visitedViews) { + const item = getRawRoute(v); + const needCache = !item.meta?.noCache; + if (!needCache) { + continue; + } + const name = item.name as string; + cacheMap.add(name); + } + if ( + [...this.cachedViews].sort().toString() === + [...cacheMap].sort().toString() + ) + return; + this.cachedViews = cacheMap; + }, + // 删除某个 + delView(view: RouteLocationNormalizedLoaded) { + this.delVisitedView(view); + this.delCachedView(); + }, + // 删除tag + delVisitedView(view: RouteLocationNormalizedLoaded) { + for (const [i, v] of this.visitedViews.entries()) { + if (v.fullPath === view.fullPath) { + this.visitedViews.splice(i, 1); + break; + } + } + }, + // 删除缓存 + delCachedView() { + const route = router.currentRoute.value; + const index = findIndex( + this.getCachedViews, + (v) => v === route.name, + ); + for (const v of this.visitedViews) { + if (v.name === route.name) { + return; + } + } + if (index > -1) { + this.cachedViews.delete( + this.getCachedViews[index] as unknown as string, + ); + } + }, + // 删除所有缓存和tag + delAllViews() { + this.delAllVisitedViews(); + this.delCachedView(); + }, + // 删除所有tag + delAllVisitedViews() { + // const userStore = useUserStoreWithOut(); + + // const affixTags = this.visitedViews.filter((tag) => tag.meta.affix) + this.visitedViews = userStore.userInfo + ? this.visitedViews.filter((tag) => tag?.meta?.affix) + : []; + }, + // 删除其他 + delOthersViews(view: RouteLocationNormalizedLoaded) { + this.delOthersVisitedViews(view); + this.addCachedView(); + }, + // 删除其他tag + delOthersVisitedViews(view: RouteLocationNormalizedLoaded) { + this.visitedViews = this.visitedViews.filter((v) => { + return v?.meta?.affix || v.fullPath === view.fullPath; + }); + }, + // 删除左侧 + delLeftViews(view: RouteLocationNormalizedLoaded) { + const index = findIndex( + this.visitedViews, + (v) => v.fullPath === view.fullPath, + ); + if (index > -1) { + this.visitedViews = this.visitedViews.filter((v, i) => { + return v?.meta?.affix || v.fullPath === view.fullPath || i > index; + }); + this.addCachedView(); + } + }, + // 删除右侧 + delRightViews(view: RouteLocationNormalizedLoaded) { + const index = findIndex( + this.visitedViews, + (v) => v.fullPath === view.fullPath, + ); + if (index > -1) { + this.visitedViews = this.visitedViews.filter((v, i) => { + return v?.meta?.affix || v.fullPath === view.fullPath || i < index; + }); + this.addCachedView(); + } + }, + updateVisitedView(view: RouteLocationNormalizedLoaded) { + for (let v of this.visitedViews) { + if (v.fullPath === view.fullPath) { + v = Object.assign(v, view); + break; + } + } + }, + // 设置当前选中的 tag + setSelectedTag(tag: RouteLocationNormalizedLoaded) { + this.selectedTag = tag; + }, + setTitle(title: string, path?: string) { + for (const v of this.visitedViews) { + if (v.path === (path ?? this.selectedTag?.path)) { + v.meta.title = title; + break; + } + } + }, + }, + persist: false, +}); + +export const useTagsViewStoreWithOut = () => { + return useTagsViewStore(store); +}; diff --git a/apps/web-antd/src/utils/index.ts b/apps/web-antd/src/utils/index.ts index 3a066fd4a..5877010a3 100644 --- a/apps/web-antd/src/utils/index.ts +++ b/apps/web-antd/src/utils/index.ts @@ -1,2 +1,29 @@ +import type { Recordable } from '@vben/types'; + export * from './rangePickerProps'; export * from './routerHelper'; + +/** + * 查找数组对象的某个下标 + * @param {Array} ary 查找的数组 + * @param {Function} fn 判断的方法 + */ +type Fn = (item: T, index: number, array: Array) => boolean; +export const findIndex = >( + ary: Array, + fn: Fn, +): number => { + if (ary.findIndex) { + return ary.findIndex((item, index, array) => fn(item, index, array)); + } + let index = -1; + ary.some((item: T, i: number, ary: Array) => { + const ret: boolean = fn(item, i, ary); + if (ret) { + index = i; + return true; + } + return false; + }); + return index; +}; diff --git a/apps/web-antd/src/utils/routerHelper.ts b/apps/web-antd/src/utils/routerHelper.ts index 0f3c52096..a9e99f54b 100644 --- a/apps/web-antd/src/utils/routerHelper.ts +++ b/apps/web-antd/src/utils/routerHelper.ts @@ -1,3 +1,8 @@ +import type { + RouteLocationNormalized, + RouteRecordNormalized, +} from 'vue-router'; + import { defineAsyncComponent } from 'vue'; const modules = import.meta.glob('../views/**/*.{vue,tsx}'); @@ -14,3 +19,20 @@ export function registerComponent(componentPath: string) { } } } + +export const getRawRoute = ( + route: RouteLocationNormalized, +): RouteLocationNormalized => { + if (!route) return route; + const { matched, ...opt } = route; + return { + ...opt, + matched: (matched + ? matched.map((item) => ({ + meta: item.meta, + name: item.name, + path: item.path, + })) + : undefined) as RouteRecordNormalized[], + }; +}; diff --git a/apps/web-antd/src/views/mp/autoReply/data.ts b/apps/web-antd/src/views/mp/autoReply/data.ts new file mode 100644 index 000000000..31c0481b5 --- /dev/null +++ b/apps/web-antd/src/views/mp/autoReply/data.ts @@ -0,0 +1,88 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeGridPropTypes } from '#/adapter/vxe-table'; + +import { markRaw } from 'vue'; + +import { DICT_TYPE } from '@vben/constants'; + +import WxAccountSelect from '#/views/mp/modules/wx-account-select/main.vue'; + +import { MsgType } from './modules/types'; + +/** 获取表格列配置 */ +export function useGridColumns(msgType: MsgType): VxeGridPropTypes.Columns { + const columns: VxeGridPropTypes.Columns = []; + // 请求消息类型列(仅消息回复显示) + if (msgType === MsgType.Message) { + columns.push({ + field: 'requestMessageType', + title: '请求消息类型', + minWidth: 120, + }); + } + + // 关键词列(仅关键词回复显示) + if (msgType === MsgType.Keyword) { + columns.push({ + field: 'requestKeyword', + title: '关键词', + minWidth: 150, + }); + } + + // 匹配类型列(仅关键词回复显示) + if (msgType === MsgType.Keyword) { + columns.push({ + field: 'requestMatch', + title: '匹配类型', + minWidth: 120, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH }, + }, + }); + } + + // 回复消息类型列 + columns.push( + { + field: 'responseMessageType', + title: '回复消息类型', + minWidth: 120, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.MP_MESSAGE_TYPE }, + }, + }, + { + field: 'responseContent', + title: '回复内容', + minWidth: 200, + slots: { default: 'replyContent' }, + }, + { + field: 'createTime', + title: '创建时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 140, + fixed: 'right', + slots: { default: 'actions' }, + }, + ); + return columns; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'accountId', + label: '公众号', + component: markRaw(WxAccountSelect), + }, + ]; +} diff --git a/apps/web-antd/src/views/mp/autoReply/index.vue b/apps/web-antd/src/views/mp/autoReply/index.vue index 06996f6d1..fb1b9ce3f 100644 --- a/apps/web-antd/src/views/mp/autoReply/index.vue +++ b/apps/web-antd/src/views/mp/autoReply/index.vue @@ -1,29 +1,259 @@ diff --git a/apps/web-antd/src/views/mp/autoReply/modules/ReplyForm.vue b/apps/web-antd/src/views/mp/autoReply/modules/ReplyForm.vue new file mode 100644 index 000000000..d8e137083 --- /dev/null +++ b/apps/web-antd/src/views/mp/autoReply/modules/ReplyForm.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/apps/web-antd/src/views/mp/autoReply/modules/ReplyTable.vue b/apps/web-antd/src/views/mp/autoReply/modules/ReplyTable.vue new file mode 100644 index 000000000..bdfb41ade --- /dev/null +++ b/apps/web-antd/src/views/mp/autoReply/modules/ReplyTable.vue @@ -0,0 +1,55 @@ + + + diff --git a/apps/web-antd/src/views/mp/autoReply/modules/form.vue b/apps/web-antd/src/views/mp/autoReply/modules/form.vue new file mode 100644 index 000000000..7f9b9ed38 --- /dev/null +++ b/apps/web-antd/src/views/mp/autoReply/modules/form.vue @@ -0,0 +1,142 @@ + + + diff --git a/apps/web-antd/src/views/mp/autoReply/modules/types.ts b/apps/web-antd/src/views/mp/autoReply/modules/types.ts new file mode 100644 index 000000000..f07a7d758 --- /dev/null +++ b/apps/web-antd/src/views/mp/autoReply/modules/types.ts @@ -0,0 +1,7 @@ +// 消息类型(Follow: 关注时回复;Message: 消息回复;Keyword: 关键词回复) +// 作为 tab.name,enum 的数字不能随意修改,与 api 参数相关 +export enum MsgType { + Follow = 1, + Keyword = 3, + Message = 2, +} diff --git a/apps/web-antd/src/views/mp/modules/wx-account-select/main.vue b/apps/web-antd/src/views/mp/modules/wx-account-select/main.vue index 75272e669..8388a6e9a 100644 --- a/apps/web-antd/src/views/mp/modules/wx-account-select/main.vue +++ b/apps/web-antd/src/views/mp/modules/wx-account-select/main.vue @@ -1,23 +1,26 @@ + + diff --git a/apps/web-ele/src/views/mp/autoReply/modules/ReplyForm.vue b/apps/web-ele/src/views/mp/autoReply/modules/ReplyForm.vue new file mode 100644 index 000000000..f55afc365 --- /dev/null +++ b/apps/web-ele/src/views/mp/autoReply/modules/ReplyForm.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/apps/web-ele/src/views/mp/autoReply/modules/ReplyTable.vue b/apps/web-ele/src/views/mp/autoReply/modules/ReplyTable.vue new file mode 100644 index 000000000..bdfb41ade --- /dev/null +++ b/apps/web-ele/src/views/mp/autoReply/modules/ReplyTable.vue @@ -0,0 +1,55 @@ + + + diff --git a/apps/web-ele/src/views/mp/autoReply/modules/form.vue b/apps/web-ele/src/views/mp/autoReply/modules/form.vue new file mode 100644 index 000000000..a4a37438a --- /dev/null +++ b/apps/web-ele/src/views/mp/autoReply/modules/form.vue @@ -0,0 +1,142 @@ + + + diff --git a/apps/web-ele/src/views/mp/autoReply/modules/types.ts b/apps/web-ele/src/views/mp/autoReply/modules/types.ts new file mode 100644 index 000000000..f07a7d758 --- /dev/null +++ b/apps/web-ele/src/views/mp/autoReply/modules/types.ts @@ -0,0 +1,7 @@ +// 消息类型(Follow: 关注时回复;Message: 消息回复;Keyword: 关键词回复) +// 作为 tab.name,enum 的数字不能随意修改,与 api 参数相关 +export enum MsgType { + Follow = 1, + Keyword = 3, + Message = 2, +} diff --git a/apps/web-ele/src/views/mp/modules/wx-account-select/main.vue b/apps/web-ele/src/views/mp/modules/wx-account-select/main.vue index d154b892f..abad94cce 100644 --- a/apps/web-ele/src/views/mp/modules/wx-account-select/main.vue +++ b/apps/web-ele/src/views/mp/modules/wx-account-select/main.vue @@ -1,7 +1,7 @@