diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..791cf51 --- /dev/null +++ b/.env.development @@ -0,0 +1,9 @@ +#mode 环境 +VITE_MODE = dev + +#服务器域名 +VITE_HOST = http://192.168.1.249:8084/ + +# 请求域名 +VITE_APP_BASE_URL='http://192.168.1.249:8084/ai-agent-server/' + diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..9d60707 --- /dev/null +++ b/.env.production @@ -0,0 +1,11 @@ +#mode 环境 +VITE_MODE = production + +#服务器域名 +VITE_HOST = https://test.szxgl.cn/ + +# 请求域名 +VITE_APP_BASE_URL='https://test.szxgl.cn/ai-agent-server/' + +#静态资源目录 +VITE_CDN_DIR = 'https://cdn.xglpa.com/ai-agent-m/' \ No newline at end of file diff --git a/.env.test b/.env.test index d4fb1fa..8e9e414 100644 --- a/.env.test +++ b/.env.test @@ -8,4 +8,4 @@ VITE_HOST = https://test.szxgl.cn/ VITE_APP_BASE_URL='https://test.szxgl.cn/ai-agent-admin-server' #静态资源目录 -VITE_CDN_DIR = 'https://agent-admin-test.szxgl.cn/' \ No newline at end of file +VITE_CDN_DIR = './' \ No newline at end of file diff --git a/.gitignore b/.gitignore index d73bcf6..c3b2f20 100644 --- a/.gitignore +++ b/.gitignore @@ -15,17 +15,5 @@ coverage *.local -/cypress/videos/ -/cypress/screenshots/ # Editor directories and files -.idea -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? -.hbuilderx -# .env -.env.development -.env.production diff --git a/package.json b/package.json index 597aaaf..aee66fc 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "build:quickapp-webview": "uni build -p quickapp-webview", "build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei", "build:quickapp-webview-union": "uni build -p quickapp-webview-union", + "build:prod": "uni build h5 mobile --mode production", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" }, "dependencies": { @@ -106,4 +107,4 @@ "vite": "4.1.4", "weapp-tailwindcss-webpack-plugin": "1.12.8" } -} +} \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index fc93526..71e74da 100644 --- a/src/App.vue +++ b/src/App.vue @@ -9,6 +9,7 @@ import { SHARE_ID, USER_SN } from './enums/constantEnums' import { strToParams } from './utils/util' import cache from './utils/cache' import Cookies from 'js-cookie' +import { client } from '@/utils/client' const appStore = useAppStore() const { getUser } = useUserStore() @@ -73,13 +74,11 @@ const setLocalStorage = async () => { // 写入cookie const redirect = location.href Cookies.set('redirect', redirect) - location.href = `${import.meta.env.VITE_APP_BASE_URL}/qywx/getWxUserByInside?terminal=3` + location.href = `${import.meta.env.VITE_APP_BASE_URL}/qywx/getWxUserByInside?terminal=${client}` return } } else { const obj = { expire: "", value: cookiesToken } - console.log('obj', obj); - console.log('写入前 token', token); localStorage.setItem("app_token", JSON.stringify(obj)); } diff --git a/src/api/user.ts b/src/api/user.ts index c46e6ad..73c9809 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -1,58 +1,66 @@ -import request from '@/utils/request' +import request from "@/utils/request"; export function getUserCenter(header?: any) { - return request.get({ url: '/user/center', header }) + return request.get({ url: "/user/center", header }); } // 个人信息 export function getUserInfo() { - return request.get({ url: '/user/info' }, { isAuth: true }) + return request.get({ url: "/user/info" }, { isAuth: true }); } // 个人编辑 export function userEdit(data: any) { - return request.post({ url: '/user/edit', data }, { isAuth: true }) + return request.post({ url: "/user/edit", data }, { isAuth: true }); } // 绑定手机 export function userBindMobile(data: any, header?: any) { - return request.post({ url: '/login/bindMobile', data, header }, { isAuth: true }) + return request.post( + { url: "/login/bindMobile", data, header }, + { isAuth: true } + ); } // 微信电话 export function userMnpMobile(data: any, header?: any) { - return request.post({ url: '/user/mnpMobile', data, header }, { isAuth: true }) + return request.post( + { url: "/user/mnpMobile", data, header }, + { isAuth: true } + ); } export function userChangePwd(data: any) { - return request.post({ url: '/user/changePwd', data }, { isAuth: true }) + console.log("data", data); + + return request.post({ url: "/user/changePwd", data }, { isAuth: true }); } // 绑定小程序 export function mnpAuthBind(data: any) { - return request.post({ url: '/user/bindMnp', data }) + return request.post({ url: "/user/bindMnp", data }); } // 绑定公众号 export function oaAuthBind(data: any) { - return request.post({ url: '/user/bindOa', data }) + return request.post({ url: "/user/bindOa", data }); } //更新微信小程序头像昵称 export function updateUser(data: Record, header: any) { - return request.post({ url: '/user/updateUser', data, header }) + return request.post({ url: "/user/updateUser", data, header }); } //余额明细 export function accountLog(data: any) { - return request.get({ url: '/logs/userMoney', data }) + return request.get({ url: "/logs/userMoney", data }); } //一键反馈 export function feedbackPost(data: any) { - return request.post({ url: '/feedback/add', data }) + return request.post({ url: "/feedback/add", data }); } //注销账号 export function cancelled(data?: any) { - return request.post({ url: '/login/cancelled', data }) -} \ No newline at end of file + return request.post({ url: "/login/cancelled", data }); +} diff --git a/src/components/floating-menu/floating-menu.vue b/src/components/floating-menu/floating-menu.vue index 47030ec..d462ef0 100644 --- a/src/components/floating-menu/floating-menu.vue +++ b/src/components/floating-menu/floating-menu.vue @@ -1,47 +1,26 @@ diff --git a/src/packages/pages/change_password/change_password.vue b/src/packages/pages/change_password/change_password.vue index c933cdc..ce1276a 100644 --- a/src/packages/pages/change_password/change_password.vue +++ b/src/packages/pages/change_password/change_password.vue @@ -1,38 +1,21 @@ - + 其他登录方式
-
+
微信登录
-
+
手机号登录
-
+
邮箱登录
- - + + @@ -88,7 +66,7 @@ import { mobileLogin, accountLogin, emailLogin, - uninAppLogin, + uninAppLogin, mnpLogin as mnpLoginApi } from '@/api/account' import { updateUser } from '@/api/user' @@ -201,7 +179,7 @@ const loginHandle = async () => { router.switchTab(cache.get(BACK_URL)) } } else { - router.reLaunch('/pages/index/index') + router.reLaunch('/') } cache.remove(BACK_URL) } @@ -230,7 +208,7 @@ const loginAfter = (() => { cache.remove(SHARE_ID) cache.remove(USER_SN) } - } catch (error) {} + } catch (error) { } } const updateUsers = async () => { if (loginData.value.isNew && !showLoginPopup.value) { @@ -242,7 +220,7 @@ const loginAfter = (() => { }, { token: loginData.value.token } ) - } catch (error) {} + } catch (error) { } } else if (showLoginPopup.value) { return Promise.reject() } @@ -266,10 +244,10 @@ const { lockFn: wxLoginLock, isLock } = useLockFn(async () => { // #ifdef MP-WEIXIN data = await mnpLogin() // #endif - - // #ifdef APP-PLUS - data = await appLogin() - // #endif + + // #ifdef APP-PLUS + data = await appLogin() + // #endif if (data) { loginData.value = data diff --git a/src/stores/user.ts b/src/stores/user.ts index afaffb1..1064775 100644 --- a/src/stores/user.ts +++ b/src/stores/user.ts @@ -1,43 +1,43 @@ -import { getUserCenter } from '@/api/user' -import { TOKEN_KEY } from '@/enums/constantEnums' -import { useSharedId } from '@/hooks/useShareMessage' -import { getToken } from '@/utils/auth' -import cache from '@/utils/cache' -import { defineStore } from 'pinia' +import { getUserCenter } from "@/api/user"; +import { TOKEN_KEY } from "@/enums/constantEnums"; +import { useSharedId } from "@/hooks/useShareMessage"; +import { getToken } from "@/utils/auth"; +import cache from "@/utils/cache"; +import { defineStore } from "pinia"; interface UserSate { - userInfo: Record - token: string | null - client: string | null - temToken: string | null + userInfo: Record; + token: string | null; + client: string | null; + temToken: string | null; } export const useUserStore = defineStore({ - id: 'userStore', - state: (): UserSate => ({ - userInfo: {}, - token: getToken() || null, - client: null, - temToken: null - }), - getters: { - isLogin: (state) => !!state.token + id: "userStore", + state: (): UserSate => ({ + userInfo: {}, + token: getToken() || null, + client: null, + temToken: null, + }), + getters: { + isLogin: (state) => !!state.token, + }, + actions: { + async getUser() { + const data = await getUserCenter({ + token: this.token, + }); + this.userInfo = data; }, - actions: { - async getUser() { - const data = await getUserCenter({ - token: this.token - }) - this.userInfo = data - }, - login(token: string) { - cache.set(TOKEN_KEY, token) - this.token = token - useSharedId() - }, - logout() { - this.token = '' - this.userInfo = {} - cache.remove(TOKEN_KEY) - } - } -}) + login(token: string) { + cache.set(TOKEN_KEY, token); + this.token = token; + useSharedId(); + }, + logout() { + this.token = ""; + this.userInfo = {}; + cache.remove(TOKEN_KEY); + }, + }, +}); diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 3c7403b..868f9ef 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -1,6 +1,20 @@ -import { TOKEN_KEY } from '@/enums/cacheEnums' -import cache from './cache' +import { TOKEN_KEY } from "@/enums/cacheEnums"; +import cache from "./cache"; +import Cookies from "js-cookie"; + +// 判断是否为企微环境 +const isWechatWork = () => { + const ua = navigator.userAgent.toLowerCase(); + return ua.includes("micromessenger") && ua.includes("wxwork"); +}; export function getToken() { - return cache.get(TOKEN_KEY) || '' + if (isWechatWork()) { + const token = Cookies.get("token"); + const obj = { expire: "", value: token }; + localStorage.setItem("app_token", JSON.stringify(obj)); + return token || ""; + } else { + return cache.get(TOKEN_KEY) || ""; + } } diff --git a/src/utils/request/index.ts b/src/utils/request/index.ts index 061ca9c..57b8168 100644 --- a/src/utils/request/index.ts +++ b/src/utils/request/index.ts @@ -1,137 +1,138 @@ -import HttpRequest from './http' -import { merge } from 'lodash-es' +import HttpRequest from "./http"; +import { merge } from "lodash-es"; import { - HttpRequestOptions, - RequestHooks, - RequestConfig, - RequestEventStreamConfig, - RequestOptions, - UploadFileOption -} from './type' -import { getToken } from '../auth' -import { RequestCodeEnum, RequestMethodsEnum } from '@/enums/requestEnums' -import { useUserStore } from '@/stores/user' -import { client } from '../client' -import router from '@/router' -import appConfig from '@/config' + HttpRequestOptions, + RequestHooks, + RequestConfig, + RequestEventStreamConfig, + RequestOptions, + UploadFileOption, +} from "./type"; +import { getToken } from "../auth"; +import { RequestCodeEnum, RequestMethodsEnum } from "@/enums/requestEnums"; +import { useUserStore } from "@/stores/user"; +import { client } from "../client"; +import router from "@/router"; +import appConfig from "@/config"; +import Cookies from "js-cookie"; export type { - RequestConfig, - RequestEventStreamConfig, - RequestOptions, - UploadFileOption -} + RequestConfig, + RequestEventStreamConfig, + RequestOptions, + UploadFileOption, +}; const requestHooks: RequestHooks = { - requestInterceptorsHook(options, config) { - const { urlPrefix, baseUrl, withToken } = config - options.header = options.header ?? {} - if (urlPrefix) { - options.url = `${urlPrefix}${options.url}` - } - if (baseUrl) { - options.url = `${baseUrl}${options.url}` - } - //#ifndef APP-PLUS - const token = useUserStore().token || null - //#endif - //#ifdef APP-PLUS - const token = useUserStore().token || 'null' - //#endif - // 添加token - if (withToken) { - options.header['ai-token'] = options.header.token || token - } - // 添加终端类型 - options.header['terminal'] = useUserStore().client || client - delete options.header.token - options.header.version = appConfig.version - return options - }, - async responseInterceptorsHook(response, config, options) { - const { isTransformResponse, isReturnDefaultResponse, isAuth } = config - - //返回默认响应,当需要获取响应头及其他数据时可使用 - if (isReturnDefaultResponse) { - return response - } - // 是否需要对数据进行处理 - if (!isTransformResponse) { - return response.data - } - const { logout } = useUserStore() - const { code, data, msg, show } = response.data as any - switch (code) { - case RequestCodeEnum.SUCCESS: - return data - case RequestCodeEnum.PARAMS_TYPE_ERROR: - case RequestCodeEnum.PARAMS_VALID_ERROR: - case RequestCodeEnum.REQUEST_METHOD_ERROR: - case RequestCodeEnum.ASSERT_ARGUMENT_ERROR: - case RequestCodeEnum.ASSERT_MYBATIS_ERROR: - case RequestCodeEnum.LOGIN_ACCOUNT_ERROR: - case RequestCodeEnum.LOGIN_DISABLE_ERROR: - case RequestCodeEnum.NO_PERMISSTION: - case RequestCodeEnum.FAILED: - case RequestCodeEnum.SYSTEM_ERROR: - case RequestCodeEnum.DRAW_ERROR: - uni.$u.toast(msg) - return Promise.reject(msg) - case RequestCodeEnum.REQUEST_404_ERROR: - uni.$u.toast(msg) - return Promise.reject(msg) - - case RequestCodeEnum.TOKEN_INVALID: - logout() - if (isAuth && options.method?.toUpperCase() !== 'GET') { - router.navigateTo({ path: '/pages/login/login' }) - } - return Promise.reject(msg) - case RequestCodeEnum.TOKEN_EMPTY: - logout() - if (isAuth && !getToken()) { - router.navigateTo('/pages/login/login') - } - return Promise.reject() - - default: - return data - } - }, - async responseInterceptorsCatchHook(options, error) { - if (options.method?.toUpperCase() == RequestMethodsEnum.POST) { - uni.$u.toast('请求失败,请重试') - } - return Promise.reject(error) + requestInterceptorsHook(options, config) { + const { urlPrefix, baseUrl, withToken } = config; + options.header = options.header ?? {}; + if (urlPrefix) { + options.url = `${urlPrefix}${options.url}`; } -} + if (baseUrl) { + options.url = `${baseUrl}${options.url}`; + } + //#ifndef APP-PLUS + const token = useUserStore().token || null; + //#endif + //#ifdef APP-PLUS + const token = useUserStore().token || "null"; + //#endif + // 添加token + if (withToken) { + options.header["ai-token"] = options.header.token || token; + } + // 添加终端类型 + options.header["terminal"] = useUserStore().client || client; + delete options.header.token; + options.header.version = appConfig.version; + return options; + }, + async responseInterceptorsHook(response, config, options) { + const { isTransformResponse, isReturnDefaultResponse, isAuth } = config; + + //返回默认响应,当需要获取响应头及其他数据时可使用 + if (isReturnDefaultResponse) { + return response; + } + // 是否需要对数据进行处理 + if (!isTransformResponse) { + return response.data; + } + const { logout } = useUserStore(); + const { code, data, msg, show } = response.data as any; + switch (code) { + case RequestCodeEnum.SUCCESS: + return data; + case RequestCodeEnum.PARAMS_TYPE_ERROR: + case RequestCodeEnum.PARAMS_VALID_ERROR: + case RequestCodeEnum.REQUEST_METHOD_ERROR: + case RequestCodeEnum.ASSERT_ARGUMENT_ERROR: + case RequestCodeEnum.ASSERT_MYBATIS_ERROR: + case RequestCodeEnum.LOGIN_ACCOUNT_ERROR: + case RequestCodeEnum.LOGIN_DISABLE_ERROR: + case RequestCodeEnum.NO_PERMISSTION: + case RequestCodeEnum.FAILED: + case RequestCodeEnum.SYSTEM_ERROR: + case RequestCodeEnum.DRAW_ERROR: + uni.$u.toast(msg); + return Promise.reject(msg); + case RequestCodeEnum.REQUEST_404_ERROR: + uni.$u.toast(msg); + return Promise.reject(msg); + + case RequestCodeEnum.TOKEN_INVALID: + logout(); + if (isAuth && options.method?.toUpperCase() !== "GET") { + router.navigateTo({ path: "/pages/login/login" }); + } + return Promise.reject(msg); + case RequestCodeEnum.TOKEN_EMPTY: + logout(); + if (isAuth && !getToken()) { + router.navigateTo("/pages/login/login"); + } + return Promise.reject(); + + default: + return data; + } + }, + async responseInterceptorsCatchHook(options, error) { + if (options.method?.toUpperCase() == RequestMethodsEnum.POST) { + uni.$u.toast("请求失败,请重试"); + } + return Promise.reject(error); + }, +}; const defaultOptions: HttpRequestOptions = { - requestOptions: { - timeout: appConfig.timeout - }, - baseUrl: appConfig.baseUrl, - //是否返回默认的响应 - isReturnDefaultResponse: false, - // 需要对返回数据进行处理 - isTransformResponse: true, - // 接口拼接地址 - urlPrefix: appConfig.urlPrefix, - // 忽略重复请求 - ignoreCancel: false, - // 是否携带token - withToken: true, - isAuth: false, - retryCount: 2, - retryTimeout: 1000, - requestHooks: requestHooks -} + requestOptions: { + timeout: appConfig.timeout, + }, + baseUrl: appConfig.baseUrl, + //是否返回默认的响应 + isReturnDefaultResponse: false, + // 需要对返回数据进行处理 + isTransformResponse: true, + // 接口拼接地址 + urlPrefix: appConfig.urlPrefix, + // 忽略重复请求 + ignoreCancel: false, + // 是否携带token + withToken: true, + isAuth: false, + retryCount: 2, + retryTimeout: 1000, + requestHooks: requestHooks, +}; function createRequest(opt?: HttpRequestOptions) { - return new HttpRequest( - // 深度合并 - merge(defaultOptions, opt || {}) - ) + return new HttpRequest( + // 深度合并 + merge(defaultOptions, opt || {}) + ); } -const request = createRequest() -export default request +const request = createRequest(); +export default request; diff --git a/src/utils/request/type.d.ts b/src/utils/request/type.d.ts index d976a00..3c91f0e 100644 --- a/src/utils/request/type.d.ts +++ b/src/utils/request/type.d.ts @@ -1,42 +1,45 @@ -export type RequestOptions = UniApp.RequestOptions +export type RequestOptions = UniApp.RequestOptions; export type ResponseResult = - | UniApp.RequestSuccessCallbackResult - | UniApp.UploadFileSuccessCallbackResult -export type RequestOptionsResponseError = UniApp.GeneralCallbackResult -export type RequestTask = UniApp.RequestTask -export type UploadFileOption = UniApp.UploadFileOption + | UniApp.RequestSuccessCallbackResult + | UniApp.UploadFileSuccessCallbackResult; +export type RequestOptionsResponseError = UniApp.GeneralCallbackResult; +export type RequestTask = UniApp.RequestTask; +export type UploadFileOption = UniApp.UploadFileOption; export interface HttpRequestOptions extends RequestConfig { - requestOptions: Partial + requestOptions: Partial; } export interface RequestConfig { - baseUrl: string - requestHooks: RequestHooks - isReturnDefaultResponse: boolean - isTransformResponse: boolean - urlPrefix: string - ignoreCancel: boolean - withToken: boolean - isAuth: boolean - retryCount: number - retryTimeout: number - hasRetryCount?: number - //上传文件独有 - onProgress?: (progress: number) => void + baseUrl: string; + requestHooks: RequestHooks; + isReturnDefaultResponse: boolean; + isTransformResponse: boolean; + urlPrefix: string; + ignoreCancel: boolean; + withToken: boolean; + isAuth: boolean; + retryCount: number; + retryTimeout: number; + hasRetryCount?: number; + //上传文件独有 + onProgress?: (progress: number) => void; } export interface RequestEventStreamConfig extends Partial { - onstart?: (event: AbortController | UniApp.RequestTask) => void - onmessage?: (value: string) => void - onclose?: () => void + onstart?: (event: AbortController | UniApp.RequestTask) => void; + onmessage?: (value: string) => void; + onclose?: () => void; } export interface RequestHooks { - requestInterceptorsHook?(options: RequestOptions, config: RequestConfig): RequestOptions - responseInterceptorsHook?( - response: ResponseResult, - config: RequestConfig, - options: RequestOptions - ): any - responseInterceptorsCatchHook?(options: RequestOptions, error: any): any + requestInterceptorsHook?( + options: RequestOptions, + config: RequestConfig + ): RequestOptions; + responseInterceptorsHook?( + response: ResponseResult, + config: RequestConfig, + options: RequestOptions + ): any; + responseInterceptorsCatchHook?(options: RequestOptions, error: any): any; } diff --git a/vite.config.ts b/vite.config.ts index 24629c3..b2a98e5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,43 +1,61 @@ -import { defineConfig } from 'vite' -import uni from '@dcloudio/vite-plugin-uni' -import tailwindcss from 'tailwindcss' -import autoprefixer from 'autoprefixer' -import postcssRemToResponsivePixel from 'postcss-rem-to-responsive-pixel' -import postcssWeappTailwindcssRename from 'weapp-tailwindcss-webpack-plugin/postcss' -import vwt from 'weapp-tailwindcss-webpack-plugin/vite' -import uniRouter from 'unplugin-uni-router/vite' +import { defineConfig, loadEnv } from "vite"; +import uni from "@dcloudio/vite-plugin-uni"; +import tailwindcss from "tailwindcss"; +import autoprefixer from "autoprefixer"; +import postcssRemToResponsivePixel from "postcss-rem-to-responsive-pixel"; +import postcssWeappTailwindcssRename from "weapp-tailwindcss-webpack-plugin/postcss"; +import vwt from "weapp-tailwindcss-webpack-plugin/vite"; +import uniRouter from "unplugin-uni-router/vite"; -const isH5 = process.env.UNI_PLATFORM === 'h5' -const isApp = process.env.UNI_PLATFORM === 'app' -const weappTailwindcssDisabled = isH5 || isApp +const isH5 = process.env.UNI_PLATFORM === "h5"; +const isApp = process.env.UNI_PLATFORM === "app"; +const weappTailwindcssDisabled = isH5 || isApp; -const postcssPlugin = [autoprefixer(), tailwindcss()] +const postcssPlugin = [autoprefixer(), tailwindcss()]; if (!weappTailwindcssDisabled) { - postcssPlugin.push( - postcssRemToResponsivePixel({ - rootValue: 32, - propList: ['*'], - transformUnit: 'rpx' - }) - ) - postcssPlugin.push(postcssWeappTailwindcssRename()) + postcssPlugin.push( + postcssRemToResponsivePixel({ + rootValue: 32, + propList: ["*"], + transformUnit: "rpx", + }) + ); + postcssPlugin.push(postcssWeappTailwindcssRename()); } // https://vitejs.dev/config/ -export default defineConfig({ +export default defineConfig(({ command, mode }) => { + console.log("mode:", mode); + const env = loadEnv(mode, process.cwd(), ""); + return { plugins: [ - uni(), - uniRouter({ - includes: ['style'] - }), - weappTailwindcssDisabled ? undefined : vwt() + uni(), + uniRouter({ + includes: ["style"], + }), + weappTailwindcssDisabled ? undefined : vwt(), ], css: { - postcss: { - plugins: postcssPlugin - } + postcss: { + plugins: postcssPlugin, + }, }, server: { - port: 8991 - } -}) + port: 8991, + }, + base: mode != "dev" ? env.VITE_CDN_DIR : "./", + build: { + assetsDir: "static", // 静态资源存放目录(默认是 assets) + rollupOptions: { + output: { + // 代码分割配置 + manualChunks: (id) => { + if (id.includes("node_modules")) { + return "vendor"; + } + }, + }, + }, + }, + }; +});