diff --git a/CHANGELOG.md b/CHANGELOG.md index b177b1ba76bcdaa57e5e9bea84c7510ec1b2a66d..d387b9f71e9922cde02062af562225d996952175 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ ## [Unreleased] +### Change + +- 多数据部件最后分割线不显示 +- 新增用户消息面板组件 +- 富文本样式调整 + ## [0.0.25] - 2024-10-10 ### Added diff --git a/src/control/list/md-ctrl/md-ctrl.scss b/src/control/list/md-ctrl/md-ctrl.scss index 63504379f9c4c71088a5d9217e706179438a831b..9b0cc9d2e3e75a4f3bd185313591db527bf17397 100644 --- a/src/control/list/md-ctrl/md-ctrl.scss +++ b/src/control/list/md-ctrl/md-ctrl.scss @@ -66,16 +66,17 @@ $control-mobmdctrl: ( } padding: 0.875rem 0; - &::after { - content: ''; - position: relative; - display: block; - bottom: -0.875rem; - left: 1rem; - height: rem(1px); - width: calc(100% - 2rem); - background-color: getCssVar(color, border); - } +} + +.#{bem(control-mobmdctrl-item)} + .#{bem(control-mobmdctrl-item)}::before { + content: ''; + position: relative; + display: block; + top: -0.875rem; + left: 1rem; + height: rem(1px); + width: calc(100% - 2rem); + background-color: getCssVar(color, border); } @include b(control-mobmdctrl-image) { diff --git a/src/editor/html/quill-editor/quill-editor.scss b/src/editor/html/quill-editor/quill-editor.scss index 98312656c0003d7498ba785299e5887c3018cc8a..b67aac76e52f0f6ec8c31dae28efaede73c10301 100644 --- a/src/editor/html/quill-editor/quill-editor.scss +++ b/src/editor/html/quill-editor/quill-editor.scss @@ -4,37 +4,38 @@ height: 100%; border-radius: 0; } - .van-action-sheet__content { - position: relative; - .content { - display: flex; - height: 100%; - flex-direction: column; - } - .ql-toolbar { - padding: rem(8px) rem(50px); + display: flex; + flex-direction: column; + } + @include e(header) { + flex: none; + } + @include e(content) { + flex: auto; + display: flex; + height: 100%; + flex-direction: column; + } + @include e(footer) { + flex: none; + padding: 10px; + display: flex; + .van-button { + width: 100%; } - .#{bem(quill, cancel)} { - position: absolute; - left: 0px; - top: 0; - padding: rem(8px) rem(12px); - line-height: rem(26px); - cursor: pointer; + .van-button + .van-button { + margin-left: getCssVar(spacing, tight); } - .#{bem(quill, confirm)} { - position: absolute; - right: 0px; - top: 0; - padding: rem(8px) rem(12px); - line-height: rem(26px); - cursor: pointer; + } + .ql-toolbar.ql-snow { + .ql-header.ql-picker { + width: rem(60px); } } // quill多语言特殊处理,后续补充多语言 @include m(zh-cn) { - .ql-picker.ql-size { + .ql-snow .ql-picker.ql-size { .ql-picker-label::before, .ql-picker-item::before { content: '默认' diff --git a/src/editor/html/quill-editor/quill-editor.tsx b/src/editor/html/quill-editor/quill-editor.tsx index 9dbd6108fa9b0bda0da75327cc4c9af1f826775f..1b0a43acfe0f310061f165741f3f3dcc0e33b1a8 100644 --- a/src/editor/html/quill-editor/quill-editor.tsx +++ b/src/editor/html/quill-editor/quill-editor.tsx @@ -210,18 +210,24 @@ const IBizQuill: any = defineComponent({ v-model:show={this.editing} onOpened={this.handleEdit} > -
-
- {ibiz.i18n.t('editor.common.cancel')} -
+
{this.controller.valueMode === 'html' ? (
) : null}
-
+
+
+ + {ibiz.i18n.t('editor.common.cancel')} + + {ibiz.i18n.t('editor.common.confirm')} -
+
diff --git a/src/locale/en/index.ts b/src/locale/en/index.ts index 49cd643b199cf28347516507f6a1274e7036620c..25841c827c07d2b47de9e8a1d20d3323af403cb5 100644 --- a/src/locale/en/index.ts +++ b/src/locale/en/index.ts @@ -157,6 +157,44 @@ export default { processingPersonnel: 'Processing personnel', submissionPath: 'Submission path', }, + userMessage: { + notice: 'Notice', + backendTasks: 'Backend tasks', + allRead: 'All read', + all: 'All', + unread: 'unread', + asyncActionPreview: { + downloadFailedErr: 'Download file failed', + noExistentErr: 'The file stream data does not exist', + importDetailPrompt: 'Import data details-{name}', + parseImportInfoErr: 'Abnormal parsing of import information', + downloadErrFile: 'Download error file', + importTime: 'Import time: ', + importTotal: 'Total number of imports: ', + successImport: 'Number of successful imports: ', + ImportFailed: 'Number of import failures: ', + }, + asyncActionTab: { + noSupportType: + 'Asynchronous operation type {type} is not supported currently', + noAsyncAction: + 'There are currently no asynchronous operations available', + }, + internalMessageContainer: { + markAsRead: 'Mark as read', + }, + internalMessageJson: { + jumpToView: 'Jump to view', + missingHtml: 'Missing HTML in the content of the data', + }, + internalMessageTab: { + noSupportType: + 'The message type {type} on the site is not currently supported', + notificationYet: 'Current no notification', + loadMore: 'Load more({length})', + onlyShowUnread: 'Only show unread', + }, + }, }, // 工具 util: { diff --git a/src/locale/zh-CN/index.ts b/src/locale/zh-CN/index.ts index 580beb3bc672070ee3cf431c469c974421d767e5..76e491f3595db831acc5e61791a5368843d4e85e 100644 --- a/src/locale/zh-CN/index.ts +++ b/src/locale/zh-CN/index.ts @@ -149,6 +149,41 @@ export default { processingPersonnel: '处理人', submissionPath: '提交路径', }, + userMessage: { + notice: '通知', + backendTasks: '后台作业', + allRead: '全部已读', + all: '全部', + unread: '未读', + asyncActionPreview: { + downloadFailedErr: '下载文件失败', + noExistentErr: '文件流数据不存在', + importDetailPrompt: '导入数据详情-{name}', + parseImportInfoErr: '解析导入信息异常', + downloadErrFile: '下载错误文件', + importTime: '导入时间: ', + importTotal: '导入总条数: ', + successImport: '成功导入数: ', + ImportFailed: '导入失败数: ', + }, + asyncActionTab: { + noSupportType: '异步操作类型{type}暂未支持', + noAsyncAction: '暂无异步操作', + }, + internalMessageContainer: { + markAsRead: '标记为已读', + }, + internalMessageJson: { + jumpToView: '跳转到视图', + missingHtml: '数据的content里缺少html', + }, + internalMessageTab: { + noSupportType: '站内消息类型{type}暂未支持', + notificationYet: '暂无通知', + loadMore: '加载更多({length})', + onlyShowUnread: '只显示未读', + }, + }, }, // 工具 util: { diff --git a/src/panel-component/index.ts b/src/panel-component/index.ts index 936151c3b3a4969de6a33106ced3781a19b3fbb9..f82b4b3b7eb6bbbc7b88310192ed5894e9c4c155 100644 --- a/src/panel-component/index.ts +++ b/src/panel-component/index.ts @@ -25,6 +25,7 @@ import IBizPanelTabPanel from './panel-tab-panel'; import IBizPanelCarouse from './panel-carousel'; import IBizPanelVideoPlayer from './panel-video-player'; import IBizAuthUserinfo from './auth-userinfo'; +import IBizMobUserMessage from './user-message'; export const IBizPanelComponents = { install: (v: App): void => { @@ -52,6 +53,7 @@ export const IBizPanelComponents = { v.use(IBizPanelVideoPlayer); v.use(IBizAuthUserinfo); v.use(IBizPanelItemRender); + v.use(IBizMobUserMessage); }, }; diff --git a/src/panel-component/user-message/common/index.ts b/src/panel-component/user-message/common/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..22fd95033373c63d60b109d3f1f5617a531f5d73 --- /dev/null +++ b/src/panel-component/user-message/common/index.ts @@ -0,0 +1,3 @@ +export { InternalMessageContainer } from './internal-message-container/internal-message-container'; +export { InternalMessageDefault } from './internal-message-default/internal-message-default'; +export { InternalMessageDefaultProvider } from './internal-message-default/internal-message-default.provider'; diff --git a/src/panel-component/user-message/common/internal-message-container/internal-message-container.scss b/src/panel-component/user-message/common/internal-message-container/internal-message-container.scss new file mode 100644 index 0000000000000000000000000000000000000000..86b4381c73b7e9d9b64be68f2099e4fec8ddb775 --- /dev/null +++ b/src/panel-component/user-message/common/internal-message-container/internal-message-container.scss @@ -0,0 +1,64 @@ +@include b(internal-message-container) { + position: relative; + + @include m(clickable){ + cursor: pointer; + } + + &:hover{ + .#{bem(internal-message-container-toolbar)}{ + display: block; + } + } + + @include e(unread-tag){ + position: absolute; + top: getCssVar('spacing', 'tight'); + right: getCssVar('spacing', 'tight'); + display: none; + align-self: flex-start; + width: getCssVar('width-icon', 'extra-small'); + height: getCssVar('width-icon', 'extra-small'); + background-color: getCssVar(color, danger); + border-radius: getCssVar(border, radius, circle); + } + + @include e(click-tag){ + position: absolute; + top: 50%; + right: getCssVar('spacing', 'extra-tight'); + font-size: getCssVar('font-size', 'header-5'); + transform: translateY(-50%); + } + + @include m(unread){ + @include e(unread-tag){ + display: block; + } + + &:hover{ + @include e(unread-tag){ + display: none; + } + } + + } + +} + +@include b(internal-message-container-toolbar) { + position: absolute; + top: 0; + right: 0; + display: none; + padding: getCssVar('spacing', 'tight'); + + @include e(button){ + margin-left: getCssVar('spacing', 'tight'); + cursor: pointer; + + &:hover{ + color: getCssVar(color,primary); + } + } +} \ No newline at end of file diff --git a/src/panel-component/user-message/common/internal-message-container/internal-message-container.tsx b/src/panel-component/user-message/common/internal-message-container/internal-message-container.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d42bcc27e0404708a5af7794adf1c95d4e618358 --- /dev/null +++ b/src/panel-component/user-message/common/internal-message-container/internal-message-container.tsx @@ -0,0 +1,149 @@ +/* eslint-disable camelcase */ +import { computed, defineComponent, PropType } from 'vue'; +import { IInternalMessage, showTitle } from '@ibiz-template/core'; +import { useNamespace } from '@ibiz-template/vue3-util'; +import './internal-message-container.scss'; +import { IInternalMessageProvider } from '@ibiz-template/runtime'; + +export type ToolbarItem = { + /** + * 提示文本信息 + * @author lxm + * @date 2024-01-30 03:27:33 + * @type {string} + */ + tooltip: string; + /** + * 图标名称 + * @author lxm + * @date 2024-01-30 03:27:23 + * @type {string} + */ + icon: string; + /** + * 唯一标识 + * @author lxm + * @date 2024-01-30 03:27:16 + * @type {string} + */ + key: string; +}; + +export const InternalMessageContainer = defineComponent({ + name: 'IBizInternalMessageContainer', + props: { + message: { + type: Object as PropType, + required: true, + }, + provider: { + type: Object as PropType, + required: true, + }, + clickable: { + type: Boolean, + default: undefined, + }, + toolbarItems: { + type: Array, + default: () => [] as ToolbarItem[], + }, + }, + emits: { + toolbarClick: (_key: string) => true, + close: () => true, + }, + setup(props, { emit }) { + const ns = useNamespace('internal-message-container'); + + const isUnread = computed(() => { + return props.message.status === 'RECEIVED'; + }); + + const finalToolbarItems = computed(() => { + const toolbarItems = [...props.toolbarItems]; + if (isUnread.value) { + toolbarItems.push({ + key: 'read', + icon: 'checkmark-done-outline', + tooltip: ibiz.i18n.t( + 'panelComponent.userMessage.internalMessageContainer.markAsRead', + ), + }); + } + return toolbarItems; + }); + + const onToolbarClick = (event: MouseEvent, key: string) => { + event.stopPropagation(); + if (key === 'read') { + ibiz.hub.notice.internalMessage.markRead(props.message); + } else { + emit('toolbarClick', key); + } + }; + + const isClickable = computed(() => { + if (props.clickable === undefined) { + return ibiz.env.isMob + ? !!props.message.mobile_url + : !!props.message.url; + } + return props.clickable; + }); + + const onClick = async (event: MouseEvent) => { + if (isClickable.value && props.provider.onClick) { + const isClose = await props.provider.onClick(props.message, event); + if (isClose) { + emit('close'); + } + } + }; + + return { + ns, + isUnread, + isClickable, + finalToolbarItems, + onToolbarClick, + onClick, + }; + }, + render() { + return ( +
+ {this.$slots.default?.()} +
+ {this.finalToolbarItems.map(item => { + return ( + this.onToolbarClick(e, item.key)} + /> + ); + })} +
+
+ {this.isClickable && ( + + )} +
+ ); + }, +}); diff --git a/src/panel-component/user-message/common/internal-message-default/internal-message-default.provider.ts b/src/panel-component/user-message/common/internal-message-default/internal-message-default.provider.ts new file mode 100644 index 0000000000000000000000000000000000000000..7a9b96d569d7978bb28bea28e27bf18e44557fdc --- /dev/null +++ b/src/panel-component/user-message/common/internal-message-default/internal-message-default.provider.ts @@ -0,0 +1,84 @@ +import { IBizContext, IInternalMessage } from '@ibiz-template/core'; +import { + IInternalMessageProvider, + OpenAppViewCommand, + parseViewProtocol, +} from '@ibiz-template/runtime'; +import { VNode, h } from 'vue'; +import { useRouter } from 'vue-router'; +import { InternalMessageDefault } from './internal-message-default'; + +export class InternalMessageDefaultProvider + implements IInternalMessageProvider +{ + component: unknown = InternalMessageDefault; + + router: IData = useRouter(); + + render( + props: IData & { + message: IInternalMessage; + }, + ): VNode { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return h(this.component as any as string, { + provider: this, + ...props, + }); + } + + async onClick( + message: IInternalMessage, + _event: MouseEvent, + ): Promise { + // 打开之前先标记已读 + await ibiz.hub.notice.internalMessage.markRead(message); + + const redirectUrl = ibiz.env.isMob ? message.mobile_url : message.url; + return this.openViewByUrl(redirectUrl); + } + + /** + * 解析url并打开对应视图,打开视图前会先标记已读 + * @author lxm + * @date 2024-02-02 11:56:07 + * @param {IInternalMessage} msg + * @param {string} redirectUrl + * @return {*} {Promise} + */ + async openRedirectView( + msg: IInternalMessage, + redirectUrl: string, + ): Promise { + // 打开视图之前先标记已读 + await ibiz.hub.notice.internalMessage.markRead(msg); + this.openViewByUrl(redirectUrl); + } + + /** + * 解析url并打开对应视图 + * @param {string} redirectUrl + * @return {*} + * @author: zhujiamin + * @Date: 2024-03-04 11:16:40 + */ + openViewByUrl(redirectUrl: string | undefined): boolean { + if (redirectUrl) { + if (redirectUrl.startsWith('view://')) { + const { viewId, context, params } = parseViewProtocol(redirectUrl); + ibiz.commands.execute( + OpenAppViewCommand.TAG, + viewId, + IBizContext.create(context), + params, + ); + } else if (redirectUrl.startsWith('route://')) { + const routeUrl = `/${redirectUrl.split('route://')[1]}`; + this.router.push(routeUrl); + } + return true; + } + + return false; + } +} diff --git a/src/panel-component/user-message/common/internal-message-default/internal-message-default.scss b/src/panel-component/user-message/common/internal-message-default/internal-message-default.scss new file mode 100644 index 0000000000000000000000000000000000000000..24b463113b6f4eadfc408ec00b078e08b8faa21a --- /dev/null +++ b/src/panel-component/user-message/common/internal-message-default/internal-message-default.scss @@ -0,0 +1,37 @@ +$internal-message: ( + loading-warp-bg-color: linear-gradient(90deg,getCssVar(color, primary, light, hover),getCssVar(color, primary)), +); + +@include b(internal-message) { + @include set-component-css-var('internal-message', $internal-message); + @include flex(row, space-between, center); + + @include e(caption){ + margin-bottom: getCssVar('spacing', 'tight'); + font-weight: getCssVar(font-weight, bold); + } + + @include e(short-content){ + margin-bottom: getCssVar('spacing', 'tight'); + } + + @include e(status){ + margin-right: getCssVar('spacing', 'tight'); + } + +} + +@include b(internal-message-left) { + flex-grow: 0; + flex-shrink: 0; + align-self: flex-start; + width: 50px; + font-size: getCssVar(font-size,header-3); + text-align: center; +} + + +@include b(internal-message-center) { + flex-grow: 1; + width: 342px; +} diff --git a/src/panel-component/user-message/common/internal-message-default/internal-message-default.tsx b/src/panel-component/user-message/common/internal-message-default/internal-message-default.tsx new file mode 100644 index 0000000000000000000000000000000000000000..89e1e59895e3737bc537025210ec505a1c1c609f --- /dev/null +++ b/src/panel-component/user-message/common/internal-message-default/internal-message-default.tsx @@ -0,0 +1,76 @@ +/* eslint-disable camelcase */ +import { defineComponent, PropType } from 'vue'; +import { IInternalMessage } from '@ibiz-template/core'; +import { useNamespace } from '@ibiz-template/vue3-util'; +import './internal-message-default.scss'; +import { IInternalMessageProvider } from '@ibiz-template/runtime'; + +const stateTexts = { + READ: '已阅读', + SENT: '已发送', + RECEIVED: '已接收', + REPLIED: '已回复', + SEND_FAILED: '发送失败', + NOT_SENT: '未发送', + DELETED: '已删除', +}; + +const stateType = { + READ: '', + SENT: 'success', + RECEIVED: 'success', + REPLIED: 'warning', + SEND_FAILED: 'danger', + NOT_SENT: 'info', + DELETED: 'info', +}; + +export const InternalMessageDefault = defineComponent({ + name: 'IBizInternalMessageDefault', + props: { + message: { + type: Object as PropType, + required: true, + }, + provider: { + type: Object as PropType, + required: true, + }, + }, + emits: { + close: () => true, + }, + setup() { + const ns = useNamespace('internal-message'); + return { ns }; + }, + render() { + const { title, create_time, content: msgContent, status } = this.message; + + // 内容区 + const content =
{msgContent}
; + + return ( + this.$emit('close')} + > +
+ +
+
+
+ + {stateTexts[status]} + + {title} +
+ {content} +
{create_time}
+
+
+ ); + }, +}); diff --git a/src/panel-component/user-message/index.ts b/src/panel-component/user-message/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..edfcbc033c51009f1bfb3dd99272db1ad3e7665e --- /dev/null +++ b/src/panel-component/user-message/index.ts @@ -0,0 +1,58 @@ +import { withInstall } from '@ibiz-template/vue3-util'; +import { App } from 'vue'; +import { + registerInternalMessageProvider, + registerPanelItemProvider, +} from '@ibiz-template/runtime'; +import { MobUserMessageProvider } from './user-message.provider'; +import { MobUserMessage } from './user-message'; +import { + InternalMessageContainer, + InternalMessageDefault, + InternalMessageDefaultProvider, +} from './common'; +import { InternalMessageJSON } from './internal-message-json/internal-message-json'; +import { InternalMessageHTML } from './internal-message-html/internal-message-html'; +import { InternalMessageText } from './internal-message-text/internal-message-text'; +import { InternalMessageJSONtProvider } from './internal-message-json/internal-message-json.provider'; +import { InternalMessageHTMLtProvider } from './internal-message-html/internal-message-html.provider'; +import { InternalMessageTextProvider } from './internal-message-text/internal-message-text.provider'; + +export const IBizMobUserMessage = withInstall( + MobUserMessage, + function (v: App) { + v.component(MobUserMessage.name, MobUserMessage); + registerPanelItemProvider( + 'RAWITEM_USERMESSAGE', + () => new MobUserMessageProvider(), + ); + v.component(InternalMessageContainer.name, InternalMessageContainer); + v.component(InternalMessageDefault.name, InternalMessageDefault); + v.component(InternalMessageJSON.name, InternalMessageJSON); + v.component(InternalMessageHTML.name, InternalMessageHTML); + v.component(InternalMessageText.name, InternalMessageText); + + // 注册站内信适配器 + registerInternalMessageProvider( + 'DEFAULT', + () => new InternalMessageDefaultProvider(), + ); + + registerInternalMessageProvider( + 'JSON', + () => new InternalMessageJSONtProvider(), + ); + + registerInternalMessageProvider( + 'HTML', + () => new InternalMessageHTMLtProvider(), + ); + + registerInternalMessageProvider( + 'TEXT', + () => new InternalMessageTextProvider(), + ); + }, +); + +export default IBizMobUserMessage; diff --git a/src/panel-component/user-message/internal-message-html/internal-message-html.provider.ts b/src/panel-component/user-message/internal-message-html/internal-message-html.provider.ts new file mode 100644 index 0000000000000000000000000000000000000000..624fe2601cc48bd63181d509787aa94b78b1d248 --- /dev/null +++ b/src/panel-component/user-message/internal-message-html/internal-message-html.provider.ts @@ -0,0 +1,25 @@ +import { IInternalMessage } from '@ibiz-template/core'; +import { InternalMessageHTML } from './internal-message-html'; +import { InternalMessageDefaultProvider } from '../common'; + +export class InternalMessageHTMLtProvider extends InternalMessageDefaultProvider { + component = InternalMessageHTML; + + async onClick( + message: IInternalMessage, + event: MouseEvent, + ): Promise { + const result = await super.onClick(message, event); + + // 没有url的时候看json里的redirecturl跳转 + if (!result && message.content_type === 'JSON' && message.content) { + const json = JSON.parse(message.content); + if (json.redirecturl) { + this.openRedirectView(message, json.redirecturl); + return true; + } + } + + return true; + } +} diff --git a/src/panel-component/user-message/internal-message-html/internal-message-html.scss b/src/panel-component/user-message/internal-message-html/internal-message-html.scss new file mode 100644 index 0000000000000000000000000000000000000000..a7dc46239486129395faea2388970816d6d33842 --- /dev/null +++ b/src/panel-component/user-message/internal-message-html/internal-message-html.scss @@ -0,0 +1,8 @@ +@include b(internal-message-html) { + @include flex(column); + + @include e(content){ + flex-grow: 1; + } + +} \ No newline at end of file diff --git a/src/panel-component/user-message/internal-message-html/internal-message-html.tsx b/src/panel-component/user-message/internal-message-html/internal-message-html.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5a61ff3308f4617c7d24fb00ef88e6434f1f60be --- /dev/null +++ b/src/panel-component/user-message/internal-message-html/internal-message-html.tsx @@ -0,0 +1,43 @@ +/* eslint-disable camelcase */ +import { computed, defineComponent, PropType } from 'vue'; +import { IInternalMessage } from '@ibiz-template/core'; +import { useNamespace } from '@ibiz-template/vue3-util'; +import { InternalMessageHTMLtProvider } from './internal-message-html.provider'; +import { parseHtml } from '../user-message.util'; +import './internal-message-html.scss'; + +export const InternalMessageHTML = defineComponent({ + name: 'IBizInternalMessageHTML', + props: { + message: { + type: Object as PropType, + required: true, + }, + provider: { + type: Object as PropType, + required: true, + }, + }, + emits: { + close: () => true, + }, + setup(props) { + const ns = useNamespace('internal-message-html'); + const msgContent = computed(() => { + return parseHtml(props.message.content); + }); + return { ns, msgContent }; + }, + render() { + return ( + this.$emit('close')} + > +
+
+ ); + }, +}); diff --git a/src/panel-component/user-message/internal-message-json/internal-message-json.provider.ts b/src/panel-component/user-message/internal-message-json/internal-message-json.provider.ts new file mode 100644 index 0000000000000000000000000000000000000000..dab37c07adc3230275d4e326b3828ac6c3f3fb2c --- /dev/null +++ b/src/panel-component/user-message/internal-message-json/internal-message-json.provider.ts @@ -0,0 +1,25 @@ +import { IInternalMessage } from '@ibiz-template/core'; +import { InternalMessageJSON } from './internal-message-json'; +import { InternalMessageDefaultProvider } from '../common'; + +export class InternalMessageJSONtProvider extends InternalMessageDefaultProvider { + component = InternalMessageJSON; + + async onClick( + message: IInternalMessage, + event: MouseEvent, + ): Promise { + const result = await super.onClick(message, event); + + // 没有url的时候看json里的redirecturl跳转 + if (!result && message.content_type === 'JSON' && message.content) { + const json = JSON.parse(message.content); + if (json.redirecturl) { + this.openRedirectView(message, json.redirecturl); + return true; + } + } + + return true; + } +} diff --git a/src/panel-component/user-message/internal-message-json/internal-message-json.scss b/src/panel-component/user-message/internal-message-json/internal-message-json.scss new file mode 100644 index 0000000000000000000000000000000000000000..09f9d1452321f1e5fb08890f83fc186c5701bed9 --- /dev/null +++ b/src/panel-component/user-message/internal-message-json/internal-message-json.scss @@ -0,0 +1,8 @@ +@include b(internal-message-json) { + @include flex(column); + + @include e(content){ + flex-grow: 1; + } + +} \ No newline at end of file diff --git a/src/panel-component/user-message/internal-message-json/internal-message-json.tsx b/src/panel-component/user-message/internal-message-json/internal-message-json.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d900f74e62edd33d5d08c2dc7916704b6df9d4b3 --- /dev/null +++ b/src/panel-component/user-message/internal-message-json/internal-message-json.tsx @@ -0,0 +1,97 @@ +/* eslint-disable camelcase */ +import { computed, defineComponent, PropType } from 'vue'; +import { IInternalMessage } from '@ibiz-template/core'; +import { useNamespace } from '@ibiz-template/vue3-util'; +import './internal-message-json.scss'; +import { InternalMessageJSONtProvider } from './internal-message-json.provider'; + +export const InternalMessageJSON = defineComponent({ + name: 'IBizInternalMessageJSON', + props: { + message: { + type: Object as PropType, + required: true, + }, + provider: { + type: Object as PropType, + required: true, + }, + }, + emits: { + close: () => true, + }, + setup(props, { emit }) { + const ns = useNamespace('internal-message-json'); + + const jsonContent = computed(() => { + if (props.message.content && props.message.content_type === 'JSON') { + return JSON.parse(props.message.content) as { + html?: string; + redirecturl?: string; + }; + } + return null; + }); + + // 没有短内容和长内容不一致时, 显示点击 + const redirectUrl = computed(() => { + const url = ibiz.env.isMob ? props.message.mobile_url : props.message.url; + return url || jsonContent.value?.redirecturl; + }); + + const toolbarItems = computed(() => { + if (!redirectUrl.value) { + return undefined; + } + return [ + { + icon: 'link-outline', + key: 'openRedirectView', + tooltip: ibiz.i18n.t( + 'panelComponent.userMessage.internalMessageJson.jumpToView', + ), + }, + ]; + }); + + const onToolbarClick = (key: string) => { + if (key === 'openRedirectView') { + props.provider.openRedirectView(props.message, redirectUrl.value!); + emit('close'); + } + }; + + return { ns, jsonContent, toolbarItems, redirectUrl, onToolbarClick }; + }, + render() { + // 内容区 + let content = null; + if (this.jsonContent?.html) { + content = ( +
+ ); + } else { + content = ( +
+ {ibiz.i18n.t( + 'panelComponent.userMessage.internalMessageJson.missingHtml', + )} +
+ ); + } + + return ( + this.$emit('close')} + > + {content} + + ); + }, +}); diff --git a/src/panel-component/user-message/internal-message-text/internal-message-text.provider.ts b/src/panel-component/user-message/internal-message-text/internal-message-text.provider.ts new file mode 100644 index 0000000000000000000000000000000000000000..76db547c8353d4a55a2d2c9ca0c325580f183c94 --- /dev/null +++ b/src/panel-component/user-message/internal-message-text/internal-message-text.provider.ts @@ -0,0 +1,6 @@ +import { InternalMessageText } from './internal-message-text'; +import { InternalMessageDefaultProvider } from '../common'; + +export class InternalMessageTextProvider extends InternalMessageDefaultProvider { + component = InternalMessageText; +} diff --git a/src/panel-component/user-message/internal-message-text/internal-message-text.scss b/src/panel-component/user-message/internal-message-text/internal-message-text.scss new file mode 100644 index 0000000000000000000000000000000000000000..5c040026b2967652b88f3ec2c923ef8543331019 --- /dev/null +++ b/src/panel-component/user-message/internal-message-text/internal-message-text.scss @@ -0,0 +1,17 @@ +@include b(internal-message-text) { + @include flex(column); + + @include e(caption){ + margin-bottom: getCssVar('spacing', 'tight'); + font-weight: getCssVar(font-weight, bold); + } + + @include e(content){ + margin-bottom: getCssVar('spacing', 'tight'); + } + + @include e(status){ + margin-right: getCssVar('spacing', 'tight'); + } + +} \ No newline at end of file diff --git a/src/panel-component/user-message/internal-message-text/internal-message-text.tsx b/src/panel-component/user-message/internal-message-text/internal-message-text.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0ee610a60a7885591455e8a469cd81869f0cba9a --- /dev/null +++ b/src/panel-component/user-message/internal-message-text/internal-message-text.tsx @@ -0,0 +1,42 @@ +/* eslint-disable camelcase */ +import { defineComponent, PropType } from 'vue'; +import { IInternalMessage } from '@ibiz-template/core'; +import { useNamespace } from '@ibiz-template/vue3-util'; +import './internal-message-text.scss'; +import { InternalMessageTextProvider } from './internal-message-text.provider'; + +export const InternalMessageText = defineComponent({ + name: 'IBizInternalMessageText', + props: { + message: { + type: Object as PropType, + required: true, + }, + provider: { + type: Object as PropType, + required: true, + }, + }, + emits: { + close: () => true, + }, + setup() { + const ns = useNamespace('internal-message-text'); + return { ns }; + }, + render() { + const { title, create_time, content: msgContent } = this.message; + return ( + this.$emit('close')} + > +
{title}
+ {!!msgContent &&
{msgContent}
} +
{create_time}
+
+ ); + }, +}); diff --git a/src/panel-component/user-message/user-message.controller.ts b/src/panel-component/user-message/user-message.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..3c9c21cc3065c4abe4d203c52e06be0c6e60edf8 --- /dev/null +++ b/src/panel-component/user-message/user-message.controller.ts @@ -0,0 +1,18 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { PanelItemController } from '@ibiz-template/runtime'; +import { IPanelField } from '@ibiz/model-core'; + +export class MobUserMessageController extends PanelItemController { + /** + * 初始化 + * + * @protected + * @return {*} {Promise} + * @memberof MobUserMessageController + */ + protected async onInit(): Promise { + await super.onInit(); + } +} diff --git a/src/panel-component/user-message/user-message.provider.ts b/src/panel-component/user-message/user-message.provider.ts new file mode 100644 index 0000000000000000000000000000000000000000..71a21e9a1f7c90325855f120f5fe625746d58e13 --- /dev/null +++ b/src/panel-component/user-message/user-message.provider.ts @@ -0,0 +1,22 @@ +import { + IPanelItemProvider, + PanelController, + PanelItemController, +} from '@ibiz-template/runtime'; + +import { IPanelItem } from '@ibiz/model-core'; +import { MobUserMessageController } from './user-message.controller'; + +export class MobUserMessageProvider implements IPanelItemProvider { + component: string = 'MobUserMessage'; + + async createController( + panelItem: IPanelItem, + panel: PanelController, + parent: PanelItemController | undefined, + ): Promise { + const c = new MobUserMessageController(panelItem, panel, parent); + await c.init(); + return c; + } +} diff --git a/src/panel-component/user-message/user-message.scss b/src/panel-component/user-message/user-message.scss new file mode 100644 index 0000000000000000000000000000000000000000..977512a7ab018b8d9425b77b95e74f640b3a685c --- /dev/null +++ b/src/panel-component/user-message/user-message.scss @@ -0,0 +1,56 @@ +@include b(user-message) { + position: relative; + height: 100%; + @include e(content) { + display: flex; + flex-direction: column; + height: 100%; + overflow: auto; + } + @include e(item) { + padding: 0.875rem; + + &::after { + position: relative; + bottom: -0.875rem; + left: 1rem; + display: block; + width: calc(100% - 2rem); + height: rem(1px); + content: ''; + background-color: getCssVar(color, border); + } + } + @include e(load-more) { + @include flex(row, center, center); + + height: rem(40px); + flex: none; + color: getCssVar(color, link); + cursor: pointer; + } + @include e(nodata) { + height: 100%; + @include flex(row, center, center); + } + @include e(icons-read) { + position: absolute; + right: 0; + top: 0; + z-index: 9; + height: rem(40px); + padding: 0 rem(12px); + } + .van-tabs { + height: 100%; + .van-tabs__content { + height: calc(100% - var(--van-tabs-line-height)); + >div { + height: 100%; + } + } + } + .van-tabs__wrap { + padding-right: rem(40px); + } +} diff --git a/src/panel-component/user-message/user-message.tsx b/src/panel-component/user-message/user-message.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f4c435bdf9587e586d81a986106ae1aef015399f --- /dev/null +++ b/src/panel-component/user-message/user-message.tsx @@ -0,0 +1,220 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { + defineComponent, + onActivated, + onDeactivated, + onMounted, + onUnmounted, + reactive, + ref, + watch, +} from 'vue'; +import { + getInternalMessageProvider, + IInternalMessageProvider, +} from '@ibiz-template/runtime'; +import { getRawProps, useNamespace } from '@ibiz-template/vue3-util'; +import { + clone, + IInternalMessage, + IPortalMessage, + showTitle, +} from '@ibiz-template/core'; +import { MobUserMessageController } from './user-message.controller'; +import './user-message.scss'; +import { InternalMessageDefaultProvider } from './common'; + +export const MobUserMessage = defineComponent({ + name: 'MobUserMessage', + props: getRawProps(), + setup() { + const ns = useNamespace('user-message'); + const noticeController = ibiz.hub.notice; + noticeController.internalMessage.ns = ns; + noticeController.internalMessage.provider = + new InternalMessageDefaultProvider(); + const c = noticeController.internalMessage; + + // 当前分页 + const activeTab = ref(''); + + const unreadOnlyTag = `${ibiz.appData?.context.srfsystemid}-unreadOnly`; + + // 是否显示消息列表 + const isActive = ref(true); + const hasNotice = ref(false); + ibiz.mc.command.internalMessage.on(async (msg: IPortalMessage) => { + ibiz.log.debug('mqtt internalMessage: ', msg); + if (msg.subtype === 'INTERNALMESSAGE') { + hasNotice.value = true; + } + }); + + // 激活且存在变更时重新加载 + watch( + () => isActive.value, + newVal => { + if (newVal && hasNotice.value) { + c.load(); + c.refreshUnreadCount(); + hasNotice.value = false; + } + }, + ); + + const allItems = ref([]); + const state = reactive({ + total: 0, + pageSize: 0, + unreadOnly: c.unreadOnly, + }); + + /** + * 从控制器里更新数据 + * @author lxm + * @date 2024-01-26 10:51:17 + */ + const updateData = () => { + allItems.value = clone(c.messages); + state.total = c.total; + state.pageSize = c.size; + }; + + // 第一次计算数据 + updateData(); + const updateUnreadOnlyChange = (val: boolean) => { + state.unreadOnly = val; + }; + + c.evt.on('dataChange', updateData); + c.evt.on('unreadOnlyChange', updateUnreadOnlyChange); + + onUnmounted(() => { + isActive.value = false; + c.evt.off('dataChange', updateData); + c.evt.off('unreadOnlyChange', updateUnreadOnlyChange); + }); + + const switchChange = (name: string) => { + const bol = name === 'unread'; + c.toggleUnReadOnly(bol); + localStorage.setItem(unreadOnlyTag, c.unreadOnly.toString()); + }; + + const initUnreadOnly = (): void => { + const unreadOnlyStr = localStorage.getItem(unreadOnlyTag); + if (unreadOnlyStr) { + if (unreadOnlyStr === 'true') { + state.unreadOnly = true; + c.unreadOnly = true; + } else { + state.unreadOnly = false; + c.unreadOnly = false; + } + } + }; + + const onBatchReadClick = () => { + c.batchMarkRead(); + }; + + const showMore = () => { + c.loadMore(); + }; + + onActivated(() => { + isActive.value = true; + }); + + onDeactivated(() => { + isActive.value = false; + }); + + onMounted(() => { + initUnreadOnly(); + isActive.value = true; + c.load(); + }); + return { + ns, + allItems, + state, + activeTab, + showMore, + switchChange, + onBatchReadClick, + }; + }, + render() { + const restLength = this.state.total - this.allItems.length; + const content = ( +
+ {this.allItems.length > 0 && + this.allItems.map(msg => { + let provider: IInternalMessageProvider | undefined; + try { + provider = getInternalMessageProvider(msg); + } catch (error) { + ibiz.log.error(error); + } + if (provider) { + return provider.render({ + class: [this.ns.e('item')], + message: msg, + }); + } + return ( +
+ {ibiz.i18n.t( + 'panelComponent.userMessage.internalMessageTab.noSupportType', + { type: msg.content_type }, + )} +
+ ); + })} + {this.allItems.length === 0 && ( +
+ {ibiz.i18n.t( + 'panelComponent.userMessage.internalMessageTab.notificationYet', + )} +
+ )} + {restLength > 0 && ( +
+ {ibiz.i18n.t( + 'panelComponent.userMessage.internalMessageTab.loadMore', + { length: restLength }, + )} +
+ )} +
+ ); + return ( +
+ + + + {content} + + + {content} + + +
+ ); + }, +}); diff --git a/src/panel-component/user-message/user-message.util.ts b/src/panel-component/user-message/user-message.util.ts new file mode 100644 index 0000000000000000000000000000000000000000..a919c080ca65dcde280f98866ab4b13609d5bee9 --- /dev/null +++ b/src/panel-component/user-message/user-message.util.ts @@ -0,0 +1,28 @@ +/* eslint-disable no-cond-assign */ +/** + * 解析html内容 + * @description 解析html内容, 用法传入字符串 返回值为string类型 + * ``` + * parseHtml(`JUYwJTlGJTk4JTg0` => `😄` + * 正则中class=['"]emoji['"]适配单双引号的情况 + * ``` + * @export + * @param {string} str + * @return {*} {string} + */ +export function parseHtml(str: string): string { + // 表情解析 + const regex = + /(.+?)<\/span>/g; + let match; + let result = str; + while ((match = regex.exec(str)) !== null) { + const emoji = match[1]; + const tempVal = decodeURIComponent(atob(emoji)); + result = result.replace( + match[0], + `${tempVal}`, + ); + } + return result; +}