Acathur 5 lat temu
commit
ca73d125b9

+ 10 - 0
.editorconfig

@@ -0,0 +1,10 @@
+root = true
+
+[*]
+charset = utf-8
+indent_size = 2
+indent_style = space
+tab_width = 2
+end_of_line = lf
+trim_trailing_whitespace = true
+insert_final_newline = true

+ 18 - 0
.gitignore

@@ -0,0 +1,18 @@
+.DS_Store
+node_modules
+/dist
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 71 - 0
README.md

@@ -0,0 +1,71 @@
+# Proginn Common Library
+
+## Exports
+
+### ProginnBridge
+
+Usage:
+
+```ts
+import { ProginnBridge } from 'proginn-lib'
+
+const bridge = new ProginnBridge(/* opts */)
+```
+
+### ProginnRequest
+
+Usage:
+
+```ts
+import { ProginnRequest } from 'proginn-lib'
+
+const request = ProginnRequest.create({
+  baseURL: 'https://example.com/api',
+  // ...
+})
+
+await request({
+  method: 'GET',
+  url: '/list'
+})
+```
+
+### convertDate
+
+Usage:
+
+```ts
+import { convertDate } from 'proginn-lib'
+
+convertDate(/* timestamp | timestring */)
+```
+
+### formatTime
+
+Mask Pattern:
+
+```txt
+yyyy-MM-dd hh:mm:ss
+```
+
+Usage:
+
+```ts
+import { formatTime } from 'proginn-lib'
+
+formatTime(/* timestamp | timestring | date */, /* mask */)
+```
+
+## Vue components
+
+### DebugWindow
+
+```ts
+import DebugWindow from 'proginn-lib/src/vue/components/DebugWindow.vue'
+
+export default {
+  components: {
+    DebugWindow
+  }
+}
+```

+ 28 - 0
package.json

@@ -0,0 +1,28 @@
+{
+  "name": "proginn-lib",
+  "version": "0.1.0",
+  "description": "Proginn front-end common library.",
+  "main": "dist/index.js",
+  "module": "dist/index.js",
+  "author": "Acathur",
+  "private": true,
+  "scripts": {
+    "build": "rm -rf dist && tsc",
+    "install": "tsc"
+  },
+  "files": [
+    "dist/**/*"
+  ],
+  "dependencies": {
+    "axios": "^0.19.2",
+    "js-cookie": "^2.2.1",
+    "querystring": "^0.2.0"
+  },
+  "devDependencies": {
+    "@types/js-cookie": "^2.2.6",
+    "typescript": "^3.9.7"
+  },
+  "peerDependencies": {
+    "vue": "^2.6.11"
+  }
+}

+ 4 - 0
src/bridge/constant.ts

@@ -0,0 +1,4 @@
+export const COOKIE_ROOT_DOMAIN = 'proginn.com'
+export const COOKIE_APP_KEY = 'x_app'
+export const COOKIE_ACCESS_TOKEN_KEY = 'x_access_token'
+export const MSG_REQUIRE_LOGIN = '请在登录后操作'

+ 145 - 0
src/bridge/index.ts

