import * as openpgp from 'openpgp_hi/lightweight'; import {initLanguage, languageList, languageName} from "../language"; import {$callData, $urlSafe, SSEClient} from '../utils' import {isLocalHost} from "../components/Replace/utils"; import emitter from "./events"; import axios from "axios"; const dialogDraftState = { timer: {}, subTemp: null } export default { /** * 预加载 * @param state */ preload({state}) { window.addEventListener('resize', () => { const windowWidth = $A(window).width(), windowHeight = $A(window).height(), windowOrientation = $A.screenOrientation() state.windowTouch = "ontouchend" in document state.windowWidth = windowWidth state.windowHeight = windowHeight state.windowOrientation = windowOrientation state.windowLandscape = windowOrientation === 'landscape' state.windowPortrait = windowOrientation === 'portrait' state.windowIsFullScreen = $A.isFullScreen() state.formOptions = { class: windowWidth > 576 ? '' : 'form-label-weight-bold', labelPosition: windowWidth > 576 ? 'right' : 'top', labelWidth: windowWidth > 576 ? 'auto' : '', } $A.eeuiAppSendMessage({ action: 'windowSize', width: windowWidth, height: windowHeight, }); }) window.addEventListener('scroll', () => { state.windowScrollY = window.scrollY }) window.addEventListener('message', ({data}) => { data = $A.jsonParse(data); if (data.action === 'eeuiAppSendMessage') { const items = $A.isArray(data.data) ? data.data : [data.data]; items.forEach(item => { $A.eeuiAppSendMessage(item); }) } }) window.addEventListener('fullscreenchange', () => { if (document.fullscreenElement) { $A("body").addClass("fullscreen-mode") } else { $A("body").removeClass("fullscreen-mode") } }); window.visualViewport?.addEventListener('resize', () => { state.viewportHeight = window.visualViewport.height || 0; }); }, /** * 初始化 * @param state * @param dispatch * @returns {Promise} */ init({state, dispatch}) { return new Promise(async resolve => { // 语言、主题、用户信息 const urlParams = $A.urlParameterAll() const paramMap = { language: '__system:languageName__', theme: '__system:themeConf__', userid: '__system:userId__', token: '__system:userToken__' }; const paramUser = { userid: 0, token: null } Object.entries(paramMap).forEach(([param, key]) => { if (urlParams[param]) { window.localStorage.setItem(key, urlParams[param]); param === 'userid' && (paramUser.userid = $A.runNum(urlParams[param])) param === 'token' && (paramUser.token = urlParams[param]) } }); if (Object.keys(paramMap).some(param => urlParams[param])) { const newUrl = $A.removeURLParameter(window.location.href, Object.keys(paramMap)); window.history.replaceState(null, '', newUrl); } // 处理用户身份信息 if (paramUser.userid > 0 && paramUser.token) { const userInfo = await $A.IDBJson('userInfo') await $A.IDBSet("userInfo", Object.assign(userInfo, paramUser)); } // 清理缓存、读取缓存 let action = null const clearCache = await $A.IDBString("clearCache") if (clearCache) { if (clearCache === "handle") { action = "handleClearCache" } await $A.IDBRemove("clearCache") await $A.IDBSet("cacheVersion", "clear") } const cacheVersion = await $A.IDBString("cacheVersion") if (cacheVersion && cacheVersion !== state.cacheVersion) { await dispatch("handleClearCache") } else { await dispatch("handleReadCache") } // 主题皮肤 await dispatch("synchTheme") // Keyboard await dispatch("handleKeyboard") // 客户端ID if (!state.clientId) { state.clientId = $A.randomString(6) await $A.IDBSet("clientId", state.clientId) } // 获取apiKey dispatch("call", { url: "users/key/client", data: {client_id: state.clientId}, encrypt: false, }).then(({data}) => { state.apiKeyData = data; }) // 获取系统设置 dispatch("systemSetting") // 载入静态资源 await $A.loadScriptS([ // 基础包 'js/jsencrypt.min.js', 'js/scroll-into-view.min.js', // 加载语言包 `language/web/key.js`, `language/web/${languageName}.js`, `language/iview/${languageName}.js`, ]) // 初始化语言 initLanguage() resolve(action) }) }, /** * 获取安全区域 * @param state * @returns {Promise} */ safeAreaInsets({state}) { return new Promise(resolve => { if (!state.isFirstPage) { return resolve(null) } $A.eeuiAppGetSafeAreaInsets().then(data => { data.top = data.top || state.safeAreaSize?.data?.top || 0 data.bottom = data.bottom || state.safeAreaSize?.data?.bottom || 0 const proportion = data.height / window.outerHeight state.safeAreaSize = { top: Math.round(data.top / proportion * 100) / 100, bottom: Math.round(data.bottom / proportion * 100) / 100, data } resolve(state.safeAreaSize) }).catch(e => { console.warn(e) resolve(null) }) }) }, /** * 访问接口 * @param state * @param dispatch * @param params // {url,data,method,timeout,header,spinner,websocket,encrypt, before,complete,success,error,after} * @returns {Promise} */ call({state, dispatch}, params) { if (!$A.isJson(params)) params = {url: params} const header = { 'Content-Type': 'application/json', 'language': languageName, 'token': state.userToken, 'fd': $A.getSessionStorageString("userWsFd"), 'version': window.systemInfo.version || "0.0.1", 'platform': $A.Platform, } if (!state.userToken && state.meetingWindow?.meetingSharekey) { header.sharekey = state.meetingWindow.meetingSharekey; } if ($A.isJson(params.header)) { params.header = Object.assign(header, params.header) } else { params.header = header } if (state.systemConfig.e2e_message === 'open' && params.encrypt === undefined && $A.inArray(params.url, [ 'users/login', 'users/editpass', 'users/operation', 'users/delete/account', 'system/license', 'users/bot/*', 'dialog/msg/*', ], true)) { params.encrypt = true } if (params.encrypt) { const userAgent = window.navigator.userAgent; if (window.systemInfo.debug === "yes" || /Windows NT 5.1|Windows XP/.test(userAgent) || userAgent.indexOf("Windows NT 6.0") !== -1 || userAgent.indexOf("Windows NT 6.1") !== -1 || userAgent.indexOf("Windows NT 6.2") !== -1) { params.encrypt = false // 是 Windows Xp, Vista, 7, 8 系统,不支持加密 } } params.url = $A.apiUrl(params.url) params.data = $A.newDateString(params.data) // const cloneParams = $A.cloneJSON(params) return new Promise(async (resolve, reject) => { // 判断服务器地址 if (/^https?:\/\/public\//.test(params.url)) { reject({ret: -1, data: {}, msg: "No server address"}) return } // 加密传输 const encrypt = [] if (params.encrypt === true) { if (params.data) { // 有数据才加密 if (state.apiKeyData.type === 'pgp') { // PGP加密 encrypt.push(`encrypt_type=${state.apiKeyData.type};encrypt_id=${state.apiKeyData.id}`) params.method = "post" // 加密传输时强制使用post params.data = {encrypted: await dispatch("pgpEncryptApi", params.data)} } } encrypt.push("client_type=pgp;client_key=" + (await dispatch("pgpGetLocalKey")).publicKeyB64) } if (encrypt.length > 0) { params.header.encrypt = encrypt.join(";") } // 数据转换 if (params.method === "post") { params.data = JSON.stringify(params.data) } // 等待效果(Spinner) if (params.spinner === true || (typeof params.spinner === "number" && params.spinner > 0)) { const {before, complete} = params params.before = () => { dispatch("showSpinner", typeof params.spinner === "number" ? params.spinner : 0) typeof before === "function" && before() } params.complete = () => { dispatch("hiddenSpinner") typeof complete === "function" && complete() } } // 请求成功 params.success = async (result, status, xhr) => { // 数据校验 if (!$A.isJson(result)) { console.log(result, status, xhr) reject({ret: -1, data: {}, msg: $A.L('返回参数错误')}) return } // 数据解密 if (params.encrypt === true && result.encrypted) { result = await dispatch("pgpDecryptApi", result.encrypted) } const {ret, data, msg} = result // 身份判断(身份丢失) if (ret === -1) { state.userId = 0 if (params.checkAuth !== false) { state.ajaxAuthException = msg || $A.L('请登录后继续...') reject(Object.assign(result, {msg: false})) return } } // 身份判断(需要昵称) if (ret === -2 && params.checkNick !== false) { dispatch("userEditInput", 'nickname').then(() => { dispatch("call", cloneParams).then(resolve).catch(reject) }).catch(err => { reject({ret: -1, data, msg: err || $A.L('请设置昵称!')}) }) return } // 身份判断(需要联系电话) if (ret === -3 && params.checkTel !== false) { dispatch("userEditInput", 'tel').then(() => { dispatch("call", cloneParams).then(resolve).catch(reject) }).catch(err => { reject({ret: -1, data, msg: err || $A.L('请设置联系电话!')}) }) return } // 返回数据 if (ret === 1) { resolve({data, msg, xhr}) return } // 错误处理 reject({ret, data, msg: msg || $A.L('未知错误')}) if (ret === -4001) { dispatch("forgetProject", {id: data.project_id}) } else if (ret === -4002) { data.force === 1 && (state.taskArchiveView = 0) dispatch("forgetTask", {id: data.task_id}) } else if (ret === -4003) { dispatch("forgetDialog", {id: data.dialog_id}) } else if (ret === -4004) { dispatch("getTaskForParent", data.task_id).catch(() => {}) } } // 请求失败 params.error = async (xhr, status) => { const reason = {ret: -1, data: {}, msg: $A.L('请求失败,请稍后重试。')} const isNetworkException = window.navigator.onLine === false || (status === 0 && xhr.readyState === 4) // 网络异常 if (isNetworkException) { // 重试一次 if (cloneParams.method !== "post" && cloneParams.networkFailureRetry !== false) { await new Promise(resolve => setTimeout(resolve, 1000)); dispatch("call", Object.assign(cloneParams, {networkFailureRetry: false})).then(resolve).catch(reject) return } // 异常提示 reason.ret = -1001 reason.msg = params.checkNetwork !== false ? false : $A.L('网络异常,请稍后重试。') if (params.checkNetwork !== false && $A.Ready !== false) { state.ajaxNetworkException = $A.L("网络连接失败,请检查网络设置。") } } // 异常处理 reject(reason) console.error(xhr, status); } // 发起请求 $A.ajaxc(params) }) }, /** * 取消请求 * @param state * @param requestId * @returns {Promise} */ callCancel({state}, requestId) { return new Promise((resolve, reject) => { if ($A.ajaxcCancel(requestId)) { resolve() } else { reject() } }) }, /** * 获取系统设置 * @param dispatch * @param state * @returns {Promise} */ systemSetting({dispatch, state}) { return new Promise((resolve, reject) => { switch (state.systemConfig.__state) { case "success": resolve(state.systemConfig) break case "loading": setTimeout(_ => { dispatch("systemSetting").then(resolve).catch(reject) }, 100) break default: state.systemConfig.__state = "loading" dispatch("call", { url: "system/setting", }).then(({data}) => { state.systemConfig = Object.assign(data, { timezoneDifference: $A.updateTimezone(data.server_timezone), __state: "success", }) resolve(state.systemConfig) }).catch(_ => { state.systemConfig.__state = "error" reject() }); break } }) }, /** * 下载文件 * @param state * @param data */ downUrl({state}, data) { if (!data) { return } let url = data; let addToken = true if ($A.isJson(data)) { url = data.url addToken = !!data.token } if (addToken) { let params = { token: state.userToken }; if ($A.isJson(data)) { url = data.url; params = data.params || {}; } url = $A.urlAddParams(url, params); } if ($A.Electron) { $A.Electron.request({ action: 'openDownloadWindow', language: languageName, theme: state.themeName, }); $A.Electron.request({ action: 'createDownload', url }); } else if ($A.isEEUIApp) { $A.eeuiAppOpenWeb(url); } else { window.open(url) } }, /** * 显示文件(打开文件所在位置) * @param state * @param getters * @param dispatch * @param data */ filePos({state, getters, dispatch}, data) { if ($A.isSubElectron) { $A.syncDispatch("filePos", data) $A.Electron.sendMessage('mainWindowActive'); return } dispatch('openTask', 0) if (!getters.isMessengerPage || state.windowPortrait) { // 如果 当前不是消息页面 或 是竖屏 则关闭对话窗口 dispatch("openDialog", 0); } $A.goForward({name: 'manage-file', params: data}); }, /** * 切换面板变量 * @param commit * @param state * @param data * @param data|{key, project_id} */ toggleProjectParameter({commit, state}, data) { $A.syncDispatch("toggleProjectParameter", data) // let key = data; let value = null; let project_id = state.projectId; if ($A.isJson(data)) { key = data.key; value = data.value; project_id = data.project_id; } if (project_id) { let index = state.cacheProjectParameter.findIndex(item => item.project_id == project_id) if (index === -1) { commit("project/parameter/push", $A.projectParameterTemplate(project_id)) index = state.cacheProjectParameter.findIndex(item => item.project_id == project_id) } const cache = state.cacheProjectParameter[index]; if (!$A.isJson(key)) { key = {[key]: value || !cache[key]}; } commit("project/parameter/splice", {index, data: Object.assign(cache, key)}) } }, /** * 设置主题 * @param state * @param dispatch * @param mode * @returns {Promise} */ setTheme({state, dispatch}, mode) { return new Promise(function (resolve) { if (mode === undefined) { resolve(false) return; } if (!$A.dark.utils.supportMode()) { if ($A.isEEUIApp) { $A.modalWarning("仅Android设置支持主题功能"); } else { $A.modalWarning("仅客户端或Chrome浏览器支持主题功能"); } resolve(false) return; } dispatch("synchTheme", mode) resolve(true) }); }, /** * 同步主题 * @param state * @param dispatch * @param mode */ synchTheme({state, dispatch}, mode = undefined) { if (typeof mode === "undefined") { mode = state.themeConf } else { state.themeConf = mode } switch (mode) { case 'dark': $A.dark.enableDarkMode() break; case 'light': $A.dark.disableDarkMode() break; default: state.themeConf = "auto" $A.dark.autoDarkMode() break; } state.themeName = $A.dark.isDarkEnabled() ? 'dark' : 'light' window.localStorage.setItem("__system:themeConf__", state.themeConf) // if ($A.isEEUIApp) { $A.eeuiAppSendMessage({ action: 'updateTheme', themeName: state.themeName, themeDefault: { theme: { dark: '#131313', light: '#f8f8f8' }, nav: { dark: '#cdcdcd', light: '#232323' } } }); } else if ($A.isElectron) { $A.Electron.sendMessage('setStore', { key: 'themeConf', value: state.themeConf }); } }, /** * 获取基本数据(项目、对话、仪表盘任务、会员基本信息) * @param state * @param dispatch * @param timeout */ getBasicData({state, dispatch}, timeout) { if (typeof timeout === "number") { window.__getBasicDataTimer && clearTimeout(window.__getBasicDataTimer) if (timeout > -1) { window.__getBasicDataTimer = setTimeout(_ => dispatch("getBasicData", null), timeout) } return } // const tmpKey = state.userId + $A.dayjs().unix() if (window.__getBasicDataKey === tmpKey) { return } window.__getBasicDataKey = tmpKey // dispatch("getDialogAuto").catch(() => {}); dispatch("getDialogTodo", 0).catch(() => {}); dispatch("getTaskPriority", 1000); dispatch("getReportUnread", 1000); dispatch("getApproveUnread", 1000); dispatch("getProjectByQueue"); dispatch("getTaskForDashboard"); dispatch("dialogMsgRead"); dispatch("updateMicroAppsStatus"); // const allIds = Object.values(state.userAvatar).map(({userid}) => userid); [...new Set(allIds)].some(userid => dispatch("getUserBasic", {userid})) }, /** * 获取未读工作报告数量 * @param state * @param dispatch * @param timeout */ getReportUnread({state, dispatch}, timeout) { window.__getReportUnread && clearTimeout(window.__getReportUnread) window.__getReportUnread = setTimeout(() => { if (state.userId === 0) { state.reportUnreadNumber = 0; } else { dispatch("call", { url: 'report/unread', }).then(({data}) => { state.reportUnreadNumber = data.total || 0; }).catch(_ => {}); } }, typeof timeout === "number" ? timeout : 1000) }, /** * 获取审批待办未读数量 * @param state * @param dispatch * @param timeout */ getApproveUnread({state, dispatch}, timeout) { window.__getApproveUnread && clearTimeout(window.__getApproveUnread) window.__getApproveUnread = setTimeout(() => { if (state.userId === 0) { state.approveUnreadNumber = 0; } else { dispatch("call", { url: 'approve/process/doto' }).then(({data}) => { state.approveUnreadNumber = data.total || 0; }).catch(({msg}) => { if( msg.indexOf("404 not found") !== -1){ $A.modalInfo({ title: '版本过低', content: '服务器版本过低,请升级服务器。', }) } }); } }, typeof timeout === "number" ? timeout : 1000) }, /** * 获取/更新会员信息 * @param dispatch * @returns {Promise} */ getUserInfo({dispatch}) { return new Promise(function (resolve, reject) { dispatch("call", { url: 'users/info', }).then(result => { dispatch("saveUserInfo", result.data); resolve(result) }).catch(e => { console.warn(e); reject(e) }); }); }, /** * 更新会员信息 * @param state * @param dispatch * @param info * @returns {Promise} */ saveUserInfoBase({state, dispatch}, info) { return new Promise(async resolve => { const userInfo = $A.cloneJSON(info); userInfo.userid = $A.runNum(userInfo.userid); userInfo.token = userInfo.userid > 0 ? (userInfo.token || state.userToken) : ''; state.userInfo = userInfo; state.userId = userInfo.userid; state.userToken = userInfo.token; state.userIsAdmin = $A.inArray('admin', userInfo.identity); if ($A.isSubElectron || ($A.isEEUIApp && !state.isFirstPage)) { // 子窗口(Electron)、不是第一个页面(App) 不保存 } else { await $A.IDBSet("userInfo", state.userInfo); } // $A.eeuiAppSendMessage({ action: 'userChatList', language: $A.eeuiAppConvertLanguage(), url: $A.mainUrl('api/users/share/list') + `?token=${state.userToken}` }); $A.eeuiAppSendMessage({ action:"userUploadUrl", dirUrl: $A.mainUrl('api/file/content/upload') + `?token=${state.userToken}`, chatUrl: $A.mainUrl('api/dialog/msg/sendfiles') + `?token=${state.userToken}`, }); // resolve() }) }, /** * 更新会员信息 * @param commit * @param state * @param dispatch * @param info * @returns {Promise} */ saveUserInfo({commit, state, dispatch}, info) { return new Promise(async resolve => { await dispatch("saveUserInfoBase", info); // dispatch("getBasicData", null); if (state.userId > 0) { commit("user/save", state.cacheUserBasic.filter(({userid}) => userid !== state.userId)) dispatch("saveUserBasic", state.userInfo); } resolve() }); }, /** * 获取用户基础信息 * @param state * @param dispatch * @param data {userid} */ getUserBasic({state, dispatch}, data) { if (state.loadUserBasic === true) { data && state.cacheUserWait.push(data); return; } // let time = $A.dayjs().unix(); let list = $A.cloneJSON(state.cacheUserWait); if (data && data.userid) { list.push(data) } state.cacheUserWait = []; // let array = []; let timeout = 0; list.some((item) => { let temp = state.cacheUserBasic.find(({userid}) => userid == item.userid); if (temp && time - temp._time <= 30) { setTimeout(() => { emitter.emit('userActive', {type: 'cache', data: temp}); }, timeout += 5); return false; } array.push(item); }); if (array.length === 0) { return; } else if (array.length > 30) { state.cacheUserWait = array.slice(30) array = array.slice(0, 30) } // state.loadUserBasic = true; dispatch("call", { url: 'users/basic', data: { userid: [...new Set(array.map(({userid}) => userid))] }, checkAuth: false }).then(result => { time = $A.dayjs().unix(); array.forEach(value => { let data = result.data.find(({userid}) => userid == value.userid) || Object.assign(value, {email: ""}); data._time = time; dispatch("saveUserBasic", data); }); state.loadUserBasic = false; dispatch("getUserBasic"); }).catch(e => { console.warn(e); state.loadUserBasic = false; dispatch("getUserBasic"); }); }, /** * 获取用户基础信息(缓存没有则请求网络) * @param state * @param dispatch * @param userid * @returns {Promise} */ getUserData({state, dispatch}, userid) { return new Promise(async (resolve, reject) => { let tempUser = state.cacheUserBasic.find(item => item.userid == userid); if (!tempUser) { try { const {data} = await dispatch("call", { url: 'users/basic', data: { userid: [userid] }, checkAuth: false }); tempUser = data.find(item => item.userid == userid); } catch (_) {} } if (tempUser) { resolve(tempUser); } else { reject(); } }) }, /** * 保存用户基础信息 * @param commit * @param state * @param data */ saveUserBasic({commit, state}, data) { $A.syncDispatch("saveUserBasic", data) // const index = state.cacheUserBasic.findIndex(({userid}) => userid == data.userid); if (index > -1) { data = Object.assign({}, state.cacheUserBasic[index], data) commit("user/splice", {index, data}) } else { commit("user/push", data) } emitter.emit('userActive', {type: 'cache', data}); }, /** * 修改机器人信息 * @param dispatch * @param data * @returns {Promise} */ editUserBot({dispatch}, data) { return new Promise((resolve, reject) => { let dialogId = 0 if (data.dialog_id) { dialogId = data.dialog_id; delete data.dialog_id; } dispatch("call", { url: 'users/bot/edit', data, method: 'post' }).then(({data, msg}) => { dispatch("saveUserBasic", { userid: data.id, nickname: data.name, userimg: data.avatar, }); if (dialogId) { dispatch("saveDialog", { id: dialogId, name: data.name }); } resolve({data, msg}) }).catch(reject); }) }, /** * 设置用户信息 * @param dispatch * @param type * @returns {Promise} */ userEditInput({dispatch}, type) { return new Promise(function (userResolve, userReject) { let desc = ''; if (type === 'nickname') { desc = '昵称'; } else if (type === 'tel') { desc = '联系电话'; } else { userReject('参数错误') return } setTimeout(_ => { $A.modalInput({ title: `设置${desc}`, placeholder: `请输入您的${desc}`, okText: "保存", onOk: (value) => { if (!value) { return `请输入${desc}` } return new Promise((inResolve, inReject) => { dispatch("call", { url: 'users/editdata', data: { [type]: value, }, checkNick: false, checkTel: false, }).then(() => { dispatch('getUserInfo').finally(_ => { inResolve() userResolve() }); }).catch(({msg}) => { inReject(msg) }); }) }, onCancel: _ => userReject }); }, 100) }); }, /** * 获取部门列表 * @param dispatch * @returns {Promise} */ getDepartmentList({dispatch}) { return new Promise((resolve, reject) => { const generateList = (data, parent_id = 0, level = 0, chains = []) => { let result = []; data.some(item => { if (item.parent_id == parent_id) { const newItem = Object.assign({}, item, { chains: chains.concat([item.name]), level: level + 1 }); result.push(newItem); // 递归获取子部门 const children = generateList(data, item.id, level + 1, chains.concat([item.name])); result = result.concat(children); } }); return result; }; dispatch("call", { url: 'users/department/list', }).then(({data}) => { resolve(generateList(data, 0, 1)); }).catch(reject); }); }, /** * 登出(打开登录页面) * @param state * @param dispatch * @param appendFrom * @returns {Promise} */ logout({state, dispatch}, appendFrom = true) { return new Promise(async resolve => { try { await dispatch("call", { url: "users/logout", timeout: 6000 }) } catch (e) { console.log(e); } dispatch("handleClearCache", {}).then(() => { let from = ["/", "/login"].includes(window.location.pathname) ? "" : encodeURIComponent(window.location.href); if (appendFrom === false) { from = null; } $A.goForward({name: 'login', query: from ? {from: from} : {}}, true); resolve(); }); }) }, /** * 处理快捷键配置 * @param state * @param newData * @returns {Promise} */ handleKeyboard({state}, newData) { return new Promise(resolve => { if (!window.localStorage.getItem("__system:keyboardConf__")) { window.localStorage.setItem("__system:keyboardConf__", window.localStorage.getItem("__keyboard:data__")) window.localStorage.removeItem("__keyboard:data__") } const data = $A.isJson(newData) ? newData : ($A.jsonParse(window.localStorage.getItem("__system:keyboardConf__")) || {}) data.screenshot_key = (data.screenshot_key || "").trim().toLowerCase() data.send_button_app = data.send_button_app || 'enter' // button, enter 移动端发送按钮,默认 enter (键盘回车发送) data.send_button_desktop = data.send_button_desktop || 'enter' // button, enter 桌面端发送按钮,默认 enter (键盘回车发送) window.localStorage.setItem("__system:keyboardConf__", $A.jsonStringify(data)) state.cacheKeyboard = data resolve(data) }) }, /** * 清除缓存 * @param state * @param dispatch * @param userData * @returns {Promise} */ handleClearCache({state, dispatch}, userData) { return new Promise(async resolve => { // localStorage const keys = ['themeConf', 'languageName', 'keyboardConf']; const savedData = keys.reduce((acc, key) => ({ ...acc, [key]: window.localStorage.getItem(`__system:${key}__`) }), {}); window.localStorage.clear(); keys.forEach(key => window.localStorage.setItem(`__system:${key}__`, savedData[key]) ); // localForage const cacheItems = { clientId: await $A.IDBString("clientId"), cacheServerUrl: await $A.IDBString("cacheServerUrl"), cacheCalendarView: await $A.IDBString("cacheCalendarView"), cacheProjectParameter: await $A.IDBArray("cacheProjectParameter"), cacheLoginEmail: await $A.IDBString("cacheLoginEmail"), cacheFileSort: await $A.IDBJson("cacheFileSort"), cacheTranslationLanguage: await $A.IDBString("cacheTranslationLanguage"), cacheTranscriptionLanguage: await $A.IDBString("cacheTranscriptionLanguage"), cacheTranslations: await $A.IDBArray("cacheTranslations"), cacheEmojis: await $A.IDBArray("cacheEmojis"), userInfo: await $A.IDBJson("userInfo"), cacheVersion: state.cacheVersion, }; await $A.IDBClear(); await Promise.all( Object.entries(cacheItems).map(([key, value]) => $A.IDBSet(key, value) ) ); // userInfo await dispatch("saveUserInfoBase", $A.isJson(userData) ? userData : cacheItems.userInfo) // readCache await dispatch("handleReadCache") // Reset auth exception flag after successful login flow state.ajaxAuthException = null resolve() }); }, /** * 读取缓存 * @param state * @param dispatch * @returns {Promise} */ handleReadCache({state}) { return new Promise(async resolve => { // 定义需要获取的数据映射 const dataMap = { string: [ 'clientId', 'cacheServerUrl', 'cacheCalendarView', 'cacheTranslationLanguage', 'cacheTranscriptionLanguage' ], array: [ 'cacheUserBasic', 'cacheProjects', 'cacheColumns', 'cacheTasks', 'cacheProjectParameter', 'cacheTranslations', 'dialogMsgs', 'dialogDrafts', 'dialogQuotes', 'fileLists', 'callAt', 'cacheEmojis', 'cacheDialogs', 'microAppsIds', 'microAppsMenus', ], json: [ 'userInfo' ] }; // 批量获取数据 const data = await Promise.all([ ...dataMap.string.map(key => $A.IDBString(key)), ...dataMap.array.map(key => $A.IDBArray(key)), ...dataMap.json.map(key => $A.IDBJson(key)) ]); // 更新 state [...dataMap.string, ...dataMap.array, ...dataMap.json].forEach((key, index) => { state[key] = data[index]; }); // 特殊处理 cacheDialogs state.cacheDialogs = state.cacheDialogs.map(item => ({ ...item, loading: false, })); // 特殊处理 dialogDrafts state.dialogDrafts = state.dialogDrafts.filter(item => !!item.content).map(item => ({ ...item, tag: !!item.content, })); // TranslationLanguage 检查 if (typeof languageList[state.cacheTranslationLanguage] === "undefined") { state.cacheTranslationLanguage = languageName; } // TranscriptionLanguage 检查 if (typeof languageList[state.cacheTranscriptionLanguage] === "undefined") { state.cacheTranscriptionLanguage = ''; } // 处理用户信息 if (state.userInfo.userid) { state.userId = state.userInfo.userid = $A.runNum(state.userInfo.userid); state.userToken = state.userInfo.token; state.userIsAdmin = $A.inArray("admin", state.userInfo.identity); } // 处理 ServerUrl if (state.cacheServerUrl) { window.systemInfo.apiUrl = state.cacheServerUrl } resolve(); }) }, /** * Electron 页面卸载触发 */ onBeforeUnload() { if ($A.isSubElectron && dialogDraftState.subTemp) { $A.syncDispatch("saveDialogDraft", dialogDraftState.subTemp) dialogDraftState.subTemp = null; } }, /** * 滚动到底部(将 el 底部对齐到网页底部) * @param state * @param el */ scrollBottom({state}, el) { if (!el) { return } const rect = el.getBoundingClientRect(); if (!rect) { return; } window.scrollTo({ top: rect.bottom + state.safeAreaSize.bottom, behavior: 'smooth' }); }, /** *****************************************************************************************/ /** *************************************** 新窗口打开 ****************************************/ /** *****************************************************************************************/ /** * 链接添加用户身份 * @param state * @param url * @returns {Promise} */ userUrl({state}, url) { return new Promise(resolve => { // 如果是访问:服务器域名 且 当前是本地文件,则将服务器域名替换成本地路径 if ($A.getDomain(url) == $A.getDomain($A.mainUrl()) && isLocalHost(window.location)) { try { const remoteURL = new URL(url) if (/^\/(single|meeting)\//.test(remoteURL.pathname)) { // 判断将服务器域名替换成本地路径 const localURL = new URL(window.location) localURL.hash = remoteURL.pathname + remoteURL.search return resolve(localURL.toString()) } } catch (e) { // 解析失败则不做任何处理 } } // 基本参数 const params = { language: languageName, theme: state.themeConf, userid: state.userId, } // 如果是访问:服务器域名 或 本地文件,则添加 token 参数 if ($A.getDomain(url) == $A.getDomain($A.mainUrl()) || isLocalHost(url)) { params.token = state.userToken } resolve($A.urlAddParams(url, params)) }) }, /** * 打开地图选位置(App) * @param dispatch * @param objects {{type: string, key: string, point: string, radius: number}} * @returns {Promise} */ openAppMapPage({dispatch}, objects) { return new Promise(resolve => { const title = $A.L("定位签到") const channel = $A.randomString(6) const params = { title, label: $A.L("选择附近地点"), placeholder: $A.L("搜索地点"), noresult: $A.L("附近没有找到地点"), errtip: $A.L("定位失败"), selectclose: "true", channel, } $A.eeuiAppSetVariate(`location::${channel}`, ""); const url = $A.urlAddParams(window.location.origin + '/tools/map/index.html', Object.assign(params, objects || {})) dispatch('openAppChildPage', { pageType: 'app', pageTitle: title, url: 'web.js', params: { titleFixed: true, hiddenDone: true, url }, callback: ({status}) => { if (status === 'pause') { const data = $A.jsonParse($A.eeuiAppGetVariate(`location::${channel}`)); if (data.point) { $A.eeuiAppSetVariate(`location::${channel}`, ""); if (data.distance > objects.radius) { $A.modalError(`你选择的位置「${data.title}」不在签到范围内`) return } resolve(data); } } } }) }) }, /** * 打开子窗口(App) * @param dispatch * @param objects */ async openAppChildPage({dispatch}, objects) { objects.params.url = await dispatch("userUrl", objects.params.url) if (typeof objects.params.allowAccess === "undefined") { // 如果是本地文件,则允许跨域 objects.params.allowAccess = isLocalHost(objects.params.url) } if (typeof objects.params.showProgress === "undefined") { // 如果不是本地文件,则显示进度条 objects.params.showProgress = !isLocalHost(objects.params.url) } $A.eeuiAppOpenPage(objects) }, /** * 打开子窗口(客户端) * @param dispatch * @param params */ async openChildWindow({dispatch}, params) { params.path = await dispatch("userUrl", params.path) $A.Electron.sendMessage('openChildWindow', params) }, /** * 打开新标签窗口(客户端) * @param dispatch * @param url */ async openWebTabWindow({dispatch}, url) { const params = {url} if ($A.getDomain(url) == $A.getDomain($A.mainUrl())) { params.url = await dispatch("userUrl", url) } else { params.webPreferences = {contextIsolation: false} } $A.Electron.sendMessage('openWebTabWindow', params) }, /** *****************************************************************************************/ /** ************************************** 文件 **********************************************/ /** *****************************************************************************************/ /** * 保存文件数据 * @param commit * @param state * @param dispatch * @param data */ saveFile({commit, state, dispatch}, data) { $A.syncDispatch("saveFile", data) // if ($A.isArray(data)) { data.forEach((file) => { dispatch("saveFile", file); }); } else if ($A.isJson(data)) { let base = {_load: false, _edit: false}; const index = state.fileLists.findIndex(({id}) => id == data.id); if (index > -1) { commit("file/splice", {index, data: Object.assign(base, state.fileLists[index], data)}) } else { commit("file/push", Object.assign(base, data)) } } }, /** * 忘记文件数据 * @param commit * @param state * @param dispatch * @param data */ forgetFile({commit, state, dispatch}, data) { $A.syncDispatch("forgetFile", data) // const ids = $A.isArray(data.id) ? data.id : [data.id]; ids.some(id => { commit("file/save", state.fileLists.filter(file => file.id != id)) state.fileLists.some(file => { if (file.pid == id) { dispatch("forgetFile", file); } }); }) }, /** * 获取压缩进度 * @param state * @param dispatch * @param data */ packProgress({state, dispatch}, data) { $A.syncDispatch("packProgress", data) // const index = state.filePackLists.findIndex(({name}) => name == data.name); if (index > -1) { state.filePackLists[index].progress = data.progress; } else { state.filePackLists.push(data); } }, /** * 获取文件 * @param commit * @param state * @param dispatch * @param pid * @returns {Promise} */ getFiles({commit, state, dispatch}, pid) { return new Promise(function (resolve, reject) { dispatch("call", { url: 'file/lists', data: { pid }, }).then((result) => { const ids = result.data.map(({id}) => id) commit("file/save", state.fileLists.filter((item) => item.pid != pid || ids.includes(item.id))); // dispatch("saveFile", result.data); resolve(result) }).catch(e => { console.warn(e); reject(e) }); }); }, /** * 搜索文件 * @param state * @param dispatch * @param data * @returns {Promise} */ searchFiles({state, dispatch}, data) { if (!$A.isJson(data)) { data = {key: data} } return new Promise(function (resolve, reject) { dispatch("call", { url: 'file/search', data, }).then((result) => { dispatch("saveFile", result.data); resolve(result) }).catch(e => { console.warn(e); reject(e) }); }); }, /** *****************************************************************************************/ /** ************************************** 项目 **********************************************/ /** *****************************************************************************************/ /** * 保存项目数据 * @param commit * @param state * @param dispatch * @param data */ saveProject({commit, state, dispatch}, data) { $A.syncDispatch("saveProject", data) // if ($A.isArray(data)) { data.forEach((project) => { dispatch("saveProject", project) }); } else if ($A.isJson(data)) { if (typeof data.project_column !== "undefined") { dispatch("saveColumn", data.project_column) delete data.project_column; } const index = state.cacheProjects.findIndex(({id}) => id == data.id); if (index > -1) { commit("project/splice", {index, data: Object.assign({}, state.cacheProjects[index], data)}) } else { if (typeof data.project_user === "undefined") { data.project_user = [] } commit("project/push", data) state.projectTotal++ } // state.cacheDialogs.some(dialog => { if (dialog.type == 'group' && dialog.group_type == 'project' && dialog.group_info && dialog.group_info.id == data.id) { if (data.name !== undefined) { dialog.name = data.name } for (let key in dialog.group_info) { if (!dialog.group_info.hasOwnProperty(key) || data[key] === undefined) continue; dialog.group_info[key] = data[key]; } } }) } }, /** * 忘记项目数据 * @param commit * @param state * @param dispatch * @param data */ forgetProject({commit, state, dispatch}, data) { $A.syncDispatch("forgetProject", data) // const ids = $A.isArray(data.id) ? data.id : [data.id]; ids.some(id => { const index = state.cacheProjects.findIndex(project => project.id == id); if (index > -1) { dispatch("forgetTask", {id: state.cacheTasks.filter(item => item.project_id == data.id).map(item => item.id)}) dispatch("forgetColumn", {id: state.cacheColumns.filter(item => item.project_id == data.id).map(item => item.id)}) commit("project/splice", {index}) state.projectTotal = Math.max(0, state.projectTotal - 1) } }) if (ids.includes(state.projectId)) { const project = $A.cloneJSON(state.cacheProjects).sort((a, b) => { if (a.top_at || b.top_at) { return $A.sortDay(b.top_at, a.top_at); } return b.id - a.id; }).find(({id}) => id && id != data.id); if (project) { $A.goForward({name: 'manage-project', params: {projectId: project.id}}); } else { $A.goForward({name: 'manage-dashboard'}); } } }, /** * 获取项目 * @param state * @param dispatch * @param requestData * @returns {Promise} */ getProjects({state, dispatch}, requestData) { return new Promise(function (resolve, reject) { if (state.userId === 0) { state.cacheProjects = []; reject({msg: 'Parameter error'}); return; } const callData = $callData('projects', requestData, state) // setTimeout(() => { state.loadProjects++; }, 2000) dispatch("call", { url: 'project/lists', data: callData.get() }).then(({data}) => { dispatch("saveProject", data.data); callData.save(data).then(ids => dispatch("forgetProject", {id: ids})) state.projectTotal = data.total_all; // resolve(data) }).catch(e => { console.warn(e); reject(e) }).finally(_ => { state.loadProjects--; }); }); }, /** * 获取项目(队列) * @param dispatch * @param timeout */ getProjectByQueue({dispatch}, timeout = null) { window.__getProjectByQueueTimer && clearTimeout(window.__getProjectByQueueTimer) if (typeof timeout === "number") { window.__getProjectByQueueTimer = setTimeout(_ => dispatch("getProjectByQueue", null), timeout) return } dispatch("getProjects").catch(() => {}); }, /** * 获取单个项目 * @param state * @param dispatch * @param project_id * @returns {Promise} */ getProjectOne({state, dispatch}, project_id) { return new Promise(function (resolve, reject) { if ($A.runNum(project_id) === 0) { reject({msg: 'Parameter error'}); return; } state.projectLoad++; dispatch("call", { url: 'project/one', data: { project_id, }, }).then(result => { setTimeout(() => { state.projectLoad--; }, 10) dispatch("saveProject", result.data); resolve(result) }).catch(e => { console.warn(e); state.projectLoad--; reject(e) }); }); }, /** * 归档项目 * @param state * @param dispatch * @param project_id */ archivedProject({state, dispatch}, project_id) { return new Promise(function (resolve, reject) { if ($A.runNum(project_id) === 0) { reject({msg: 'Parameter error'}); return; } dispatch("call", { url: 'project/archived', data: { project_id, }, }).then(result => { dispatch("forgetProject", {id: project_id}) resolve(result) }).catch(e => { console.warn(e); dispatch("getProjectOne", project_id).catch(() => {}) reject(e) }); }); }, /** * 删除项目 * @param state * @param dispatch * @param project_id */ removeProject({state, dispatch}, project_id) { return new Promise(function (resolve, reject) { if ($A.runNum(project_id) === 0) { reject({msg: 'Parameter error'}); return; } dispatch("call", { url: 'project/remove', data: { project_id, }, }).then(result => { dispatch("forgetProject", {id: project_id}) resolve(result) }).catch(e => { console.warn(e); dispatch("getProjectOne", project_id).catch(() => {}) reject(e) }); }); }, /** * 退出项目 * @param state * @param dispatch * @param project_id */ exitProject({state, dispatch}, project_id) { return new Promise(function (resolve, reject) { if ($A.runNum(project_id) === 0) { reject({msg: 'Parameter error'}); return; } dispatch("call", { url: 'project/exit', data: { project_id, }, }).then(result => { dispatch("forgetProject", {id: project_id}) resolve(result) }).catch(e => { console.warn(e); dispatch("getProjectOne", project_id).catch(() => {}) reject(e) }); }); }, /** *****************************************************************************************/ /** ************************************** 列表 **********************************************/ /** *****************************************************************************************/ /** * 保存列表数据 * @param commit * @param state * @param dispatch * @param data */ saveColumn({commit, state, dispatch}, data) { $A.syncDispatch("saveColumn", data) // if ($A.isArray(data)) { data.forEach((column) => { dispatch("saveColumn", column) }); } else if ($A.isJson(data)) { const index = state.cacheColumns.findIndex(({id}) => id == data.id); if (index > -1) { commit("project/column/splice", {index, data: Object.assign({}, state.cacheColumns[index], data)}) } else { commit("project/column/push", data) } } }, /** * 忘记列表数据 * @param commit * @param state * @param dispatch * @param data */ forgetColumn({commit, state, dispatch}, data) { $A.syncDispatch("forgetColumn", data) // const ids = $A.isArray(data.id) ? data.id : [data.id]; const project_ids = []; ids.some(id => { const index = state.cacheColumns.findIndex(column => column.id == id); if (index > -1) { dispatch("forgetTask", {id: state.cacheTasks.filter(item => item.column_id == data.id).map(item => item.id)}) project_ids.push(state.cacheColumns[index].project_id) commit("project/column/splice", {index}) } }) Array.from(new Set(project_ids)).some(id => dispatch("getProjectOne", id).catch(() => {})) }, /** * 获取列表 * @param commit * @param state * @param dispatch * @param project_id * @returns {Promise} */ getColumns({commit, state, dispatch}, project_id) { return new Promise(function (resolve, reject) { if (state.userId === 0) { state.cacheColumns = []; reject({msg: 'Parameter error'}) return; } state.projectLoad++; dispatch("call", { url: 'project/column/lists', data: { project_id } }).then(({data}) => { state.projectLoad--; // const ids = data.data.map(({id}) => id) commit("project/column/save", state.cacheColumns.filter((item) => item.project_id != project_id || ids.includes(item.id))) // dispatch("saveColumn", data.data); resolve(data.data) // 判断只有1列的时候默认版面为表格模式 if (state.cacheColumns.filter(item => item.project_id == project_id).length === 1) { const cache = state.cacheProjectParameter.find(item => item.project_id == project_id) || {}; if (typeof cache.menuInit === "undefined" || cache.menuInit === false) { dispatch("toggleProjectParameter", { project_id, key: { menuInit: true, menuType: 'table', } }); } } }).catch(e => { console.warn(e); state.projectLoad--; reject(e); }); }) }, /** * 删除列表 * @param state * @param dispatch * @param column_id */ removeColumn({state, dispatch}, column_id) { return new Promise(function (resolve, reject) { if ($A.runNum(column_id) === 0) { reject({msg: 'Parameter error'}); return; } dispatch("call", { url: 'project/column/remove', data: { column_id, }, }).then(result => { dispatch("forgetColumn", {id: column_id}) resolve(result) }).catch(e => { console.warn(e); reject(e); }); }); }, /** *****************************************************************************************/ /** ************************************** 任务 **********************************************/ /** *****************************************************************************************/ /** * 保存任务数据 * @param commit * @param state * @param dispatch * @param data */ saveTask({commit, state, dispatch}, data) { $A.syncDispatch("saveTask", data) // if ($A.isArray(data)) { data.forEach((task) => { dispatch("saveTask", task) }); } else if ($A.isJson(data)) { data._time = $A.dayjs().unix(); // if (data.flow_item_name && data.flow_item_name.indexOf("|") !== -1) { const flowInfo = $A.convertWorkflow(data.flow_item_name) data.flow_item_status = flowInfo.status; data.flow_item_name = flowInfo.name; data.flow_item_color = flowInfo.color; } // if (typeof data.archived_at !== "undefined") { state.cacheTasks.filter(task => task.parent_id == data.id).some(task => { dispatch("saveTask", Object.assign(task, { archived_at: data.archived_at, archived_userid: data.archived_userid })) }) } // let updateMarking = {}; if (typeof data.update_marking !== "undefined") { updateMarking = $A.isJson(data.update_marking) ? data.update_marking : {}; delete data.update_marking; } // const index = state.cacheTasks.findIndex(({id}) => id == data.id); if (index > -1) { commit("task/splice", {index, data: Object.assign({}, state.cacheTasks[index], data)}); } else { commit("task/push", data); } // if (updateMarking.is_update_maintask === true || (data.parent_id > 0 && state.cacheTasks.findIndex(({id}) => id == data.parent_id) === -1)) { dispatch("getTaskOne", data.parent_id).catch(() => {}) } if (updateMarking.is_update_project === true) { dispatch("getProjectOne", data.project_id).catch(() => {}) } if (updateMarking.is_update_content === true) { dispatch("getTaskContent", data.id); } if (updateMarking.is_update_subtask === true) { dispatch("getTaskForParent", data.id).catch(() => {}) } // state.cacheDialogs.some(dialog => { if (dialog.name === undefined || dialog.dialog_delete === 1) { return false; } if (dialog.type == 'group' && dialog.group_type == 'task' && dialog.group_info && dialog.group_info.id == data.id) { if (data.name !== undefined) { dialog.name = data.name } for (let key in dialog.group_info) { if (!dialog.group_info.hasOwnProperty(key) || data[key] === undefined) continue; dialog.group_info[key] = data[key]; } } }) } }, /** * 忘记任务数据 * @param commit * @param state * @param dispatch * @param data */ forgetTask({commit, state, dispatch}, data) { $A.syncDispatch("forgetTask", data) // const ids = ($A.isArray(data.id) ? data.id : [data.id]).filter(id => id != state.taskArchiveView); const parent_ids = []; const project_ids = []; ids.some(id => { const index = state.cacheTasks.findIndex(task => task.id == id); if (index > -1) { if (state.cacheTasks[index].parent_id) { parent_ids.push(state.cacheTasks[index].parent_id) } project_ids.push(state.cacheTasks[index].project_id) commit("task/splice", {index}) } state.cacheTasks.filter(task => task.parent_id == id).some(childTask => { let cIndex = state.cacheTasks.findIndex(task => task.id == childTask.id); if (cIndex > -1) { project_ids.push(childTask.project_id) commit("task/splice", {index: cIndex}) } }) }) Array.from(new Set(parent_ids)).some(id => dispatch("getTaskOne", id).catch(() => {})) Array.from(new Set(project_ids)).some(id => dispatch("getProjectOne", id).catch(() => {})) // if (ids.includes(state.taskId)) { state.taskId = 0; } }, /** * 更新任务“今日任务”、“过期任务” * @param state * @param dispatch */ todayAndOverdue({state, dispatch}) { const now = $A.daytz(); const today = now.format("YYYY-MM-DD"); state.cacheTasks.some(task => { if (!task.end_at) { return false; } const data = {}; const endAt = $A.dayjs(task.end_at) if (!task.today && endAt.format("YYYY-MM-DD") == today) { data.today = true } if (!task.overdue && endAt < now) { data.overdue = true; } if (Object.keys(data).length > 0) { dispatch("saveTask", Object.assign(task, data)); } }) }, /** * 增加任务消息数量 * @param state * @param commit * @param data */ increaseTaskMsgNum({state, commit}, data) { $A.syncDispatch("increaseTaskMsgNum", data) // const index = state.cacheTasks.findIndex(item => item.dialog_id === data.id); if (index !== -1) { const newData = $A.cloneJSON(state.cacheTasks[index]) newData.msg_num++; commit("task/splice", {index, data: newData}) } }, /** * 新增回复数量 * @param state * @param commit * @param data */ increaseMsgReplyNum({state, commit}, data) { $A.syncDispatch("increaseMsgReplyNum", data) // const index = state.dialogMsgs.findIndex(m => m.id == data.id) if (index !== -1) { const newData = $A.cloneJSON(state.dialogMsgs[index]) newData.reply_num++ commit("message/splice", {index, data: newData}) } }, /** * 减少回复数量 * @param state * @param commit * @param data */ decrementMsgReplyNum({state, commit}, data) { $A.syncDispatch("decrementMsgReplyNum", data) // const index = state.dialogMsgs.findIndex(m => m.id == data.id) if (index !== -1) { const newData = $A.cloneJSON(state.dialogMsgs[index]) newData.reply_num-- commit("message/splice", {index, data: newData}) } }, /** * 获取任务 * @param state * @param dispatch * @param requestData * @returns {Promise} */ getTasks({state, dispatch}, requestData) { if (requestData === null) { requestData = {} } const callData = $callData('tasks', requestData, state) // return new Promise(function (resolve, reject) { if (state.userId === 0) { state.cacheTasks = []; reject({msg: 'Parameter error'}); return; } if (requestData.project_id) { state.projectLoad++; } // dispatch("call", { url: 'project/task/lists', data: callData.get() }).then(({data}) => { if (requestData.project_id) { state.projectLoad--; } dispatch("saveTask", data.data); callData.save(data).then(ids => dispatch("forgetTask", {id: ids})) // if (data.next_page_url) { requestData.page = data.current_page + 1 if (data.current_page % 30 === 0) { $A.modalConfirm({ content: "数据已超过" + data.to + "条,是否继续加载?", onOk: () => { dispatch("getTasks", requestData).then(resolve).catch(reject) }, onCancel: () => { resolve() } }); } else { dispatch("getTasks", requestData).then(resolve).catch(reject) } } else { resolve() } }).catch(e => { console.warn(e); reject(e) if (requestData.project_id) { state.projectLoad--; } }); }); }, /** * 获取单个任务 * @param state * @param dispatch * @param data Number|JSONObject{task_id, ?archived_at} * @returns {Promise} */ getTaskOne({state, dispatch}, data) { return new Promise(function (resolve, reject) { if (/^\d+$/.test(data)) { data = {task_id: data} } if ($A.runNum(data.task_id) === 0) { reject({msg: 'Parameter error'}); return; } // if ($A.isArray(state.taskOneLoad[data.task_id])) { state.taskOneLoad[data.task_id].push({resolve, reject}) return; } state.taskOneLoad[data.task_id] = [] // dispatch("call", { url: 'project/task/one', data, }).then(result => { dispatch("saveTask", result.data); resolve(result) state.taskOneLoad[data.task_id].some(item => { item.resolve(result) }) }).catch(e => { console.warn(e); reject(e) state.taskOneLoad[data.task_id].some(item => { item.reject(e) }) }).finally(_ => { delete state.taskOneLoad[data.task_id] }); }); }, /** * 获取Dashboard相关任务 * @param state * @param dispatch * @param timeout */ getTaskForDashboard({state, dispatch}, timeout) { window.__getTaskForDashboard && clearTimeout(window.__getTaskForDashboard) if (typeof timeout === "number") { if (timeout > -1) { window.__getTaskForDashboard = setTimeout(_ => dispatch("getTaskForDashboard", null), timeout) } return; } // if (state.loadDashboardTasks === true) { return; } state.loadDashboardTasks = true; // dispatch("getTasks", null).finally(_ => { state.loadDashboardTasks = false; }) }, /** * 获取项目任务 * @param state * @param dispatch * @param project_id * @returns {Promise} */ getTaskForProject({state, dispatch}, project_id) { return new Promise(function (resolve, reject) { dispatch("getTasks", {project_id}).then(resolve).catch(reject) }) }, /** * 获取子任务 * @param state * @param dispatch * @param parent_id * @returns {Promise} */ getTaskForParent({state, dispatch}, parent_id) { return new Promise(function (resolve, reject) { dispatch("getTasks", {parent_id}).then(resolve).catch(reject) }) }, /** * 删除任务 * @param state * @param dispatch * @param data * @returns {Promise} */ removeTask({state, dispatch}, data) { return new Promise(function (resolve, reject) { if ($A.runNum(data.task_id) === 0) { reject({msg: 'Parameter error'}); return; } dispatch("setLoad", { key: `task-${data.task_id}`, delay: 300 }) dispatch("call", { url: 'project/task/remove', data, }).then(result => { state.taskArchiveView = 0; dispatch("forgetTask", {id: data.task_id}) resolve(result) }).catch(e => { console.warn(e); dispatch("getTaskOne", data.task_id).catch(() => {}) reject(e) }).finally(_ => { dispatch("cancelLoad", `task-${data.task_id}`) }); }); }, /** * 归档(还原)任务 * @param state * @param dispatch * @param data Number|JSONObject{task_id, ?archived_at} * @returns {Promise} */ archivedTask({state, dispatch}, data) { return new Promise(function (resolve, reject) { if (/^\d+$/.test(data)) { data = {task_id: data} } if ($A.runNum(data.task_id) === 0) { reject({msg: 'Parameter error'}); return; } dispatch("setLoad", { key: `task-${data.task_id}`, delay: 300 }) dispatch("call", { url: 'project/task/archived', data, }).then(result => { dispatch("saveTask", result.data) resolve(result) }).catch(e => { console.warn(e); dispatch("getTaskOne", data.task_id).catch(() => {}) reject(e) }).finally(_ => { dispatch("cancelLoad", `task-${data.task_id}`) }); }); }, /** * 获取任务详细描述 * @param state * @param dispatch * @param task_id */ getTaskContent({state, dispatch}, task_id) { if ($A.runNum(task_id) === 0) { return; } dispatch("setLoad", { key: `task-${task_id}`, delay: 1200 }) dispatch("call", { url: 'project/task/content', data: { task_id, }, }).then(result => { dispatch("saveTaskContent", result.data) }).catch(e => { console.warn(e); }).finally(_ => { dispatch("cancelLoad", `task-${task_id}`) }); }, /** * 更新任务详情 * @param commit * @param state * @param dispatch * @param data */ saveTaskContent({commit, state, dispatch}, data) { $A.syncDispatch("saveTaskContent", data) // if ($A.isArray(data)) { data.forEach(item => { dispatch("saveTaskContent", item) }); } else if ($A.isJson(data)) { const index = state.taskContents.findIndex(({task_id}) => task_id == data.task_id); if (index > -1) { commit("task/content/splice", {index, data: Object.assign({}, state.taskContents[index], data)}) } else { commit("task/content/push", data) } } }, /** * 获取任务文件 * @param state * @param dispatch * @param task_id */ getTaskFiles({state, dispatch}, task_id) { if ($A.runNum(task_id) === 0) { return; } dispatch("call", { url: 'project/task/files', data: { task_id, }, }).then(result => { result.data.forEach((data) => { const index = state.taskFiles.findIndex(({id}) => id == data.id) if (index > -1) { state.taskFiles.splice(index, 1, data) } else { state.taskFiles.push(data) } }) dispatch("saveTask", { id: task_id, file_num: result.data.length }); }).catch(e => { console.warn(e); }); }, /** * 忘记任务文件 * @param state * @param dispatch * @param file_id */ forgetTaskFile({state, dispatch}, file_id) { const ids = $A.isArray(file_id) ? file_id : [file_id]; ids.some(id => { const index = state.taskFiles.findIndex(file => file.id == id) if (index > -1) { state.taskFiles.splice(index, 1) } }) }, /** * 打开任务详情页 * @param state * @param dispatch * @param task */ openTask({state, dispatch}, task) { let task_id = task; if ($A.isJson(task)) { if (task.parent_id > 0) { task_id = task.parent_id; } else { task_id = task.id; } } if ($A.isSubElectron) { if (task_id > 0) { $A.Electron.sendMessage('updateChildWindow', { name: `task-${task_id}`, path: `/single/task/${task_id}`, }); } else { $A.Electron.sendMessage('windowClose'); } return } if (state.taskId > 0) { emitter.emit('handleMoveTop', 'taskModal'); // 已打开任务时将任务窗口置顶 } state.taskArchiveView = task_id; state.taskId = task_id; if (task_id > 0) { dispatch("getTaskOne", { task_id, archived: 'all' }).then(() => { dispatch("getTaskContent", task_id); dispatch("getTaskFiles", task_id); dispatch("getTaskForParent", task_id).catch(() => {}); dispatch("saveTaskBrowse", task_id); }).catch(({msg}) => { $A.modalWarning({ content: msg, onOk: () => { state.taskId = 0; } }); }); } else { state.taskOperation = {}; } }, /** * 添加任务 * @param state * @param dispatch * @param data * @returns {Promise} */ taskAdd({state, dispatch}, data) { return new Promise(function (resolve, reject) { const post = $A.cloneJSON($A.newDateString(data)); if ($A.isArray(post.column_id)) post.column_id = post.column_id.find((val) => val) // dispatch("call", { url: 'project/task/add', data: post, spinner: 600, method: 'post', }).then(result => { if (result.data.is_visible === 1) { dispatch("addTaskSuccess", result.data) } state.taskLatestId = result.data.id resolve(result) }).catch(e => { console.warn(e); reject(e); }); }); }, /** * 添加子任务 * @param dispatch * @param data {task_id, name} * @returns {Promise} */ taskAddSub({dispatch}, data) { return new Promise(function (resolve, reject) { dispatch("call", { url: 'project/task/addsub', data: data, spinner: 600, }).then(result => { dispatch("addTaskSuccess", result.data) resolve(result) }).catch(e => { console.warn(e); reject(e); }); }); }, /** * 添加任务成功 * @param dispatch * @param task */ addTaskSuccess({dispatch}, task) { if (typeof task.new_column !== "undefined") { dispatch("saveColumn", task.new_column) delete task.new_column } dispatch("saveTask", task) dispatch("getProjectOne", task.project_id).catch(() => {}) }, /** * 更新任务 * @param state * @param dispatch * @param data {task_id, ?} * @returns {Promise} */ taskUpdate({state, dispatch}, data) { return new Promise(function (resolve, reject) { dispatch("taskBeforeUpdate", data).then(({post}) => { dispatch("setLoad", { key: `task-${post.task_id}`, delay: 300 }) dispatch("call", { url: 'project/task/update', data: post, method: 'post', }).then(result => { dispatch("saveTask", result.data) resolve(result) }).catch(e => { console.warn(e); dispatch("getTaskOne", post.task_id).catch(() => {}) reject(e) }).finally(_ => { dispatch("cancelLoad", `task-${post.task_id}`) }); }).catch(reject) }); }, /** * 更新任务之前判断 * @param state * @param dispatch * @param data * @returns {Promise} */ taskBeforeUpdate({state, dispatch}, data) { return new Promise(function (resolve, reject) { let post = $A.cloneJSON($A.newDateString(data)); let title = "温馨提示"; let content = null; // 修改时间前置判断 if (typeof post.times !== "undefined") { if (data.times[0] === false) { content = "你确定要取消任务时间吗?" } const currentTask = state.cacheTasks.find(({id}) => id == post.task_id); title = currentTask.parent_id > 0 ? "更新子任务" : "更新主任务" if (currentTask) { if (currentTask.parent_id > 0) { // 修改子任务,判断主任务 if (post.times[0]) { state.cacheTasks.some(parentTask => { if (parentTask.id != currentTask.parent_id) { return false; } if (!parentTask.end_at) { content = "主任务没有设置时间,设置子任务将同步设置主任务" return true; } let n1 = $A.dayjs(post.times[0]).unix(), n2 = $A.dayjs(post.times[1]).unix(), o1 = $A.dayjs(parentTask.start_at).unix(), o2 = $A.dayjs(parentTask.end_at).unix(); if (n1 < o1) { content = "新设置的子任务开始时间在主任务时间之外,修改后将同步修改主任务" // 子任务开始时间 < 主任务开始时间 return true; } if (n2 > o2) { content = "新设置的子任务结束时间在主任务时间之外,修改后将同步修改主任务" // 子任务结束时间 > 主任务结束时间 return true; } }) } } else { // 修改主任务,判断子任务 state.cacheTasks.some(subTask => { if (subTask.parent_id != currentTask.id) { return false; } if (!subTask.end_at) { return false; } let n1 = $A.dayjs(post.times[0]).unix(), n2 = $A.dayjs(post.times[1]).unix(), c1 = $A.dayjs(currentTask.start_at).unix(), c2 = $A.dayjs(currentTask.end_at).unix(), o1 = $A.dayjs(subTask.start_at).unix(), o2 = $A.dayjs(subTask.end_at).unix(); if (c1 == o1 && c2 == o2) { return false; } if (!post.times[0]) { content = `子任务(${subTask.name})已设置时间,清除主任务时间后将同步清除子任务的时间` return true; } if (n1 > o1) { content = `新设置的开始时间在子任务(${subTask.name})时间之内,修改后将同步修改子任务` // 主任务开始时间 > 子任务开始时间 return true; } if (n2 < o2) { content = `新设置的结束时间在子任务(${subTask.name})时间之内,修改后将同步修改子任务` // 主任务结束时间 < 子任务结束时间 return true; } }) } } } // if (content === null) { resolve({ confirm: false, post }); return } $A.modalConfirm({ title, content, onOk: () => { resolve({ confirm: true, post }); }, onCancel: () => { reject({msg: false}) } }); }); }, /** * 获取任务流程信息 * @param state * @param dispatch * @param task_id * @param project_id * @returns {Promise} */ getTaskFlow({state, dispatch}, {task_id, project_id}) { return new Promise(function (resolve, reject) { dispatch("call", { url: 'project/task/flow', data: { task_id: task_id, project_id: project_id || 0 }, }).then(result => { let task = state.cacheTasks.find(({id}) => id == task_id) let {data} = result data.turns.some(item => { const index = state.taskFlowItems.findIndex(({id}) => id == item.id); if (index > -1) { state.taskFlowItems.splice(index, 1, item); } else { state.taskFlowItems.push(item); } if (task && task.flow_item_id == item.id && task.flow_item_name != item.name) { state.cacheTasks.filter(({flow_item_id})=> flow_item_id == item.id).some(task => { dispatch("saveTask", { id: task.id, flow_item_name: `${item.status}|${item.name}|${item.color}`, }) }) } }) // delete data.turns; const index = state.taskFlows.findIndex(({task_id}) => task_id == data.task_id); if (index > -1) { state.taskFlows.splice(index, 1, data); } else { state.taskFlows.push(data); } resolve(result) }).catch(e => { console.warn(e); reject(e); }); }); }, /** * 获取任务优先级预设数据 * @param state * @param dispatch * @param timeout */ getTaskPriority({state, dispatch}, timeout) { window.__getTaskPriority && clearTimeout(window.__getTaskPriority) window.__getTaskPriority = setTimeout(() => { dispatch("call", { url: 'system/priority', }).then(result => { state.taskPriority = result.data; }).catch(e => { console.warn(e); }); }, typeof timeout === "number" ? timeout : 1000) }, /** * 获取添加项目列表预设数据 * @param state * @param dispatch * @returns {Promise} */ getColumnTemplate({state, dispatch}) { return new Promise(function (resolve, reject) { dispatch("call", { url: 'system/column/template', }).then(result => { state.columnTemplate = result.data; resolve(result) }).catch(e => { console.warn(e); reject(e); }); }); }, /** * 保存完成任务临时表 * @param state * @param task_id */ saveTaskCompleteTemp({state}, task_id) { if (/^\d+$/.test(task_id) && !state.taskCompleteTemps.includes(task_id)) { state.taskCompleteTemps.push(task_id) } }, /** * 忘记完成任务临时表 * @param state * @param task_id 任务ID 或 true标识忘记全部 */ forgetTaskCompleteTemp({state}, task_id) { if (task_id === true) { state.taskCompleteTemps = []; } else if (/^\d+$/.test(task_id)) { state.taskCompleteTemps = state.taskCompleteTemps.filter(id => id != task_id); } }, /** * 保存任务浏览记录 * @param dispatch * @param task_id */ saveTaskBrowse({dispatch}, task_id) { // 直接调用API保存到远程,不维护本地缓存 dispatch('call', { url: 'users/task/browse_save', data: { task_id: task_id }, method: 'post', spinner: 0, // 静默调用,不显示loading }).catch(error => { console.warn('保存任务浏览历史失败:', error); // API失败时不影响用户体验,只记录错误 }); }, /** * 获取任务浏览历史 * @param dispatch * @param limit */ getTaskBrowseHistory({dispatch}, limit = 20) { return dispatch('call', { url: 'users/task/browse', data: { limit: limit }, method: 'get', spinner: 0, // 静默调用 }); }, /** * 任务默认时间 * @param state * @param dispatch * @param array * @returns {Promise} */ taskDefaultTime({state, dispatch}, array) { return new Promise(async resolve => { if ($A.isArray(array) && array.length === 2) { if (/\s+(00:00|23:59)$/.test(array[0]) && /\s+(00:00|23:59)$/.test(array[1])) { array[0] = await dispatch("taskDefaultStartTime", array[0]) array[1] = await dispatch("taskDefaultEndTime", array[1]) } } resolve(array) }); }, /** * 任务默认开始时间 * @param state * @param value * @returns {Promise} */ taskDefaultStartTime({state}, value) { return new Promise(resolve => { if (/(\s|^)([0-2]\d):([0-5]\d)(:\d{1,2})*$/.test(value)) { value = value.replace(/(\s|^)([0-2]\d):([0-5]\d)(:\d{1,2})*$/, "$1" + state.systemConfig.task_default_time[0]) } resolve(value) }); }, /** * 任务默认结束时间 * @param state * @param value * @returns {Promise} */ taskDefaultEndTime({state}, value) { return new Promise(resolve => { if (/(\s|^)([0-2]\d):([0-5]\d)(:\d{1,2})*$/.test(value)) { value = value.replace(/(\s|^)([0-2]\d):([0-5]\d)(:\d{1,2})*$/, "$1" + state.systemConfig.task_default_time[1]) } resolve(value) }); }, /** * 更新任务模板 * @param state * @param dispatch * @param projectId * @returns {Promise} */ async updateTaskTemplates({state, dispatch}, projectId) { const {data} = await dispatch("call", { url: 'project/task/template_list', data: { project_id: projectId }, }) state.taskTemplates = state.taskTemplates.filter(template => template.project_id !== projectId).concat(data || []) }, /** *****************************************************************************************/ /** ************************************** 收藏 **********************************************/ /** *****************************************************************************************/ /** * 检查收藏状态 * @param dispatch * @param {object} params {type: 'task|project|file|message', id: number} */ checkFavoriteStatus({dispatch}, {type, id}) { return dispatch('call', { url: 'users/favorite/check', data: { type: type, id: id }, method: 'get', spinner: 0, // 静默调用 }); }, /** * 切换收藏状态 * @param dispatch * @param {object} params {type: 'task|project|file|message', id: number} */ toggleFavorite({dispatch}, {type, id}) { return dispatch('call', { url: 'users/favorite/toggle', data: { type: type, id: id }, method: 'post', }); }, /** * 批量检查收藏状态 * @param dispatch * @param {object} params {type: 'task|project|file|message', items: array} */ checkFavoritesStatus({dispatch}, {type, items}) { if (!Array.isArray(items) || items.length === 0) { return Promise.resolve([]); } // 批量检查收藏状态 const promises = items.map(item => { return dispatch('checkFavoriteStatus', {type, id: item.id}) .then(({data}) => ({ id: item.id, favorited: data.favorited || false })) .catch(() => ({ id: item.id, favorited: false })); }); return Promise.all(promises); }, /** *****************************************************************************************/ /** ************************************** 会话 **********************************************/ /** *****************************************************************************************/ /** * 更新会话数据 * @param commit * @param state * @param dispatch * @param data */ saveDialog({commit, state, dispatch}, data) { $A.syncDispatch("saveDialog", data) // if ($A.isArray(data)) { data.forEach((dialog) => { dispatch("saveDialog", dialog) }); } else if ($A.isJson(data)) { data.id = parseInt(data.id) const index = state.cacheDialogs.findIndex(({id}) => id == data.id); let lastForce = false if (typeof data.last_force !== "undefined") { lastForce = true delete data.last_force } if (index > -1) { const original = state.cacheDialogs[index] const nowTime = data.user_ms const originalTime = original.user_ms || 0 if (nowTime < originalTime) { typeof data.unread !== "undefined" && delete data.unread typeof data.unread_one !== "undefined" && delete data.unread_one typeof data.mention !== "undefined" && delete data.mention typeof data.mention_ids !== "undefined" && delete data.mention_ids } if (data.unread_one) { if (state.dialogMsgs.find(m => m.id == data.unread_one)?.read_at) { delete data.unread_one } } if (data.mention_ids) { data.mention_ids = data.mention_ids.filter(id => { return !state.dialogMsgs.find(m => m.id == id)?.read_at }) } if (!lastForce && data.last_at && original.last_at && $A.dayjs(data.last_at) < $A.dayjs(original.last_at)) { delete data.last_at delete data.last_msg } commit("dialog/splice", {index, data: Object.assign({}, original, data)}) } else { commit("dialog/push", data) } } }, /** * 更新会话最后消息 * @param state * @param dispatch * @param data */ updateDialogLastMsg({state, dispatch}, data) { $A.syncDispatch("updateDialogLastMsg", data) // if ($A.isArray(data)) { data.forEach((msg) => { dispatch("updateDialogLastMsg", msg) }); } else if ($A.isJson(data)) { const index = state.cacheDialogs.findIndex(({id}) => id == data.dialog_id); if (index > -1) { const updateData = { id: data.dialog_id, last_msg: data, last_at: data.created_at || $A.daytz().format("YYYY-MM-DD HH:mm:ss") } if (data.mtype == 'tag') { updateData.has_tag = true; } if (data.mtype == 'todo') { updateData.has_todo = true; } if (data.mtype == 'image') { updateData.has_image = true; } if (data.mtype == 'file') { updateData.has_file = true; } if (data.link) { updateData.has_link = true; } dispatch("saveDialog", updateData); } else { dispatch("getDialogOne", data.dialog_id).catch(() => {}) } } }, /** * 获取会话列表(避免重复获取) * @param state * @param dispatch * @returns {Promise} */ getDialogAuto({state, dispatch}) { return new Promise(function (resolve, reject) { if (state.loadDialogAuto) { reject({msg: 'Loading'}); return } setTimeout(_ => { state.loadDialogs++; }, 2000) state.loadDialogAuto = true dispatch("getDialogs") .then(resolve) .catch(reject) .finally(_ => { state.loadDialogs--; state.loadDialogAuto = false }) }) }, /** * 获取会话列表 * @param state * @param dispatch * @param requestData * @returns {Promise} */ getDialogs({state, dispatch}, requestData) { return new Promise(function (resolve, reject) { if (state.userId === 0) { state.cacheDialogs = []; reject({msg: 'Parameter error'}); return; } if (!$A.isJson(requestData)) { requestData = {} } if (typeof requestData.page === "undefined") { requestData.page = 1 } if (typeof requestData.pagesize === "undefined") { requestData.pagesize = 20 } const callData = $callData('dialogs', requestData, state) // dispatch("call", { url: 'dialog/lists', data: callData.get() }).then(({data}) => { dispatch("saveDialog", data.data); callData.save(data).then(ids => dispatch("forgetDialog", {id: ids})) // if (data.current_page === 1) { dispatch("getDialogLatestMsgs", data.data.map(({id}) => id)) } // if (data.next_page_url && data.current_page < 5) { requestData.page++ dispatch("getDialogs", requestData).then(resolve).catch(reject) } else { resolve() dispatch("getDialogBeyonds") } }).catch(e => { console.warn(e); reject(e) }); }); }, /** * 获取超期未读会话 * @param state * @param dispatch * @returns {Promise} */ async getDialogBeyonds({state, dispatch}) { const key = await $A.IDBString("dialogBeyond") const val = $A.daytz().format("YYYY-MM-DD HH") if (key == val) { return // 一小时取一次 } await $A.IDBSet("dialogBeyond", val) // const filter = (func) => { return state.cacheDialogs .filter(func) .sort((a, b) => { return $A.sortDay(a.last_at, b.last_at); }) .find(({id}) => id > 0) } const unreadDialog = filter(({unread, last_at}) => { return unread > 0 && last_at }); const todoDialog = filter(({todo_num, last_at}) => { return todo_num > 0 && last_at }); // dispatch("call", { url: 'dialog/beyond', data: { unread_at: unreadDialog ? unreadDialog.last_at : $A.dayjs().unix(), todo_at: todoDialog ? todoDialog.last_at : $A.dayjs().unix() } }).then(({data}) => { dispatch("saveDialog", data); }); }, /** * 获取单个会话信息 * @param state * @param dispatch * @param dialogId * @returns {Promise} */ getDialogOne({state, dispatch}, dialogId) { return new Promise(function (resolve, reject) { if ($A.runNum(dialogId) === 0) { reject({msg: 'Parameter error'}); return; } dispatch("call", { url: 'dialog/one', data: { dialog_id: dialogId, }, }).then(result => { dispatch("saveDialog", result.data); resolve(result); }).catch(e => { console.warn(e); reject(e); }); }); }, /** * 获取会话待办 * @param commit * @param state * @param dispatch * @param dialogId */ getDialogTodo({commit, state, dispatch}, dialogId) { dispatch("call", { url: 'dialog/todo', data: { dialog_id: dialogId, }, }).then(({data}) => { if ($A.arrayLength(data) > 0) { if (dialogId > 0) { dispatch("saveDialog", { id: dialogId, todo_num: $A.arrayLength(data) }); commit("dialog/todo/save", state.dialogTodos.filter(item => item.dialog_id != dialogId)) } dispatch("saveDialogTodo", data) } else { if (dialogId > 0) { dispatch("saveDialog", { id: dialogId, todo_num: 0 }); } } }).catch(console.warn); }, /** * 获取会话消息置顶 * @param state * @param dispatch * @param dialogId */ getDialogMsgTop({state, dispatch}, dialogId) { dispatch("call", { url: 'dialog/msg/topinfo', data: { dialog_id: dialogId, }, }).then(({data}) => { if ($A.isJson(data)) { dispatch("saveDialogMsgTop", data) } }).catch(console.warn); }, /** * 打开会话 * @param state * @param dispatch * @param dialogId * @returns {Promise} */ openDialog({state, dispatch}, dialogId) { return new Promise(async (resolve, reject) => { let singleWindow, searchMsgId, dialogMsgId; if ($A.isJson(dialogId)) { singleWindow = dialogId.single; searchMsgId = dialogId.search_msg_id; dialogMsgId = dialogId.dialog_msg_id; dialogId = dialogId.dialog_id; } singleWindow = typeof singleWindow === "boolean" ? singleWindow : $A.isSubElectron; searchMsgId = /^\d+$/.test(searchMsgId) ? parseInt(searchMsgId) : 0; dialogMsgId = /^\d+$/.test(dialogMsgId) ? parseInt(dialogMsgId) : 0; dialogId = /^\d+$/.test(dialogId) ? parseInt(dialogId) : 0; // if (dialogId > 0 && state.cacheDialogs.findIndex(item => item.id == dialogId) === -1) { dispatch("showSpinner", 300) try { await dispatch("getDialogOne", dialogId) } catch (e) { reject(e); return; } finally { dispatch("hiddenSpinner") } } // if ($A.Electron && singleWindow) { dispatch('openDialogNewWindow', dialogId); resolve() return } // if (state.dialogModalShow) { // 已打开对话时将对话窗口置顶 emitter.emit('handleMoveTop', 'dialogModal'); } else if (state.dialogId === dialogId) { // 如果对话窗口未打开,则清除当前对话ID(避免类此:已经在消息中打开项目对话时无法在其他地方打开项目对话) state.dialogId = 0; } // requestAnimationFrame(_ => { state.dialogSearchMsgId = searchMsgId; state.dialogMsgId = dialogMsgId; state.dialogId = dialogId; resolve() }) }) }, /** * 打开会话(通过会员ID打开个人会话) * @param state * @param dispatch * @param userid */ openDialogUserid({state, dispatch}, userid) { return new Promise((resolve, reject) => { const dialog = state.cacheDialogs.find(item => { if (item.type !== 'user' || !item.dialog_user) { return false } return item.dialog_user.userid === userid }); if (dialog) { return dispatch("openDialog", dialog.id).then(resolve).catch(reject) } dispatch("call", { url: 'dialog/open/user', data: { userid, }, spinner: 600 }).then(async ({data}) => { dispatch("saveDialog", data); dispatch("openDialog", data.id).then(resolve).catch(reject) }).catch(e => { console.warn(e); reject(e); }) }); }, /** * 打开会话(客户端新窗口) * @param state * @param dispatch * @param dialogId * @returns {Promise} */ openDialogNewWindow({state, dispatch}, dialogId) { const dialogData = state.cacheDialogs.find(({id}) => id === dialogId) || {} dispatch('openChildWindow', { name: `dialog-${dialogId}`, path: `/single/dialog/${dialogId}`, force: false, config: { title: dialogData.name, parent: null, width: Math.min(window.screen.availWidth, 1024), height: Math.min(window.screen.availHeight, 768), }, }); }, /** * 忘记对话数据 * @param commit * @param state * @param dispatch * @param data */ forgetDialog({commit, state, dispatch}, data) { $A.syncDispatch("forgetDialog", data) // const ids = $A.isArray(data.id) ? data.id : [data.id]; ids.some(id => { if ($A.isJson(id)) { id = id.id } const index = state.cacheDialogs.findIndex(dialog => dialog.id == id); if (index > -1) { dispatch("forgetDialogMsg", {id: state.dialogMsgs.filter(item => item.dialog_id == data.id).map(item => item.id)}) commit("dialog/splice", {index}) } }) if (ids.includes(state.dialogId)) { state.dialogId = 0 } }, /** * 保存正在会话 * @param commit * @param state * @param dispatch * @param data {uid, dialog_id} */ saveInDialog({commit, state, dispatch}, data) { $A.syncDispatch("saveInDialog", data) // const index = state.dialogIns.findIndex(item => item.uid == data.uid); if (index > -1) { commit("dialog/in/splice", {index, data: Object.assign({}, state.dialogIns[index], data)}); } else { commit("dialog/in/push", data); } // 会话消息总数量大于5000时只保留最近打开的50个会话 const msg_max = 5000 const retain_num = 500 commit('dialog/history/save', state.dialogHistory.filter(id => id != data.dialog_id)) commit('dialog/history/push', data.dialog_id) if (state.dialogMsgs.length > msg_max && state.dialogHistory.length > retain_num) { const historys = state.dialogHistory.slice().reverse() const newIds = [] const delIds = [] historys.forEach(id => { if (newIds.length < retain_num || state.dialogIns.findIndex(item => item.dialog_id == id) > -1) { newIds.push(id) } else { delIds.push(id) } }) if (delIds.length > 0) { commit("message/save", state.dialogMsgs.filter(item => !delIds.includes(item.dialog_id))) } commit('dialog/history/save', newIds) } }, /** * 忘记正在会话 * @param state * @param commit * @param data */ forgetInDialog({state, commit}, data) { $A.syncDispatch("forgetInDialog", data) // const index = state.dialogIns.findIndex(item => item.uid == data.uid); if (index > -1) { commit("dialog/in/splice", {index}) } }, /** * 关闭对话 * @param state * @param commit * @param data */ closeDialog({state, commit}, data) { $A.syncDispatch("closeDialog", data) // 判断参数 if (!/^\d+$/.test(data.id)) { return } // 更新草稿标签 commit('draft/tag', data.id) // 关闭会话后删除会话超限消息 const msgs = state.dialogMsgs.filter(item => item.dialog_id == data.id) if (msgs.length > state.dialogMsgKeep) { const delIds = msgs.sort((a, b) => b.id - a.id).splice(state.dialogMsgKeep).map(item => item.id) commit("message/save", state.dialogMsgs.filter(item => !delIds.includes(item.id))) } }, /** * 清理会话本地缓存 * @param state * @param commit * @param data */ clearDialogMsgs({state, commit}, data) { $A.syncDispatch("clearDialogMsgs", data) // 判断参数 if (!/^\d+$/.test(data.id)) { return } // 清理会话本地缓存 commit("message/save", state.dialogMsgs.filter(item => item.dialog_id != data.id)) }, /** * 保存待办数据 * @param commit * @param state * @param dispatch * @param data */ saveDialogTodo({commit, state, dispatch}, data) { $A.syncDispatch("saveDialogTodo", data) // if ($A.isArray(data)) { data.forEach(item => { dispatch("saveDialogTodo", item) }); } else if ($A.isJson(data)) { const index = state.dialogTodos.findIndex(item => item.id == data.id); if (index > -1) { commit('dialog/todo/splice', {index, data: Object.assign({}, state.dialogTodos[index], data)}); } else { commit('dialog/todo/push', data) } } }, /** * 忘记待办数据 * @param state * @param commit * @param data */ forgetDialogTodoForMsgId({state, commit}, data) { $A.syncDispatch("forgetDialogTodoForMsgId", data) // const index = state.dialogTodos.findIndex(item => item.msg_id == data.id); if (index > -1) { commit('dialog/todo/splice', {index}) } }, /** * 保存置顶数据 * @param commit * @param state * @param dispatch * @param data */ saveDialogMsgTop({commit, state, dispatch}, data) { $A.syncDispatch("saveDialogMsgTop", data) // if ($A.isArray(data)) { data.forEach(item => { dispatch("saveDialogMsgTop", item) }); } else if ($A.isJson(data)) { commit('dialog/msg/top/save', state.dialogMsgTops.filter(item => item.dialog_id != data.dialog_id)) const index = state.dialogMsgTops.findIndex(item => item.id == data.id); if (index > -1) { commit('dialog/msg/top/splice', {index, data: Object.assign({}, state.dialogMsgTops[index], data)}); } else { commit('dialog/msg/top/push', data) } } }, /** * 忘记消息置顶数据 * @param state * @param commit * @param data */ forgetDialogMsgTopForMsgId({state, commit}, data) { $A.syncDispatch("forgetDialogMsgTopForMsgId", data) // const index = state.dialogMsgTops.findIndex(item => item.msg_id == data.id); if (index > -1) { commit('dialog/msg/top/splice', {index}) } }, /** * 保存草稿 * @param commit * @param id * @param content * @param immediate */ saveDialogDraft({commit}, {id, content, immediate = false}) { if ($A.isSubElectron) { dialogDraftState.subTemp = {id, content, immediate: true} return } $A.syncDispatch("saveDialogDraft", {id, content, immediate}) // 清除已有的计时器 if (dialogDraftState.timer[id]) { clearTimeout(dialogDraftState.timer[id]) delete dialogDraftState.timer[id] } // 创建新的计时器 dialogDraftState.timer[id] = setTimeout(() => { commit('draft/set', {id, content}) delete dialogDraftState.timer[id] }, (immediate || !content) ? 0 : 600) }, /** * 保存引用 * @param commit * @param data {id, type, content} */ saveDialogQuote({commit}, data) { commit('quote/set', data) }, /** * 移除引用 * @param commit * @param id */ removeDialogQuote({commit}, id) { commit('quote/remove', id) }, /** *****************************************************************************************/ /** ************************************** 消息 **********************************************/ /** *****************************************************************************************/ /** * 更新消息数据 * @param commit * @param state * @param dispatch * @param data */ saveDialogMsg({commit, state, dispatch}, data) { $A.syncDispatch("saveDialogMsg", data) // if ($A.isArray(data)) { data.forEach((msg) => { dispatch("saveDialogMsg", msg) }); return } // if (data.type == "notice") { data.estimateSize = 42; } const index = state.dialogMsgs.findIndex(({id}) => id == data.id); if (index > -1) { const original = state.dialogMsgs[index] if (original.read_at) { delete data.read_at } data = Object.assign({}, original, data) commit("message/splice", {index, data}) } else { commit("message/push", data) } // const dialog = state.cacheDialogs.find(({id}) => id == data.dialog_id); if (dialog) { let isUpdate = false if (!data.read_at && data.userid != state.userId && !state.dialogIns.find(({dialog_id}) => dialog_id == dialog.id)) { if (dialog.unread_one) { dialog.unread_one = Math.min(dialog.unread_one, data.id) } else { dialog.unread_one = data.id } isUpdate = true } if (dialog.last_msg && dialog.last_msg.id == data.id) { dialog.last_msg = Object.assign({}, dialog.last_msg, data) isUpdate = true } if (isUpdate) { dispatch("saveDialog", dialog) } } }, /** * 忘记消息数据 * @param commit * @param state * @param dispatch * @param data */ forgetDialogMsg({commit, state, dispatch}, data) { $A.syncDispatch("forgetDialogMsg", data) // const ids = $A.isArray(data.id) ? data.id : [data.id]; ids.some(id => { const index = state.dialogMsgs.findIndex(item => item.id == id); if (index > -1) { const msgData = state.dialogMsgs[index] dispatch("decrementMsgReplyNum", {id: msgData.reply_id}); dispatch("audioStop", $A.getObject(msgData, 'msg.path')); commit("message/splice", {index}) } }) dispatch("forgetDialogTodoForMsgId", data) dispatch("forgetDialogMsgTopForMsgId", data) }, /** * 获取会话消息 * @param commit * @param state * @param dispatch * @param getters * @param data {dialog_id, msg_id, ?msg_type, ?position_id, ?prev_id, ?next_id, ?save_before, ?save_after, ?clear_before, ?spinner} * @returns {Promise} */ getDialogMsgs({commit, state, dispatch, getters}, data) { return new Promise((resolve, reject) => { let saveBefore = _ => {} let saveAfter = _ => {} let clearBefore = false let spinner = false if (typeof data.save_before !== "undefined") { saveBefore = typeof data.save_before === "function" ? data.save_before : _ => {} delete data.save_before } if (typeof data.save_after !== "undefined") { saveAfter = typeof data.save_after === "function" ? data.save_after : _ => {} delete data.save_after } if (typeof data.clear_before !== "undefined") { clearBefore = typeof data.clear_before === "boolean" ? data.clear_before : false delete data.clear_before } if (typeof data.spinner !== "undefined") { spinner = data.spinner delete data.spinner } // const loadKey = `msg::${data.dialog_id}-${data.msg_id}-${data.msg_type || ''}` if (getters.isLoad(loadKey)) { reject({msg: 'Loading'}); return } dispatch("setLoad", loadKey) // if (clearBefore) { commit("message/save", state.dialogMsgs.filter(({dialog_id}) => dialog_id !== data.dialog_id)) } // data.pagesize = 25; // dispatch("call", { url: 'dialog/msg/list', data, spinner, complete: _ => dispatch("cancelLoad", loadKey) }).then(result => { saveBefore() const resData = result.data; if ($A.isJson(resData.dialog)) { const ids = resData.list.map(({id}) => id) commit("message/save", state.dialogMsgs.filter(item => { return item.dialog_id != data.dialog_id || ids.includes(item.id) || $A.dayjs(item.created_at).unix() >= resData.time })) dispatch("saveDialog", resData.dialog) } if ($A.isArray(resData.todo)) { commit("dialog/todo/save", state.dialogTodos.filter(item => item.dialog_id != data.dialog_id)) dispatch("saveDialogTodo", resData.todo) } if ($A.isJson(resData.top)) { dispatch("saveDialogMsgTop", resData.top) } // dispatch("saveDialogMsg", resData.list) resolve(result) saveAfter() }).catch(e => { console.warn(e); reject(e) }).finally(_ => { // 将原数据清除,避免死循环 if (data.prev_id) { const prevMsg = state.dialogMsgs.find(({prev_id}) => prev_id == data.prev_id) if (prevMsg) { prevMsg.prev_id = 0 } } if (data.next_id) { const nextMsg = state.dialogMsgs.find(({next_id}) => next_id == data.next_id) if (nextMsg) { nextMsg.next_id = 0 } } }); }); }, /** * 获取最新消息 * @param state * @param dispatch * @param dialogIds * @returns {Promise} */ getDialogLatestMsgs({state, dispatch}, dialogIds = []) { return new Promise(function (resolve, reject) { if (state.userId === 0) { reject({msg: 'Parameter error'}); return; } if (!$A.isArray(dialogIds)) { reject({msg: 'Parameter is not array'}); return } if (dialogIds.length === 0) { resolve() return } // const wait = dialogIds.slice(5) const dialogs = dialogIds.slice(0, 5) dispatch("call", { method: 'post', url: 'dialog/msg/latest', data: { dialogs: dialogs.map(id => { return { id, latest_id: state.dialogMsgs.sort((a, b) => { return b.id - a.id }).find(({dialog_id}) => dialog_id == id)?.id || 0 } }), take: state.dialogMsgKeep }, }).then(({data}) => { dispatch("saveDialogMsg", data.data); if (wait.length > 0) { dispatch("getDialogLatestMsgs", wait).then(resolve).catch(reject) } else { resolve() } }).catch(e => { reject(e) }); }) }, /** * 发送已阅消息 * @param state * @param dispatch * @param data */ dialogMsgRead({state, dispatch}, data) { if ($A.isJson(data)) { if (data.userid == state.userId) return; if (data.read_at) return; data.read_at = $A.daytz().format("YYYY-MM-DD HH:mm:ss"); state.readWaitData[data.id] = state.readWaitData[data.id] || 0 // const dialog = state.cacheDialogs.find(({id}) => id == data.dialog_id); if (dialog) { let mark = false if (data.id == dialog.unread_one) { dialog.unread_one = 0 mark = true } if ($A.isArray(dialog.mention_ids)) { const index = dialog.mention_ids.findIndex(id => id == data.id) if (index > -1) { dialog.mention_ids.splice(index, 1) mark = true } } if (mark) { dispatch("saveDialog", dialog) state.readWaitData[data.id] = data.dialog_id } } } clearTimeout(state.readTimeout); state.readTimeout = setTimeout(_ => { state.readTimeout = null; // if (state.userId === 0) { data && (data.read_at = null); return; } const entries = Object.entries(state.readWaitData); if (entries.length === 0) { data && (data.read_at = null); return } const ids = Object.fromEntries(entries.slice(0, 100)); state.readWaitData = Object.fromEntries(entries.slice(100)); // dispatch("call", { method: 'post', url: 'dialog/msg/read', data: { id: ids } }).then(({data}) => { Object.entries(ids) .filter(([_, dialogId]) => /^\d+$/.test(dialogId)) .forEach(([msdId, dialogId]) => { state.dialogMsgs .filter(item => item.dialog_id == dialogId && item.id >= msdId) .forEach(item => { item.read_at = $A.daytz().format("YYYY-MM-DD HH:mm:ss"); }); }); dispatch("saveDialog", data) }).catch(_ => { Object.keys(ids) .forEach(id => { const msg = state.dialogMsgs.find(item => item.id == id); if (msg) msg.read_at = null; }); state.readWaitData = Object.assign(state.readWaitData, ids); }).finally(_ => { state.readLoadNum++ }); }, 50); }, /** * 消息去除点 * @param state * @param dispatch * @param data */ dialogMsgDot({state, dispatch}, data) { if (!$A.isJson(data)) { return; } if (!data.dot) { return; } data.dot = 0; // dispatch("call", { url: 'dialog/msg/dot', data: { id: data.id } }).then(({data}) => { dispatch("saveDialog", data) }); }, /** * 标记已读、未读 * @param state * @param dispatch * @param data * @returns {Promise} */ dialogMsgMark({state, dispatch}, data) { return new Promise((resolve, reject) => { dispatch("call", { url: 'dialog/msg/mark', data, }).then(result => { if (typeof data.after_msg_id !== "undefined") { state.dialogMsgs.some(item => { if (item.dialog_id == data.dialog_id && item.id >= data.after_msg_id) { item.read_at = $A.daytz().format("YYYY-MM-DD HH:mm:ss") } }) } dispatch("saveDialog", result.data) resolve(result) }).catch(e => { reject(e) }) }) }, /** * 消息流订阅 * @param state * @param dispatch * @param streamUrl */ streamMsgSubscribe({state, dispatch}, streamUrl) { if (!/^https?:\/\//i.test(streamUrl)) { streamUrl = $A.mainUrl(streamUrl.substring(1)) } if (state.dialogSseList.find(item => item.streamUrl == streamUrl)) { return } const sse = new SSEClient(streamUrl) sse.subscribe(['append', 'replace', 'done'], (type, e) => { switch (type) { case 'append': case 'replace': const data = $A.jsonParse(e.data); dispatch("streamMsgData", { type, id: e.lastEventId, text: data.content }) break; case 'done': const index = state.dialogSseList.findIndex(item => sse === item.sse) if (index > -1) { state.dialogSseList.splice(index, 1) } sse.unsunscribe() break; } }) state.dialogSseList.push({sse, streamUrl, time: $A.dayjs().unix()}) if (state.dialogSseList.length > 10) { state.dialogSseList.shift().sse.close() } }, /** * 消息流数据 * @param state * @param data */ streamMsgData({state}, data) { $A.syncDispatch("streamMsgData", data) emitter.emit('streamMsgData', data); }, /** * 保存翻译 * @param state * @param data {key, content, language} */ saveTranslation({state}, data) { if (!$A.isJson(data)) { return } const translation = state.cacheTranslations.find(item => item.key == data.key && item.language == data.language) if (translation) { translation.content = data.content } else { const label = languageList[data.language] || data.language state.cacheTranslations.push(Object.assign(data, {label})) } $A.IDBSave("cacheTranslations", state.cacheTranslations.slice(-200)) }, /** * 删除翻译 * @param state * @param key */ removeTranslation({state}, key) { state.cacheTranslations = state.cacheTranslations.filter(item => item.key != key) $A.IDBSave("cacheTranslations", state.cacheTranslations.slice(-200)) }, /** * 设置翻译语言 * @param state * @param language */ setTranslationLanguage({state}, language) { state.cacheTranslationLanguage = language $A.IDBSave('cacheTranslationLanguage', language); }, /** * 设置语音转文字语言 * @param state * @param language */ setTranscriptionLanguage({state}, language) { state.cacheTranscriptionLanguage = language $A.IDBSave('cacheTranscriptionLanguage', language); }, /** *****************************************************************************************/ /** ************************************* loads *********************************************/ /** *****************************************************************************************/ /** * 设置等待 * @param state * @param dispatch * @param key */ setLoad({state, dispatch}, key) { if ($A.isJson(key)) { setTimeout(_ => { dispatch("setLoad", key.key) }, key.delay || 0) return; } const load = state.loads.find(item => item.key == key) if (!load) { state.loads.push({key, num: 1}) } else { load.num++; } }, /** * 取消等待 * @param state * @param key */ cancelLoad({state}, key) { const load = state.loads.find(item => item.key == key) if (!load) { state.loads.push({key, num: -1}) } else { load.num--; } }, /** * 显示全局浮窗加载器 * @param state * @param delay */ showSpinner({state}, delay) { const id = $A.randomString(6) state.floatSpinnerTimer.push({ id, timer: setTimeout(_ => { state.floatSpinnerTimer = state.floatSpinnerTimer.filter(item => item.id !== id) state.floatSpinnerLoad++ }, typeof delay === "number" ? delay : 0) }) }, /** * 隐藏全局浮窗加载器 * @param state * @param dispatch * @param delay */ hiddenSpinner({state, dispatch}, delay) { if (typeof delay === "number") { setTimeout(_ => { dispatch("hiddenSpinner") }, delay) return } const item = state.floatSpinnerTimer.shift() if (item) { clearTimeout(item.timer) } else { state.floatSpinnerLoad-- } }, /** * 预览图片 * @param state * @param data {{index: number | string, list: array} | string} */ previewImage({state}, data) { if (!$A.isJson(data)) { data = {index: 0, list: [data]} } data.list = data.list.map(item => { if ($A.isJson(item)) { item.src = $A.thumbRestore(item.src) } else { item = $A.thumbRestore(item) } return item }) if (typeof data.index === "string") { const current = $A.thumbRestore(data.index) data.index = Math.max(0, data.list.findIndex(item => { return $A.isJson(item) ? item.src == current : item == current })) } state.previewImageIndex = data.index; state.previewImageList = data.list; }, /** * 播放音频 * @param state * @param dispatch * @param src */ audioPlay({state, dispatch}, src) { const old = document.getElementById("__audio_play_element__") if (old) { // 删除已存在 old.pause() old.src = "" old.parentNode.removeChild(old); } if (!src || src === state.audioPlaying) { // 空地址或跟现在播放的地址一致时仅停止 state.audioPlaying = null return } // const audio = document.createElement("audio") audio.id = state.audioPlayId = "__audio_play_element__" audio.controls = false audio.loop = false audio.volume = 1 audio.src = state.audioPlaying = src audio.onended = _ => { dispatch("audioStop", audio.src) } document.body.appendChild(audio) audio.play().then(_ => {}) }, /** * 停止播放音频 * @param state * @param src */ audioStop({state}, src) { const old = document.getElementById("__audio_play_element__") if (!old) { return } if (old.src === src || src === true) { old.pause() old.src = "" old.parentNode.removeChild(old); state.audioPlaying = null } }, /** *****************************************************************************************/ /** *********************************** websocket *******************************************/ /** *****************************************************************************************/ /** * 初始化 websocket * @param state * @param dispatch */ websocketConnection({state, dispatch}) { clearTimeout(state.wsTimeout); if (state.ws) { state.ws.close(); state.ws = null; } if (state.userId === 0) { return; } // if (typeof window.wsInfo === "undefined") { window.wsInfo = { msgCount: 0, repeatCount: 0, lastTime: 0, lastData: null, } } // let url = $A.mainUrl('ws'); url = url.replace("https://", "wss://"); url = url.replace("http://", "ws://"); url += `?action=web&token=${state.userToken}&language=${languageName}`; // const wgLog = $A.openLog; const wsRandom = $A.randomString(16); state.wsRandom = wsRandom; // state.ws = new WebSocket(url); state.ws.onopen = async (e) => { wgLog && console.log("[WS] Open", e, $A.daytz().format("YYYY-MM-DD HH:mm:ss")) state.wsOpenNum++; // if (window.systemInfo.debug === "yes" || state.systemConfig.e2e_message !== 'open') { return // 测试环境不发送加密信息 } dispatch("websocketSend", { type: 'encrypt', data: { type: 'pgp', key: (await dispatch("pgpGetLocalKey")).publicKeyB64 } }) }; state.ws.onclose = async (e) => { wgLog && console.log("[WS] Close", e, $A.daytz().format("YYYY-MM-DD HH:mm:ss")) state.ws = null; // clearTimeout(state.wsTimeout); state.wsTimeout = setTimeout(() => { wsRandom === state.wsRandom && dispatch('websocketConnection'); }, 3000); }; state.ws.onerror = async (e) => { wgLog && console.log("[WS] Error", e, $A.daytz().format("YYYY-MM-DD HH:mm:ss")) state.ws = null; // clearTimeout(state.wsTimeout); state.wsTimeout = setTimeout(() => { wsRandom === state.wsRandom && dispatch('websocketConnection'); }, 3000); }; state.ws.onmessage = async (e) => { if ($A.inArray(state.routeName, ['preload', '404'])) { wgLog && console.log("[WS] Preload", e); return; } // if ($A.dayjs().unix() - window.wsInfo.lastTime < 3 && window.wsInfo.lastData === e.data) { console.log("[WS] Repeat", e, window.wsInfo.repeatCount); window.wsInfo.repeatCount++ return } window.wsInfo.msgCount++ window.wsInfo.lastTime = $A.dayjs().unix() window.wsInfo.lastData = e.data // wgLog && console.log("[WS] Message", e); let result = $A.jsonParse(e.data); if (result.type === "encrypt" && result.encrypted) { result = await dispatch("pgpDecryptApi", result.encrypted) } const msgDetail = $A.formatMsgBasic(result); const {type, msgId} = msgDetail; switch (type) { case "open": $A.setSessionStorage("userWsFd", msgDetail.data.fd) break case "receipt": typeof state.wsCall[msgId] === "function" && state.wsCall[msgId](msgDetail.body, true); delete state.wsCall[msgId]; break case "line": emitter.emit('userActive', {type: 'line', data: msgDetail.data}); break case "msgStream": if ($A.isSubElectron) { return } dispatch("streamMsgSubscribe", msgDetail.stream_url); break default: msgId && dispatch("websocketSend", {type: 'receipt', msgId}).catch(_ => {}); emitter.emit('websocketMsg', msgDetail); if ($A.isSubElectron) { return } switch (type) { /** * 聊天会话消息 */ case "dialog": // 更新会话 (function (msg) { const {mode, silence, data} = msg; const {dialog_id} = data; switch (mode) { case 'delete': // 删除消息 dispatch("forgetDialogMsg", data) // const dialog = state.cacheDialogs.find(({id}) => id == dialog_id); if (dialog) { // 更新最后消息 const newData = { id: dialog_id, last_msg: data.last_msg, last_at: data.last_msg ? data.last_msg.created_at : $A.daytz().format("YYYY-MM-DD HH:mm:ss"), last_force: true, } if (data.update_read) { // 更新未读数量 dispatch("call", { url: 'dialog/msg/unread', data: {dialog_id} }).then(({data}) => { dispatch("saveDialog", Object.assign(newData, data)) }).catch(() => {}); } else { dispatch("saveDialog", newData) } } break; case 'add': case 'chat': const isAdd = mode === "add"; if (!state.dialogMsgs.find(({id}) => id == data.id)) { // 新增任务消息数量 dispatch("increaseTaskMsgNum", {id: data.dialog_id}); // 新增回复数量 dispatch("increaseMsgReplyNum", {id: data.reply_id}); // if (isAdd) { if (data.userid !== state.userId) { // 更新对话新增未读数 const dialog = state.cacheDialogs.find(({id}) => id == dialog_id); if (dialog) { const newData = { id: dialog_id, unread: dialog.unread + 1, mention: dialog.mention, user_at: data.user_at, user_ms: data.user_ms, } if (data.mention) { newData.mention++; } dispatch("saveDialog", newData) } } if (!silence) { emitter.emit('dialogMsgPush', data); } } } const saveMsg = (data, count) => { if (count > 5 || state.dialogMsgs.find(({id}) => id == data.id)) { // 更新消息列表 dispatch("saveDialogMsg", data) // 更新最后消息 isAdd && dispatch("updateDialogLastMsg", data); return; } setTimeout(() => saveMsg(data, count + 1), 50); } saveMsg(data, 0); break; case 'update': case 'readed': const updateMsg = (data, count) => { if (state.dialogMsgs.find(({id}) => id == data.id)) { dispatch("saveDialogMsg", data) // 更新待办 if (typeof data.todo !== "undefined") { dispatch("getDialogTodo", dialog_id) } return; } if (count <= 5) { setTimeout(_ => { updateMsg(data, ++count) }, 500); } } updateMsg(data, 0); break; case 'groupAdd': case 'groupJoin': case 'groupRestore': // 群组添加、加入、恢复 dispatch("getDialogOne", data.id).catch(() => {}) break; case 'groupUpdate': // 群组更新 if (state.cacheDialogs.find(({id}) => id == data.id)) { dispatch("saveDialog", data) } break; case 'groupExit': case 'groupDelete': // 群组退出、解散 dispatch("forgetDialog", data) break; case 'updateTopMsg': // 更新置顶 dispatch("saveDialog", { id: data.dialog_id, top_msg_id: data.top_msg_id, top_userid: data.top_userid }) dispatch("getDialogMsgTop", dialog_id) break; } })(msgDetail); break; /** * 项目消息 */ case "project": (function (msg) { const {action, data} = msg; switch (action) { case 'add': case 'update': case 'recovery': dispatch("saveProject", data) break; case 'detail': dispatch("getProjectOne", data.id).catch(() => {}) dispatch("getTaskForProject", data.id).catch(() => {}) break; case 'delete': case 'archived': dispatch("forgetProject", data); break; case 'sort': dispatch("getTaskForProject", data.id).catch(() => {}) break; } })(msgDetail); break; /** * 任务列表消息 */ case "projectColumn": (function (msg) { const {action, data} = msg; switch (action) { case 'add': case 'update': case 'recovery': dispatch("saveColumn", data) break; case 'delete': dispatch("forgetColumn", data) break; } })(msgDetail); break; /** * 任务消息 */ case "projectTask": (function (msg) { const {action, data} = msg; switch (action) { case 'add': case 'restore': // 恢复(删除) dispatch("addTaskSuccess", data) break; case 'update': case 'archived': // 归档 case 'recovery': // 恢复(归档) dispatch("saveTask", data) break; case 'dialog': dispatch("saveTask", data) dispatch("getDialogOne", data.dialog_id).catch(() => {}) break; case 'upload': dispatch("getTaskFiles", data.task_id) break; case 'filedelete': dispatch("forgetTaskFile", data.id) break; case 'delete': dispatch("forgetTask", data) break; } })(msgDetail); break; /** * 文件消息 */ case "file": (function (msg) { const {action, data} = msg; switch (action) { case 'add': case 'update': dispatch("saveFile", data); break; case 'delete': dispatch("forgetFile", data); break; case 'compress': dispatch("packProgress", data); break; } })(msgDetail); break; /** * 工作报告 */ case "report": (function ({action}) { if (action == 'unreadUpdate') { dispatch("getReportUnread", 1000) } })(msgDetail); break; /** * 流程审批 */ case "approve": (function ({action}) { if (action == 'unread') { dispatch("getApproveUnread", 1000) } })(msgDetail); break; } break } } }, /** * 发送 websocket 消息 * @param state * @param params {type, data, callback} * @returns {Promise} */ websocketSend({state}, params) { return new Promise((resolve, reject) => { if (!$A.isJson(params)) { reject() return } const {type, data, callback} = params let msgId = undefined if (!state.ws) { typeof callback === "function" && callback(null, false) reject() return } if (typeof callback === "function") { msgId = type + '_' + $A.randomString(3) state.wsCall[msgId] = callback } try { state.ws?.send(JSON.stringify({type, msgId, data})) resolve() } catch (e) { typeof callback === "function" && callback(null, false) reject(e) } }) }, /** * 记录 websocket 访问状态 * @param state * @param dispatch * @param path */ websocketPath({state, dispatch}, path) { clearTimeout(state.wsPathTimeout); state.wsPathValue = path; state.wsPathTimeout = setTimeout(() => { if (state.wsPathValue == path) { dispatch("websocketSend", {type: 'path', data: {path}}).catch(_ => {}); } }, 1000); }, /** * 关闭 websocket * @param state */ websocketClose({state}) { if (state.ws) { state.ws.close(); state.ws = null; } }, /** *****************************************************************************************/ /** *************************************** pgp *********************************************/ /** *****************************************************************************************/ /** * 创建密钥对 * @param state * @returns {Promise} */ pgpGenerate({state}) { return new Promise(async resolve => { const data = await openpgp.generateKey({ type: 'ecc', curve: 'curve25519', passphrase: state.clientId, userIDs: [{name: 'doo', email: 'admin@admin.com'}], }) data.publicKeyB64 = $urlSafe(data.publicKey.replace(/\s*-----(BEGIN|END) PGP PUBLIC KEY BLOCK-----\s*/g, '')) resolve(data) }) }, /** * 获取密钥对(不存在自动创建) * @param state * @param dispatch * @returns {Promise} */ pgpGetLocalKey({state, dispatch}) { return new Promise(async resolve => { // 已存在 if (state.localKeyPair.privateKey) { return resolve(state.localKeyPair) } // 避免重复生成 while (state.localKeyLock === true) { await new Promise(r => setTimeout(r, 100)); } if (state.localKeyPair.privateKey) { return resolve(state.localKeyPair) } // 生成密钥对 state.localKeyLock = true state.localKeyPair = await dispatch("pgpGenerate") state.localKeyLock = false resolve(state.localKeyPair) }) }, /** * 加密 * @param state * @param dispatch * @param data {message:any, ?publicKey:string} * @returns {Promise} */ pgpEncrypt({state, dispatch}, data) { return new Promise(async resolve => { if (!$A.isJson(data)) { data = {message: data} } const message = data.message || data.text const publicKeyArmored = data.publicKey || data.key || (await dispatch("pgpGetLocalKey")).publicKey const encryptionKeys = await openpgp.readKey({armoredKey: publicKeyArmored}) // const encrypted = await openpgp.encrypt({ message: await openpgp.createMessage({text: message}), encryptionKeys, }) resolve(encrypted) }) }, /** * 解密 * @param state * @param dispatch * @param data {encrypted:any, ?privateKey:string, ?passphrase:string} * @returns {Promise} */ pgpDecrypt({state, dispatch}, data) { return new Promise(async resolve => { if (!$A.isJson(data)) { data = {encrypted: data} } const encrypted = data.encrypted || data.text const privateKeyArmored = data.privateKey || data.key || (await dispatch("pgpGetLocalKey")).privateKey const decryptionKeys = await openpgp.decryptKey({ privateKey: await openpgp.readPrivateKey({armoredKey: privateKeyArmored}), passphrase: data.passphrase || state.clientId }) // const {data: decryptData} = await openpgp.decrypt({ message: await openpgp.readMessage({armoredMessage: encrypted}), decryptionKeys }) resolve(decryptData) }) }, /** * API加密 * @param state * @param dispatch * @param data * @returns {Promise} */ pgpEncryptApi({state, dispatch}, data) { return new Promise(resolve => { data = $A.jsonStringify(data) dispatch("pgpEncrypt", { message: data, publicKey: state.apiKeyData.key }).then(data => { resolve(data.replace(/\s*-----(BEGIN|END) PGP MESSAGE-----\s*/g, '')) }) }) }, /** * API解密 * @param state * @param dispatch * @param data * @returns {Promise} */ pgpDecryptApi({state, dispatch}, data) { return new Promise(resolve => { dispatch("pgpDecrypt", { encrypted: "-----BEGIN PGP MESSAGE-----\n\n" + data + "\n-----END PGP MESSAGE-----" }).then(data => { resolve($A.jsonParse(data)) }) }) }, /** *****************************************************************************************/ /** *************************************** meeting *********************************************/ /** *****************************************************************************************/ /** * 关闭会议窗口 * @param state * @param type */ closeMeetingWindow({state}, type) { state.meetingWindow = { show: false, type: type, meetingid: 0 }; }, /** * 显示会议窗口 * @param state * @param data */ showMeetingWindow({state}, data) { state.meetingWindow = Object.assign(data, { show: data.type !== 'direct', }); }, /** *****************************************************************************************/ /** ************************************ App Store ******************************************/ /** *****************************************************************************************/ /** * 打开微应用 * @param state * @param data * - id 应用ID(必须) * - name 应用名称(必须) * - url 应用地址(必须) * - url_type 地址类型(可选) * - background 背景颜色(可选) * - capsule 应用胶囊配置(可选) * - transparent 是否透明模式 (true/false),默认 false * - disable_scope_css 是否禁用样式隔离 (true/false),默认 false * - auto_dark_theme 是否自动适配深色主题 (true/false),默认 true * - keep_alive 是否开启微应用保活 (true/false),默认 true * - immersive 是否开启沉浸式模式 (true/false),默认 false * - props 传递参数 * 更多说明详见 https://appstore.dootask.com/development/manual */ async openMicroApp({state}, data) { if (!data || !$A.isJson(data)) { return } if (!data.id || !data.name || !data.url) { return } const serverLocation = new URL($A.mainUrl('')) data.url = data.url .replace(/^\/+/, '') .replace(/^\:(\d+)/ig, (_, port) => { return serverLocation.protocol + '//' + serverLocation.hostname + ':' + port; }) .replace(/\{window[._]location[._](\w+)}/ig, (_, property) => { if (property in serverLocation) { return serverLocation[property]; } }) .replace(/\{system_base_url}/g, serverLocation.origin) const config = { id: data.id, name: data.name, url: $A.mainUrl(data.url), url_type: data.url_type || 'inline', background: data.background || null, capsule: $A.isJson(data.capsule) ? data.capsule : {}, transparent: typeof data.transparent == 'boolean' ? data.transparent : false, disable_scope_css: typeof data.disable_scope_css == 'boolean' ? data.disable_scope_css : false, auto_dark_theme: typeof data.auto_dark_theme == 'boolean' ? data.auto_dark_theme : true, keep_alive: typeof data.keep_alive == 'boolean' ? data.keep_alive : true, immersive: typeof data.immersive == 'boolean' ? data.immersive : false, props: $A.isJson(data.props) ? data.props : {}, } if (!state.microAppsIds.includes(config.id)) { $A.modalWarning(`应用「${config.id}」未安装`); return; } config.url = config.url .replace(/\{user_id}/g, state.userId) .replace(/\{user_nickname}/g, encodeURIComponent(state.userInfo.nickname)) .replace(/\{user_email}/g, encodeURIComponent(state.userInfo.email)) .replace(/\{user_avatar}/g, encodeURIComponent(state.userInfo.userimg)) .replace(/\{user_token}/g, encodeURIComponent(state.userToken)) .replace(/\{system_theme}/g, state.themeName) .replace(/\{system_lang}/g, languageName); emitter.emit('observeMicroApp:open', config); }, /** * 微应用是否已安装 * @param state * @param appName * @returns {Promise} */ isMicroAppInstalled({state}, appName) { return new Promise(resolve => { if (!appName) { resolve(false) return } resolve(!!state.microAppsIds.includes(appName)) }) }, /** * 更新微应用状态(已安装应用、菜单项) * @param commit * @param dispatch */ async updateMicroAppsStatus({commit, state}) { const {data: {code, data}} = await axios.get($A.mainUrl('appstore/api/v1/internal/installed'), { headers: { Token: state.userToken, Language: languageName, } }) if (code === 200) { commit("microApps/data", data|| []) } }, }