@@ -0,0 +1,145 @@
+import cookies from 'js-cookie'
+import { COOKIE_ROOT_DOMAIN, COOKIE_APP_KEY, COOKIE_ACCESS_TOKEN_KEY, MSG_REQUIRE_LOGIN } from './constant'
+import { Notifier } from '../types/global'
+
+const parseJWT = (token: string): any => {
+  const payloadStr = token.split('.')[1]
+
+  if (!payloadStr) {
+    return null
+  }
+
+  try {
+    return JSON.parse(atob(payloadStr))
+  } catch (e) {
+    // tslint:disable-next-line
+    console.error('[parseJWT]', e)
+  }
+
+  return null
+}
+
+class ProginnBridge {
+  // @ts-ignore
+  root = window.app_event
+  isAndroid = /Android/.test(window.navigator.userAgent)
+
+  private notifier?: Notifier
+
+  constructor(opts?: {
+    notifier?: Notifier
+  }) {
+    const { notifier } = opts || {}
+
+    this.notifier = notifier
+  }
+
+  get cookie() {
+    return cookies.get()
+  }
+
+  get isInApp() {
+    // @ts-ignore
+    return !!(cookies.get(COOKIE_APP_KEY) || this.root || window.appBridge)
+  }
+
+  get isLogined() {
+    return !!cookies.get(COOKIE_ACCESS_TOKEN_KEY)
+  }
+
+  get uid(): string | null {
+    const token = cookies.get(COOKIE_ACCESS_TOKEN_KEY)
+    const payload = token && parseJWT(token)
+
+    return payload?.uid
+  }
+
+  injectJSObject(name: string, cb: () => void) {
+    // @ts-ignore
+    window.Jishuin = window.Jishuin || {}
+    // @ts-ignore
+    window.Jishuin[name] = cb
+  }
+
+  syncCookies() {
+    const app = cookies.get(COOKIE_APP_KEY)
+    const token = cookies.get(COOKIE_ACCESS_TOKEN_KEY)
+    const opts = {
+      domain: COOKIE_ROOT_DOMAIN,
+      expires: 7200
+    }
+
+    if (app) {
+      cookies.set(COOKIE_APP_KEY, app, opts)
+    }
+
+    if (token) {
+      cookies.set(COOKIE_ACCESS_TOKEN_KEY, token, opts)
+    }
+  }
+
+  invoke(fn: string, payload?: any) {
+    if (!this.root) {
+      // tslint:disable-next-line
+      console.warn(`Bridge invoke ${fn} skipped.`)
+      return
+    }
+
+    if (this.isAndroid) {
+      return this.root[fn] && this.root[fn](payload)
+    } else {
+      return this.root(fn, payload)
+    }
+  }
+
+  back() {
+    if (!this.isInApp) {
+      window.history.back()
+    } else {
+      this.invoke('back_page')
+    }
+  }
+
+  load(url: string) {
+    window.location.href = url
+  }
+
+  login() {
+    if (!this.isInApp) {
+      return
+    }
+
+    this.load('proginn://login?backToPage=true')
+  }
+
+  checkLogin(force = false) {
+    if (force || !this.isLogined) {
+      this.notifier && this.notifier(MSG_REQUIRE_LOGIN)
+      this.login()
+      return false
+    }
+
+    return true
+  }
+
+  userLoad(userInfo: any) {
+    this.invoke('user_load', this.isAndroid ? userInfo : {
+      userInfo
+    })
+  }
+
+  topicLoad(id: string, data: {
+    topic_id: string
+    user_id: string
+    share_content: any
+    topics: any[]
+  }) {
+    this.invoke('topic_load', this.isAndroid ? id : data)
+  }
+
+  setNavigationBarTitle(title: string) {
+    this.invoke('setNavigationBarTitle', title)
+  }
+}
+
+export default ProginnBridge

+ 5 - 0
src/index.ts

@@ -0,0 +1,5 @@
+export { default as ProginnBridge } from './bridge/index'
+export { default as ProginnRequest } from './request/index'
+
+export { convertDate } from './utils/converter'
+export { formatTime } from './utils/formatter'

+ 75 - 0
src/request/index.ts

@@ -0,0 +1,75 @@
+import Axios, { AxiosResponse } from 'axios'
+import qs from 'querystring'
+import ProginnBridge from '../bridge'
+import { Notifier } from '../types/global'
+
+const factory = (opts: {
+  baseURL: string
+  bridge?: ProginnBridge
+  notifier?: Notifier
+}) => {
+  const { baseURL, notifier, bridge } = opts || {}
+  const axios = Axios.create({
+    baseURL,
+    withCredentials: true
+  })
+
+  return async (opts: {
+    method: 'GET' | 'POST'
+    url: string
+    query?: any
+    data?: any
+    dataType?: 'json' | 'form'
+    headers?: any
+  }): Promise<AxiosResponse> => {
+    const {
+      url,
+      method,
+      query,
+      data,
+      dataType = 'form',
+      headers = {}
+    } = opts
+
+    let contentType!: string
+
+    if (dataType === 'form') {
+      contentType = 'application/x-www-form-urlencoded'
+    } else if (dataType === 'json') {
+      contentType = 'application/json'
+    }
+
+    if (method === 'POST') {
+      Object.assign(headers, {
+        'Content-Type': contentType
+      })
+    }
+
+    const res = await axios({
+      url,
+      method,
+      params: query,
+      headers,
+      data: data && (dataType === 'form' ? qs.stringify(data) : JSON.stringify(data))
+    })
+
+    if (res.data) {
+      // require login
+      if (res.data.status === -99) {
+        bridge && bridge.checkLogin(true)
+      } else if (res.data.status !== 1 && res.data.status !== 200) {
+        const message = res.data.data && res.data.data.message || res.data.message || res.data.info
+
+        message && notifier && notifier(message)
+      }
+    }
+
+    return res
+  }
+}
+
+const ProginnRequest = {
+  create: factory
+}
+
+export default ProginnRequest

+ 1 - 0
src/types/global.ts

@@ -0,0 +1 @@
+export type Notifier = (msg: string) => void

+ 9 - 0
src/utils/converter.ts

@@ -0,0 +1,9 @@
+export const convertDate = (t: number | string) => {
+  if (typeof t === 'number') {
+    t = String(t).length === 13 ? t : t * 1000
+  } else if (typeof t === 'string') {
+    t = t.replace(/\s+/g, 'T') + '+08:00'
+  }
+
+  return new Date(t)
+}

+ 31 - 0
src/utils/formatter.ts

@@ -0,0 +1,31 @@
+export const formatTime = (t: Date | number | string, format: string) => {
+  if (typeof t === 'number' || typeof t === 'string') {
+    t = new Date(t)
+  }
+
+  if (!(t instanceof Date)) {
+    return null
+  }
+
+  const o = {
+    'M+': t.getMonth() + 1, 						// month
+    'd+': t.getDate(), 							    // day
+    'h+': t.getHours(), 							// hour
+    'm+': t.getMinutes(), 							// minute
+    's+': t.getSeconds(), 							// second
+    'q+': Math.floor((t.getMonth() + 3) / 3), 		// quarter
+    'S': t.getMilliseconds() 						// millisecond
+  }
+
+  if (/(y+)/.test(format)) {
+    format = format.replace(RegExp.$1, (t.getFullYear() + '').substr(4 - RegExp.$1.length))
+  }
+
+  for (const k in o) {
+    if (new RegExp('(' + k + ')').test(format)) {
+      format = format.replace(RegExp.$1, RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length))
+    }
+  }
+
+  return format
+}

+ 108 - 0
src/vue/components/DebugWindow.vue

@@ -0,0 +1,108 @@
+<template lang="pug">
+  .debug-window(
+    :class="{folded}"
+    @click="folded = !folded"
+    )
+    .content
+      .group.warn(v-if="message")
+        .row Debugging:
+        .row {{message}}
+
+      .group
+        .row InApp: {{bridge.isInApp}}
+        template(v-if="bridge.isInApp")
+          .row logined: {{bridge.isLogined}}
+          .row(v-if="bridge.isLogined") uid: {{bridge.uid}}
+
+      .group
+        .row URL:
+        .row {{url}}
+
+      .group
+        .row User-Agent:
+        .row {{ua}}
+
+      .group
+        .row Cookie:
+        .row(v-if="cookie")
+          .subrow(v-for="(val, key) in bridge.cookie") {{key}}: {{val}}
+        .row(v-else) &lt;no cookie found&gt;
+
+</template>
+
+<script lang="ts">
+// @ts-ignore
+import Vue from 'vue'
+
+export default Vue.extend({
+  name: 'DebugWindow',
+
+  props: {
+    bridge: {
+      type: Object,
+      default: () => {}
+    },
+    message: String
+  },
+
+  data() {
+    return {
+      folded: false,
+      url: window.location.href,
+      ua: window.navigator.userAgent,
+      cookie: document.cookie
+    }
+  }
+})
+</script>
+
+<style lang="less">
+.debug-window {
+  background: rgba(0, 0, 0, 0.7);
+  box-sizing: border-box;
+  max-width: 60%;
+  position: fixed;
+  z-index: 999;
+  right: 4px;
+  top: 4px;
+  transition: all 0.25s ease;
+
+  .content {
+    color: #fff;
+    font-size: 10px;
+    line-height: 1.35;
+    padding: 8px 10px;
+  }
+
+  .group:not(:last-child) {
+    border-bottom: 1px solid rgba(255, 255, 255, 0.12);
+    padding-bottom: 3px;
+    margin-bottom: 4px;
+  }
+
+  .row {
+    padding-bottom: 2px;
+  }
+
+  .subrow {
+    white-space: nowrap;
+  }
+
+  .warn {
+    color: #ff0;
+    word-break: break-all;
+  }
+
+  &.folded {
+    width: 24px;
+    height: 24px;
+    border-radius: 12px;
+    background: rgba(255, 255, 255, 0.6);
+    border: 3px solid rgba(0, 0, 0, 0.6);
+
+    .content {
+      display: none;
+    }
+  }
+}
+</style>

+ 22 - 0
tsconfig.json

@@ -0,0 +1,22 @@
+{
+  "compilerOptions": {
+    "target": "es6",
+    "module": "es6",
+    "moduleResolution": "node",
+    "jsx": "preserve",
+    "strict": true,
+    "esModuleInterop": true,
+    "skipLibCheck": true,
+    "forceConsistentCasingInFileNames": true,
+    "noImplicitAny": false,
+    "outDir": "dist",
+    "rootDir": "src",
+    "declaration": true
+  },
+  "include": [
+    "src"
+  ],
+  "exclude": [
+    "node_modules"
+  ]
+}

+ 49 - 0
yarn.lock

@@ -0,0 +1,49 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@types/js-cookie@^2.2.6":
+  version "2.2.6"
+  resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.6.tgz#f1a1cb35aff47bc5cfb05cb0c441ca91e914c26f"
+  integrity sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw==
+
+axios@^0.19.2:
+  version "0.19.2"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
+  integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
+  dependencies:
+    follow-redirects "1.5.10"
+
+debug@=3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
+  integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
+  dependencies:
+    ms "2.0.0"
+
+follow-redirects@1.5.10:
+  version "1.5.10"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
+  integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
+  dependencies:
+    debug "=3.1.0"
+
+js-cookie@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
+  integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==
+
+ms@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+  integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+
+querystring@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
+  integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
+
+typescript@^3.9.7:
+  version "3.9.7"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa"
+  integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==