提交 85b6307c 作者: 方治民

合并分支 '3.x' 到 'main'

3.x

查看合并请求 !57
......@@ -68,5 +68,8 @@ module.exports = {
],
'vue/multi-word-component-names': 'off',
'no-console': 'off',
'@stylistic/ts/indent': 'off',
'@stylistic/ts/brace-style': 'off',
'@stylistic/js/operator-linebreak': 'off',
},
}
......@@ -66,64 +66,64 @@
}
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-3081220230817001",
"@dcloudio/uni-app-plus": "3.0.0-3081220230817001",
"@dcloudio/uni-components": "3.0.0-3081220230817001",
"@dcloudio/uni-h5": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-alipay": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-baidu": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-jd": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-lark": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-qq": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-toutiao": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-weixin": "3.0.0-3081220230817001",
"@dcloudio/uni-quickapp-webview": "3.0.0-3081220230817001",
"@dcloudio/uni-app": "3.0.0-alpha-3090120230927001",
"@dcloudio/uni-app-plus": "3.0.0-alpha-3090120230927001",
"@dcloudio/uni-components": "3.0.0-alpha-3090120230927001",
"@dcloudio/uni-h5": "3.0.0-alpha-3090120230927001",
"@dcloudio/uni-mp-alipay": "3.0.0-alpha-3090120230927001",
"@dcloudio/uni-mp-baidu": "3.0.0-alpha-3090120230927001",
"@dcloudio/uni-mp-jd": "3.0.0-alpha-3090120230927001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-alpha-3090120230927001",
"@dcloudio/uni-mp-lark": "3.0.0-alpha-3090120230927001",
"@dcloudio/uni-mp-qq": "3.0.0-alpha-3090120230927001",
"@dcloudio/uni-mp-toutiao": "3.0.0-alpha-3090120230927001",
"@dcloudio/uni-mp-weixin": "3.0.0-alpha-3090120230927001",
"@dcloudio/uni-quickapp-webview": "3.0.0-alpha-3090120230927001",
"@dcloudio/uni-ui": "^1.4.28",
"@faker-js/faker": "^8.0.2",
"@faker-js/faker": "^8.1.0",
"@vue/runtime-core": "~3.2.47",
"@vueuse/core": "^10.4.1",
"axios": "^1.5.0",
"@vueuse/core": "^10.5.0",
"axios": "^1.5.1",
"crypto-js": "^4.1.1",
"dayjs": "^1.11.9",
"dayjs": "^1.11.10",
"lodash-es": "^4.17.21",
"nanoid": "^4.0.2",
"nanoid": "^5.0.1",
"pinia": "~2.0.36",
"qs": "~6.9.7",
"stompjs": "^2.3.3",
"urijs": "^1.19.11",
"vue": "~3.2.47",
"vue-i18n": "^9.2.2",
"vue-i18n": "^9.5.0",
"vue-request": "^2.0.3",
"vue-types": "^5.1.1"
},
"devDependencies": {
"@antfu/eslint-config": "^0.41.0",
"@commitlint/cli": "^17.7.1",
"@antfu/eslint-config": "^0.43.1",
"@commitlint/cli": "^17.7.2",
"@commitlint/config-conventional": "^17.7.0",
"@dcloudio/types": "^3.3.3",
"@dcloudio/uni-automator": "3.0.0-3081220230817001",
"@dcloudio/uni-cli-shared": "3.0.0-3081220230817001",
"@dcloudio/types": "^3.4.0",
"@dcloudio/uni-automator": "3.0.0-alpha-3090120230927001",
"@dcloudio/uni-cli-shared": "3.0.0-alpha-3090120230927001",
"@dcloudio/uni-helper-json": "^1.0.13",
"@dcloudio/uni-stacktracey": "3.0.0-3081220230817001",
"@dcloudio/vite-plugin-uni": "3.0.0-3081220230817001",
"@iconify/json": "^2.2.108",
"@types/crypto-js": "^4.1.1",
"@dcloudio/uni-stacktracey": "3.0.0-alpha-3090120230927001",
"@dcloudio/vite-plugin-uni": "3.0.0-alpha-3090120230927001",
"@iconify/json": "^2.2.125",
"@types/crypto-js": "^4.1.2",
"@types/lodash-es": "^4.17.9",
"@types/node": "^20.5.7",
"@types/node": "^20.8.3",
"@types/qs": "^6.9.8",
"@types/stompjs": "^2.3.5",
"@types/stompjs": "^2.3.6",
"@types/urijs": "^1.19.20",
"@typescript-eslint/eslint-plugin": "^6.5.0",
"@typescript-eslint/parser": "^6.5.0",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"commitizen": "^4.3.0",
"conventional-changelog-cli": "^4.0.0",
"conventional-changelog-cli": "^4.1.0",
"cz-conventional-changelog": "^3.3.0",
"cz-customizable": "^7.0.0",
"cz-git": "^1.7.1",
"czg": "^1.7.1",
"dotenv": "^16.3.1",
"eslint": "^8.48.0",
"eslint": "^8.51.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.17.0",
......@@ -135,24 +135,24 @@
"npm-run-all": "^4.1.5",
"picocolors": "^1.0.0",
"pont-engine": "^1.5.12",
"postcss": "^8.4.29",
"postcss": "^8.4.31",
"postcss-html": "^1.5.0",
"postcss-less": "^6.0.0",
"prettier": "^3.0.3",
"rimraf": "^5.0.1",
"sass": "^1.66.1",
"sort-package-json": "^2.5.1",
"rimraf": "^5.0.5",
"sass": "^1.69.0",
"sort-package-json": "^2.6.0",
"stylelint": "^15.10.3",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recommended": "^13.0.0",
"stylelint-config-standard": "^34.0.0",
"stylelint-order": "^6.0.3",
"typescript": "~5.1.6",
"unocss": "^0.55.3",
"unocss-preset-weapp": "^0.55.2",
"unocss": "^0.56.5",
"unocss-preset-weapp": "^0.56.0",
"unplugin-auto-import": "^0.16.6",
"unplugin-vue-components": "^0.25.2",
"vite": "^4.4.9",
"vite": "^4.4.11",
"vue-eslint-parser": "^9.3.1"
},
"engines": {
......
This source diff could not be displayed because it is too large. You can view the blob instead.
<script setup lang="ts">
import { checkUpgrade } from '@/utils/upgrade'
// import * as Push from '@/utils/push'
onLaunch(() => {
console.log('App Launch')
// 版本更新检查
checkUpgrade()
// 清除消息角标
// Push.setBadge(0)
......
......@@ -46,8 +46,8 @@ export const Message = {
},
/**
* 消息确认提示
* @param title 标题
* @param content 提示内容
* @param title 标题
* @param confirmText 确认按钮文字
* @param showCancel 是否显示取消按钮
* @returns Promise<boolean> 是否点击了确认按钮
......
import { convertKB } from '@/utils/file'
/**
* 图片 Hash 缓存标识前缀
*/
export const CACHE_PREFIX = 'G_CACHE_IMAGE__'
/**
* 计算缓存大小
* @returns 字节
*/
export function calculateCacheSize() {
const storage = uni.getStorageInfoSync()
return storage.keys
.filter((key) => key.startsWith(CACHE_PREFIX))
.reduce((total, key) => {
try {
const cache = uni.getStorageSync(key)
const result = JSON.parse(cache)
total += result.size
} catch (_) {}
return total
}, 0)
}
/**
* 计算缓存大小格式化
* @returns 格式化后的缓存大小
* @example 1.2MB
* @see {@link calculateCacheSize}
* @see {@link convertKB}
* @see {@link CACHE_PREFIX}
*/
export function calculateCacheSizeFormat() {
const cacheSize = calculateCacheSize()
const { size, unit } = convertKB(cacheSize)
return `${size}${unit}`
}
/**
* 清理缓存
*/
export function cleanCache() {
const storage = uni.getStorageInfoSync()
const promises = storage.keys
.filter((key) => key.startsWith(CACHE_PREFIX))
.map((key) => {
return new Promise((resolve, reject) => {
try {
const cache = uni.getStorageSync(key)
const result = JSON.parse(cache)
uni.removeSavedFile({
filePath: result.url,
complete: resolve,
})
} catch (e) {
console.error(e)
reject(e)
} finally {
uni.removeStorageSync(key)
}
})
})
return Promise.allSettled(promises)
}
<script setup lang="ts">
import md5 from 'crypto-js/md5'
import { CACHE_PREFIX } from './index'
const props = defineProps({
width: {
......@@ -60,27 +61,53 @@
// 如果是网络图片,则缓存到本地
if (url.startsWith('http')) {
// #ifdef APP-PLUS
const hash = md5(url).toString()
hashCacheKey.value = `G_CACHE_IMAGE_${hash}`
if (uni.getStorageSync(hashCacheKey.value)) {
const hash = md5(url).toString().toUpperCase()
hashCacheKey.value = `${CACHE_PREFIX}${hash}`
const cache = uni.getStorageSync(hashCacheKey.value)
if (cache) {
log('cache hit', url)
url = uni.getStorageSync(hashCacheKey.value)
try {
const result = JSON.parse(cache)
url = result.url
} catch (_) {
url = cache
}
} else {
log('cache miss', url)
try {
const res = await uni.downloadFile({ url })
if (res.statusCode === 200) {
const { savedFilePath } = await new Promise<UniApp.SaveFileSuccess>((resolve, reject) => {
const { savedFilePath, size } = await new Promise<{ savedFilePath: string; size: number }>(
(resolve, reject) => {
uni.saveFile({
tempFilePath: res.tempFilePath,
success: (res) => resolve(res),
success: (res) => {
const filePath = res.savedFilePath
uni.getFileInfo({
filePath,
success: (res) =>
resolve({
savedFilePath: filePath,
size: res.size,
}),
fail: (err) => reject(err),
})
},
fail: (err) => reject(err),
})
},
)
url = `${savedFilePath}`
// 缓存图片本地地址
uni.setStorageSync(hashCacheKey.value, url)
uni.setStorageSync(
hashCacheKey.value,
JSON.stringify({
url,
size,
timestamp: Date.now(),
}),
)
} else {
log('cache error', url, res)
url = props.src
......@@ -97,6 +124,7 @@
}
const hasError = ref(false)
function onError() {
if (hasError.value) {
return
......
// token key
export const AREA_CODE = 'AREA__'
export const TOKEN_KEY = 'TOKEN__'
export const LOCALE_KEY = 'LOCALE__'
......@@ -12,6 +14,21 @@ export const ROLES_KEY = 'ROLES__KEY__'
// project config key
export const PROJ_CFG_KEY = 'PROJ__CFG__KEY__'
/**
* 推送客户端标识
*/
export const PUSH_CLIENT_KEY = 'PUSH__CLIENT__KEY__'
/**
* APP UUID
*/
export const APP_UUID_KEY = 'APP__UUID__KEY__'
/**
* 用户ID
*/
export const USER_ID_KEY = 'USER__ID__KEY__'
export enum CacheTypeEnum {
SESSION,
LOCAL,
......
import { APP_UUID_KEY } from '/@/enums/cacheEnum'
import { nanoid } from 'nanoid'
import md5 from 'crypto-js/md5'
interface DeviceInfo {
uuid: string
vendor: string
model: string
}
export interface Runtime {
app: Ref<PlusRuntimeWidgetInfo>
getDeviceInfo: () => Promise<DeviceInfo>
}
export function useRuntime(): Runtime {
......@@ -14,7 +25,48 @@ export function useRuntime(): Runtime {
// #endif
})
/**
* 获取设备信息
* 注意: 需要在页面的 onReady 后使用
* @returns 设备信息
*/
function getDeviceInfo(): Promise<DeviceInfo> {
return new Promise((resolve, reject) => {
// #ifdef APP-PLUS
plus.device.getInfo({
success: resolve,
fail: reject,
})
// #endif
// #ifndef APP-PLUS
let uuid = uni.getStorageSync(APP_UUID_KEY)
if (!uuid) {
uuid = md5(nanoid()).toString()
uni.setStorageSync(APP_UUID_KEY, uuid)
}
const result = { uuid, vendor: 'unknown', model: 'unknown' }
const userAgent = navigator.userAgent
const deviceInfo = userAgent.match(/\((.*?)\)/)
if (deviceInfo && deviceInfo.length > 1) {
const info = deviceInfo[1].split(';')
if (/android/i.test(userAgent)) {
if (info.length >= 3) {
result.vendor = info[1].trim()
result.model = info[2].trim()
}
} else {
result.vendor = info[0].trim()
result.model = info[1].trim()
}
}
resolve(result)
// #endif
})
}
return {
app,
getDeviceInfo,
}
}
import type { App } from 'vue'
import type { I18n, I18nOptions } from 'vue-i18n'
import { createI18n } from 'vue-i18n'
import dayjs from 'dayjs'
import dayjsLocalizedFormat from 'dayjs/plugin/localizedFormat'
import dayjsZh from 'dayjs/locale/zh-cn'
import langs from './lang'
dayjs.extend(dayjsLocalizedFormat)
dayjs.locale(dayjsZh)
// eslint-disable-next-line import/no-mutable-exports
export let i18n: ReturnType<typeof createI18n>
......
......@@ -2,10 +2,11 @@
"name" : "Beta App",
"appid" : "__UNI__2E9441A",
"description" : "APP 基础工程",
"versionName" : "3.0.3.0",
"versionCode" : 112,
"versionName" : "1.0.0",
"versionCode" : 10000,
"transformPx" : false,
"locale" : "zh-Hans",
"vueVersion" : "3",
/* 5+App特有相关 */// 配置文件详细说明
// https://uniapp.dcloud.net.cn/collocation/manifest-app.html#full-manifest
"app-plus" : {
......@@ -13,10 +14,9 @@
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
"alwaysShowBeforeRender" : false,
"autoclose" : false,
"waiting" : true
},
"screenOrientation" : [ "portrait-primary", "landscape-primary" ],
"compatible" : {
......@@ -24,7 +24,10 @@
"ignoreVersion" : true
},
/* 模块配置 */
"modules" : {},
"modules" : {
"Geolocation" : {},
"Camera" : {}
},
/* 应用发布信息 */
"distribute" : {
/* android打包配置 */
......@@ -45,19 +48,30 @@
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
],
"permissionPhoneState" : {
// app首次启动关闭权限申请
"request" : "none"
}
},
/* ios打包配置 */
"ios" : {
"dSYMs" : false
"dSYMs" : false,
"privacyDescription" : {
"NSUserTrackingUsageDescription" : "请放心,开启权限不会获取您在其他站点的隐私信息,该权限仅用于标识设备并保障服务安全与提示浏览体验",
"NSLocationAlwaysAndWhenInUseUsageDescription" : "该应用需要你的地理位置,以便检查你当前位置信息",
"NSLocationAlwaysUsageDescription" : "该应用需要你的地理位置,以便检查你当前位置信息",
"NSLocationWhenInUseUsageDescription" : "该应用需要你的地理位置,以便检查你当前位置信息",
"NSPhotoLibraryUsageDescription" : "请允许访问您的照片图库,以便能够上传应用异常问题的截图"
}
},
/* SDK配置 */
"sdkConfigs" : {
"ad" : {},
"push" : {
"unipush" : {
"version" : "2",
"offline" : false
"statics" : {},
"geolocation" : {
"system" : {
"__platform__" : [ "ios", "android" ]
}
}
},
......@@ -92,9 +106,19 @@
"spotlight@3x" : "unpackage/res/icons/120x120.png"
}
}
},
"splashscreen" : {
"useOriginalMsgbox" : true
}
},
"uniStatistics" : {
"enable" : true
}
},
"uniStatistics" : {
"enable" : true,
"version" : "2"
},
/* 快应用特有相关 */
"quickapp" : {},
/* 小程序特有相关 */
......@@ -113,10 +137,5 @@
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : true,
"version" : "2"
},
"vueVersion" : "3"
}
}
......@@ -8,11 +8,29 @@
}
},
// ================================ 通用页面分割线 ====================================
// === 关于我们 ===
{
"path": "pages/common/about/index",
"style": {
"navigationBarTitleText": "关于我们",
"backgroundColorTop": "#FFFFFF",
"backgroundColorBottom": "#FFFFFF"
}
},
// === 问题反馈 ===
{
"path": "pages/common/feedback/index",
"style": {
"navigationBarTitleText": "问题反馈",
"backgroundColorTop": "#FFFFFF",
"backgroundColorBottom": "#FFFFFF"
}
},
// === Webview ===
{
"path": "pages/common/webview/index",
"style": {
"titleNView": true,
"navigationBarTitleText": ""
}
},
......@@ -20,7 +38,6 @@
{
"path": "pages/common/viewer/pdf",
"style": {
"titleNView": true,
"navigationBarTitleText": ""
}
},
......@@ -32,13 +49,8 @@
"titleNView": false,
// #endif
"navigationBarTitleText": "开发中",
"enablePullDownRefresh": false,
"backgroundColorTop": "#FFFFFF",
"backgroundColorBottom": "#FFFFFF",
"backgroundColor": "#FFFFFE",
"mp-alipay": {
"allowsBounceVertical": "YES"
}
"backgroundColorBottom": "#FFFFFF"
}
},
// === 版本更新 ===
......
<script setup lang="ts">
import dayjs from 'dayjs'
import Navigate from '@/utils/page/navigate'
const startYear = ref(2005)
const currentYear = ref(dayjs().year())
const { privacy, services, copyright, company } = Navigate.links
</script>
<template>
<view class="wrap">
<view class="logoBox">
<image class="logo" src="/static/logo.png" />
<view class="text-#555 font-bold p-2">{{ $app.name }}</view>
</view>
<fui-footer isFixed>
<template #text>
<view class="links">
<text class="underline" @tap="Navigate.webview(services, '服务协议')">服务协议</text>
<text class="underline" @tap="Navigate.webview(privacy, '隐私政策')">隐私政策</text>
</view>
<view class="mt-1">Copyright © {{ startYear }}-{{ currentYear }} {{ copyright.name }}</view>
<view class="mt-2" v-if="copyright.name !== company.name">
技术支持:
<fui-link underline color="#b2b2b2" size="24" :href="company.link" :text="company.name" />
</view>
</template>
</fui-footer>
</view>
</template>
<style lang="less" scoped>
.wrap {
padding: 0 46rpx;
padding-top: 100rpx;
font-size: 32rpx;
height: 100vh;
background-color: white;
box-sizing: border-box;
}
.logoBox {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
.logo {
width: 160rpx;
height: 160rpx;
box-shadow: 0 0 15rpx #d7d7d7;
border-radius: 20rpx;
}
.logo_text {
width: 126rpx;
height: 40rpx;
margin-top: 20rpx;
}
}
.links {
padding: 10rpx;
* {
padding: 10rpx 20rpx;
}
}
</style>
<script lang="ts" setup>
import { USER_ID_KEY } from '/@/enums/cacheEnum'
const db = uniCloud.database()
const dbCollectionName = 'opendb-feedback'
const model = reactive({
userId: uni.getStorageSync(USER_ID_KEY),
content: '',
mobile: '',
imgs: [],
loading: false,
})
function submit() {
Message.loading('提交中...')
// 使用 clientDB 提交数据
db.collection(dbCollectionName)
.add({
user_id: model.userId,
content: model.content,
mobile: model.mobile,
imgs: model.imgs,
})
.then(() => {
Message.toast('提交成功')
setTimeout(() => uni.navigateBack(), 500)
})
.catch((err) => {
uni.showModal({
content: err.message || '请求服务失败',
showCancel: false,
})
})
.finally(() => {
uni.hideLoading()
})
}
</script>
<template>
<view class="feedback-body">
<text class="text-black">问题反馈和意见建议 <text style="color: red">*</text></text>
<textarea
placeholder="请描述您遇到的问题或对本产品的建议..."
v-model="model.content"
class="feedback-textare"
maxlength="-1"
></textarea>
<view class="image-title">
<text class="text-black">上传问题截图<text class="text-grey"> (选填,最多可上传6张) </text> </text>
<view class="text-grey">{{ model.imgs.length }}/6</view>
</view>
<view class="filepicker">
<uni-file-picker file-mediatype="image" :limit="6" return-type="array" v-model="model.imgs" />
</view>
<text class="text-black">联系方式<text class="text-grey">(选填)</text> </text>
<input class="feedback-input" v-model="model.mobile" placeholder="请输入您的手机号" />
<view class="btn">
<fui-button
:disabled="!model.content || model.loading"
:loading="model.loading"
type="primary"
@tap="submit"
>
提 交
</fui-button>
<!-- 安全区 -->
<fui-safe-area />
</view>
</view>
</template>
<style lang="scss" scoped>
.text-black {
color: #303133;
font-size: 32rpx;
}
.text-grey {
color: #bcbcbc;
font-size: 24rpx;
margin-left: 15rpx;
}
.feedback-quick {
padding-right: 10rpx;
color: #606266;
font-size: 32rpx;
}
.feedback-body {
padding: 30rpx;
background-color: white;
min-height: 100vh;
box-sizing: border-box;
}
.feedback-textare {
margin-top: 30rpx;
margin-bottom: 30rpx;
height: 266rpx;
color: #303133;
font-size: 28rpx;
line-height: 2em;
width: 100%;
box-sizing: border-box;
padding: 20rpx 30rpx;
border-radius: 20rpx;
background-color: #f5f6f8;
}
.feedback-input {
font-size: 28rpx;
color: #303133;
background-color: #f5f6f8;
border-radius: 20rpx;
height: 100rpx;
min-height: 100rpx;
padding: 0 30rpx;
margin-top: 30rpx;
margin-bottom: 60rpx;
}
.btn-submit {
border-radius: 20rpx;
color: #ffffff;
margin-top: 100rpx;
background-color: #007aff;
margin-bottom: 70rpx;
}
.image-title {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
color: #606266;
}
.filepicker {
margin-top: 30rpx;
margin-bottom: 30rpx;
}
.btn {
position: fixed;
bottom: 0;
left: 0;
right: 0;
margin: 30rpx;
}
</style>
<script lang="ts" setup>
import URI from 'urijs'
const page = reactive({
security: false,
title: '',
link: '',
styles: {
......@@ -10,10 +13,16 @@
},
})
onLoad(({ title, link }) => {
page.title = title ? decodeURI(title) : ''
onLoad(({ title, link, sub }) => {
page.title = title ? decodeURIComponent(title) : ''
page.link = decodeURIComponent(link)
// 嵌入页面进行二级子页面跳转时,参数会被多次 encode
if (sub) {
page.title = decodeURIComponent(page.title)
page.link = decodeURIComponent(page.link)
}
if (!link) {
Message.toast('页面打开失败,参数错误')
uni.navigateBack()
......@@ -26,11 +35,52 @@
title: page.title,
})
}
// #ifdef APP-PLUS
// 白名单同源检测
if (isSameOrigin(new URI(page.link))) {
page.security = true
} else {
// 对于不安全或不信任的网站地址或可能触发 plus API 的来源,采用手动创建 webview 的方式打开
const { windowHeight, statusBarHeight } = uni.getSystemInfoSync()
const webview = plus.webview.create(page.link, 'no-security-webview', {
plusrequire: 'none',
progress: page.styles.progress,
top: `${statusBarHeight + 44}px`,
height: `${windowHeight}px`,
})
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const currentWebview = currentPage.$getAppWebview()
currentWebview.setStyle({ progress: page.styles.progress })
currentWebview.append(webview)
}
// #endif
// #ifndef APP-PLUS
page.security = true
// #endif
})
// 安全域白名单,防止 APP 环境的意外注入和非法安全调用 plus API 的问题
const allows = ['yiring.com']
// 在安全域白名单中,但仍然需要排除的链接(通常是代理的网站链接)
const denyLinks = []
function isSameOrigin(url: URI) {
if (denyLinks.includes(url.href())) {
return false
}
return allows.some((item) => {
const regex = new RegExp(`^(.*\\.)?${item}$`, 'i')
return regex.test(url.hostname())
})
}
</script>
<template>
<view class="wrap" v-if="page.link">
<template v-if="page.security">
<web-view :src="page.link" :update-title="!page.title" :webview-styles="page.styles" />
</template>
</view>
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import { checkUpgrade } from '@/utils/upgrade'
import { checkUpgrade, closeSplashscreenAndChechUpgrade } from '@/utils/upgrade'
import { useRuntime } from '@/hooks/app/useRuntime'
import { useConcealedExit } from '@/hooks/app/useConcealedExit'
......@@ -10,7 +10,10 @@
const title = ref('Hello World')
const version = computed(() => app.value.version)
onMounted(() => {
onLoad(() => {
// 关闭启动页并检查更新
closeSplashscreenAndChechUpgrade()
// test API
API.example.hello
.request()
......
import { defineStore } from 'pinia'
import { store } from '/@/store'
import { getAuthCache, setAuthCache } from '@/utils/auth'
import { TOKEN_KEY, USER_INFO_KEY } from '@/enums/cacheEnum'
import type { defs } from '@/api/services'
import Navigate from '@/utils/page/navigate'
export type UserInfo = Partial<defs.UserInfo>
interface UserState {
userInfo: Nullable<UserInfo>
token?: string
loading?: boolean
}
export const useUserStore = defineStore({
id: 'app-user',
state: (): UserState => ({
token: null,
userInfo: null,
loading: false,
}),
getters: {
getToken(): Nullable<string> {
if (this.token) {
return this.token
}
const token = getAuthCache(TOKEN_KEY)
if (token) {
this.token = token
return token
}
},
getUserInfo(): Nullable<UserInfo> {
if (this.userInfo) {
return this.userInfo
}
const info = getAuthCache(USER_INFO_KEY)
if (info) {
const userInfo = JSON.parse(info) as UserInfo
this.userInfo = userInfo
return userInfo
}
return null
},
/**
* @returns 是否为审核账号
*/
isAuditMode(): boolean {
return this.getUserInfo?.mobile === '10000000001'
},
},
actions: {
setToken(token: string) {
this.token = token
setAuthCache(TOKEN_KEY, token)
},
setUserInfo(info: UserInfo) {
this.userInfo = info
setAuthCache(USER_INFO_KEY, info ? JSON.stringify(info) : null)
},
async logout() {
if (this.loading) {
return
}
this.loading = true
try {
await API.auth.logout.request()
} finally {
this.setToken(null)
this.setUserInfo(null)
await Navigate.to('/pages/login/login')
this.loading = false
}
},
async refreshUserInfo() {
if (this.loading) {
return
}
this.loading = true
try {
const userInfo = await API.userView.getUserInfo.request()
this.setUserInfo(userInfo)
} finally {
this.loading = false
}
},
},
})
// Need to be used outside the setup
export function useUserStoreWithOut() {
return useUserStore(store)
}
{
"debug": false,
"redis": false,
"cachetime": 604800,
"sessionExpireTime": 1800,
"realtimeStat": true,
"cronMin": false,
"cron": [
{
"type": "stat",
"time": "* * * 0"
},
{
"type": "stat",
"time": "* * 1 10"
},
{
"type": "stat",
"time": "1 * 1 20"
},
{
"type": "stat",
"time": "* 1 3 30"
},
{
"type": "active-device",
"time": "* * 0 10"
},
{
"type": "active-user",
"time": "* * 0 20"
},
{
"type": "page",
"time": "* * 3 20"
},
{
"type": "event",
"time": "* * 4 20"
},
{
"type": "error",
"time": "* * 5 20"
},
{
"type": "loyalty",
"time": "* * 6 20"
},
{
"type": "clean",
"time": "* * 5 30"
},
{
"type": "retention-device",
"time": "* * 2 20"
},
{
"type": "retention-device",
"time": "* 1 4 30"
},
{
"type": "retention-device",
"time": "1 * 2 30"
},
{
"type": "retention-user",
"time": "* * 3 40"
},
{
"type": "retention-user",
"time": "* 1 5 40"
},
{
"type": "retention-user",
"time": "1 * 6 30"
},
{
"type": "pay-result",
"time": "* * * 10",
"dimension": "hour",
"description": "每小时执行统计(会自动统计小时、天、周、月、季度、年度)",
"timely": true
}
],
"batchInsertNum": 5000,
"errorCheck": {
"needCheck": true,
"checkTime": 5
},
"cleanLog": {
"open": true,
"reserveDays": {
"sessionLog": 31,
"userSessionLog": 31,
"pageLog": 7,
"eventLog": 7,
"shareLog": 7,
"errorLog": 7
}
}
}
## 1.0.16(2023-04-25)
- 新增maxTokenLength配置,用于限制数据库用户记录token数组的最大长度
## 1.0.15(2023-04-06)
- 修复部分语言国际化出错的Bug
## 1.0.14(2023-03-07)
- 修复 admin用户包含其他角色时未包含在token的Bug
## 1.0.13(2022-07-21)
......
{
"displayName": "uni-id-common",
"version": "1.0.14",
"version": "1.0.16",
"description": "包含uni-id token生成、校验、刷新功能的云函数公共模块",
"keywords": [
"uni-id-common",
......
"use strict";var e,t=(e=require("crypto"))&&"object"==typeof e&&"default"in e?e.default:e;const n={TOKEN_EXPIRED:"uni-id-token-expired",CHECK_TOKEN_FAILED:"uni-id-check-token-failed",PARAM_REQUIRED:"uni-id-param-required",ACCOUNT_EXISTS:"uni-id-account-exists",ACCOUNT_NOT_EXISTS:"uni-id-account-not-exists",ACCOUNT_CONFLICT:"uni-id-account-conflict",ACCOUNT_BANNED:"uni-id-account-banned",ACCOUNT_AUDITING:"uni-id-account-auditing",ACCOUNT_AUDIT_FAILED:"uni-id-account-audit-failed",ACCOUNT_CLOSED:"uni-id-account-closed"};function i(e){return!!e&&("object"==typeof e||"function"==typeof e)&&"function"==typeof e.then}function r(e){if(!e)return;const t=e.match(/^(\d+).(\d+).(\d+)/);return t?t.slice(1,4).map(e=>parseInt(e)):void 0}function o(e,t){const n=r(e),i=r(t);return n?i?function(e,t){const n=Math.max(e.length,t.length);for(let i=0;i<n;i++){const n=e[i],r=t[i];if(n>r)return 1;if(n<r)return-1}return 0}(n,i):1:i?-1:0}const s={"uni-id-token-expired":30203,"uni-id-check-token-failed":30202};function c(e){const{errCode:t,errMsgValue:n}=e;e.errMsg=this._t(t,n),t in s&&(e.code=s[t]),delete e.errMsgValue}function a(e){return"object"===(i=e,Object.prototype.toString.call(i).slice(8,-1).toLowerCase())&&e.errCode&&(t=e.errCode,Object.values(n).includes(t))&&!!e.errCode;var t,i}let u={"zh-Hans":{"uni-id-token-expired":"登录状态失效,token已过期","uni-id-check-token-failed":"token校验未通过","uni-id-param-required":"缺少参数: {param}","uni-id-account-exists":"此账号已注册","uni-id-account-not-exists":"此账号未注册","uni-id-account-conflict":"用户账号冲突","uni-id-account-banned":"从账号已封禁","uni-id-account-auditing":"此账号正在审核中","uni-id-account-audit-failed":"此账号审核失败","uni-id-account-closed":"此账号已注销"},en:{"uni-id-token-expired":"The login status is invalid, token has expired","uni-id-check-token-failed":"Check token failed","uni-id-param-required":"Parameter required: {param}","uni-id-account-exists":"Account exists","uni-id-account-not-exists":"Account does not exists","uni-id-account-conflict":"User account conflict","uni-id-account-banned":"Account has been banned","uni-id-account-auditing":"Account audit in progress","uni-id-account-audit-failed":"Account audit failed","uni-id-account-closed":"Account has been closed"}};try{const e=require.resolve("uni-config-center/uni-id/lang/index.js");u=function(e,t){const n=Object.keys(e);n.push(...Object.keys(t));const i={};for(let r=0;r<n.length;r++){const o=n[r];i[o]=Object.assign({},e[o],t[o])}return i}(u,require(e))}catch(e){}var d=u;function l(e){return e.replace(/=/g,"").replace(/\+/g,"-").replace(/\//g,"_")}function h(e){return JSON.parse((t=function(e){var t=4-(e=e.toString()).length%4;if(4!==t)for(var n=0;n<t;++n)e+="=";return e.replace(/-/g,"+").replace(/_/g,"/")}(e),Buffer.from(t,"base64").toString("utf-8")));var t}function f(e){return l((t=JSON.stringify(e),Buffer.from(t,"utf-8").toString("base64")));var t}function p(e,n){return l(t.createHmac("sha256",n).update(e).digest("base64"))}const k=function(e,t){if("string"!=typeof e)throw new Error("Invalid token");const n=e.split(".");if(3!==n.length)throw new Error("Invalid token");const[i,r,o]=n;if(p(i+"."+r,t)!==o)throw new Error("Invalid token");const s=h(i);if("HS256"!==s.alg||"JWT"!==s.typ)throw new Error("Invalid token");const c=h(r);if(1e3*c.exp<Date.now()){const e=new Error("Token expired");throw e.name="TokenExpiredError",e}return c},g=function(e,t,n={}){const{expiresIn:i}=n;if(!i)throw new Error("expiresIn is required");const r=parseInt(Date.now()/1e3),o={...e,iat:r,exp:r+n.expiresIn},s=f({alg:"HS256",typ:"JWT"})+"."+f(o);return s+"."+p(s,t)},I=uniCloud.database(),_=I.command,C=I.collection("uni-id-users"),m=I.collection("uni-id-roles");class T{constructor({uniId:e}={}){this.uid=null,this.userRecord=null,this.userPermission=null,this.oldToken=null,this.oldTokenPayload=null,this.uniId=e,this.config=this.uniId._getConfig(),this.clientInfo=this.uniId._clientInfo,this.checkConfig()}checkConfig(){const{tokenExpiresIn:e,tokenExpiresThreshold:t}=this.config;if(t>e)throw new Error("Config error, tokenExpiresThreshold should be less than tokenExpiresIn")}get customToken(){return this.uniId.interceptorMap.get("customToken")}isTokenInDb(e){return o(e,"1.0.10")>=0}async getUserRecord(){if(this.userRecord)return this.userRecord;const e=await C.doc(this.uid).get();if(this.userRecord=e.data[0],!this.userRecord)throw{errCode:n.ACCOUNT_NOT_EXISTS};switch(this.userRecord.status){case void 0:case 0:break;case 1:throw{errCode:n.ACCOUNT_BANNED};case 2:throw{errCode:n.ACCOUNT_AUDITING};case 3:throw{errCode:n.ACCOUNT_AUDIT_FAILED};case 4:throw{errCode:n.ACCOUNT_CLOSED}}if(this.oldTokenPayload){if(this.isTokenInDb(this.oldTokenPayload.uniIdVersion)){if(-1===(this.userRecord.token||[]).indexOf(this.oldToken))throw{errCode:n.CHECK_TOKEN_FAILED}}if(this.userRecord.valid_token_date&&this.userRecord.valid_token_date>1e3*this.oldTokenPayload.iat)throw{errCode:n.TOKEN_EXPIRED}}return this.userRecord}async updateUserRecord(e){await C.doc(this.uid).update(e)}async getUserPermission(){if(this.userPermission)return this.userPermission;const e=(await this.getUserRecord()).role||[];if(0===e.length)return this.userPermission={role:[],permission:[]},this.userPermission;if(e.includes("admin"))return this.userPermission={role:e,permission:[]},this.userPermission;const t=await m.where({role_id:_.in(e)}).get(),n=(i=t.data.reduce((e,t)=>(t.permission&&e.push(...t.permission),e),[]),Array.from(new Set(i)));var i;return this.userPermission={role:e,permission:n},this.userPermission}async _createToken({uid:e,role:t,permission:i}={}){if(!t||!i){const e=await this.getUserPermission();t=e.role,i=e.permission}let r={uid:e,role:t,permission:i};if(this.uniId.interceptorMap.has("customToken")){const n=this.uniId.interceptorMap.get("customToken");if("function"!=typeof n)throw new Error("Invalid custom token file");r=await n({uid:e,role:t,permission:i})}const o=Date.now(),{tokenSecret:s,tokenExpiresIn:c}=this.config,a=g({...r,uniIdVersion:"1.0.14"},s,{expiresIn:c}),u=await this.getUserRecord(),d=(u.token||[]).filter(e=>{try{const t=this._checkToken(e);if(u.valid_token_date&&u.valid_token_date>1e3*t.iat)return!1}catch(e){if(e.errCode===n.TOKEN_EXPIRED)return!1}return!0});return d.push(a),await this.updateUserRecord({last_login_ip:this.clientInfo.clientIP,last_login_date:o,token:d}),{token:a,tokenExpired:o+1e3*c}}async createToken({uid:e,role:t,permission:i}={}){if(!e)throw{errCode:n.PARAM_REQUIRED,errMsgValue:{param:"uid"}};this.uid=e;const{token:r,tokenExpired:o}=await this._createToken({uid:e,role:t,permission:i});return{errCode:0,token:r,tokenExpired:o}}async refreshToken({token:e}={}){if(!e)throw{errCode:n.PARAM_REQUIRED,errMsgValue:{param:"token"}};this.oldToken=e;const t=this._checkToken(e);this.uid=t.uid,this.oldTokenPayload=t;const{uid:i}=t,{role:r,permission:o}=await this.getUserPermission(),{token:s,tokenExpired:c}=await this._createToken({uid:i,role:r,permission:o});return{errCode:0,token:s,tokenExpired:c}}_checkToken(e){const{tokenSecret:t}=this.config;let i;try{i=k(e,t)}catch(e){if("TokenExpiredError"===e.name)throw{errCode:n.TOKEN_EXPIRED};throw{errCode:n.CHECK_TOKEN_FAILED}}return i}async checkToken(e,{autoRefresh:t=!0}={}){if(!e)throw{errCode:n.PARAM_REQUIRED,errMsgValue:{param:"token"}};this.oldToken=e;const i=this._checkToken(e);this.uid=i.uid,this.oldTokenPayload=i;const{tokenExpiresThreshold:r}=this.config,{uid:o,role:s,permission:c}=i,a={role:s,permission:c};if(!s&&!c){const{role:e,permission:t}=await this.getUserPermission();a.role=e,a.permission=t}if(!r||!t){const e={code:0,errCode:0,...i,...a};return delete e.uniIdVersion,e}const u=Date.now();let d={};1e3*i.exp-u<1e3*r&&(d=await this._createToken({uid:o}));const l={code:0,errCode:0,...i,...a,...d};return delete l.uniIdVersion,l}}var E=Object.freeze({__proto__:null,checkToken:async function(e,{autoRefresh:t=!0}={}){return new T({uniId:this}).checkToken(e,{autoRefresh:t})},createToken:async function({uid:e,role:t,permission:n}={}){return new T({uniId:this}).createToken({uid:e,role:t,permission:n})},refreshToken:async function({token:e}={}){return new T({uniId:this}).refreshToken({token:e})}});const w=require("uni-config-center")({pluginId:"uni-id"});class A{constructor({context:e,clientInfo:t,config:n}={}){this._clientInfo=e?function(e){return{appId:e.APPID,platform:e.PLATFORM,locale:e.LOCALE,clientIP:e.CLIENTIP,deviceId:e.DEVICEID}}(e):t,this.config=n||this._getOriginConfig(),this.interceptorMap=new Map,w.hasFile("custom-token.js")&&this.setInterceptor("customToken",require(w.resolve("custom-token.js"))),this._i18n=uniCloud.initI18n({locale:this._clientInfo.locale,fallbackLocale:"zh-Hans",messages:d})}setInterceptor(e,t){this.interceptorMap.set(e,t)}_t(...e){return this._i18n.t(...e)}_parseOriginConfig(e){return Array.isArray(e)?e:e[0]?Object.values(e):e}_getOriginConfig(){if(w.hasFile("config.json")){let e;try{e=w.config()}catch(e){throw new Error("Invalid uni-id config file\n"+e.message)}return this._parseOriginConfig(e)}try{return this._parseOriginConfig(require("uni-id/config.json"))}catch(e){throw new Error("Invalid uni-id config file")}}_getAppConfig(){const e=this._getOriginConfig();return Array.isArray(e)?e.find(e=>e.dcloudAppid===this._clientInfo.appId)||e.find(e=>e.isDefaultConfig):e}_getPlatformConfig(){const e=this._getAppConfig();if(!e)throw new Error(`Config for current app (${this._clientInfo.appId}) was not found, please check your config file or client appId`);let t;switch("app-plus"===this._clientInfo.platform&&(this._clientInfo.platform="app"),"h5"===this._clientInfo.platform&&(this._clientInfo.platform="web"),this._clientInfo.platform){case"web":t="h5";break;case"app":t="app-plus"}const n=[{tokenExpiresIn:7200,tokenExpiresThreshold:1200,passwordErrorLimit:6,passwordErrorRetryTime:3600},e];t&&e[t]&&n.push(e[t]),n.push(e[this._clientInfo.platform]);const i=Object.assign(...n);return["tokenSecret","tokenExpiresIn"].forEach(e=>{if(!i||!i[e])throw new Error(`Config parameter missing, ${e} is required`)}),i}_getConfig(){return this._getPlatformConfig()}}for(const e in E)A.prototype[e]=E[e];function y(e){const t=new A(e);return new Proxy(t,{get(e,t){if(t in e&&0!==t.indexOf("_")){if("function"==typeof e[t])return(n=e[t],function(){let e;try{e=n.apply(this,arguments)}catch(e){if(a(e))return c.call(this,e),e;throw e}return i(e)?e.then(e=>(a(e)&&c.call(this,e),e),e=>{if(a(e))return c.call(this,e),e;throw e}):(a(e)&&c.call(this,e),e)}).bind(e);if("context"!==t&&"config"!==t)return e[t]}var n}})}A.prototype.createInstance=y;const x={createInstance:y};module.exports=x;
"use strict";var e,t=(e=require("crypto"))&&"object"==typeof e&&"default"in e?e.default:e;const n={TOKEN_EXPIRED:"uni-id-token-expired",CHECK_TOKEN_FAILED:"uni-id-check-token-failed",PARAM_REQUIRED:"uni-id-param-required",ACCOUNT_EXISTS:"uni-id-account-exists",ACCOUNT_NOT_EXISTS:"uni-id-account-not-exists",ACCOUNT_CONFLICT:"uni-id-account-conflict",ACCOUNT_BANNED:"uni-id-account-banned",ACCOUNT_AUDITING:"uni-id-account-auditing",ACCOUNT_AUDIT_FAILED:"uni-id-account-audit-failed",ACCOUNT_CLOSED:"uni-id-account-closed"};function i(e){return!!e&&("object"==typeof e||"function"==typeof e)&&"function"==typeof e.then}function r(e){if(!e)return;const t=e.match(/^(\d+).(\d+).(\d+)/);return t?t.slice(1,4).map(e=>parseInt(e)):void 0}function o(e,t){const n=r(e),i=r(t);return n?i?function(e,t){const n=Math.max(e.length,t.length);for(let i=0;i<n;i++){const n=e[i],r=t[i];if(n>r)return 1;if(n<r)return-1}return 0}(n,i):1:i?-1:0}const s={"uni-id-token-expired":30203,"uni-id-check-token-failed":30202};function c(e){const{errCode:t,errMsgValue:n}=e;e.errMsg=this._t(t,n),t in s&&(e.code=s[t]),delete e.errMsgValue}function a(e){return"object"===(i=e,Object.prototype.toString.call(i).slice(8,-1).toLowerCase())&&e.errCode&&(t=e.errCode,Object.values(n).includes(t))&&!!e.errCode;var t,i}let u={"zh-Hans":{"uni-id-token-expired":"登录状态失效,token已过期","uni-id-check-token-failed":"token校验未通过","uni-id-param-required":"缺少参数: {param}","uni-id-account-exists":"此账号已注册","uni-id-account-not-exists":"此账号未注册","uni-id-account-conflict":"用户账号冲突","uni-id-account-banned":"从账号已封禁","uni-id-account-auditing":"此账号正在审核中","uni-id-account-audit-failed":"此账号审核失败","uni-id-account-closed":"此账号已注销"},en:{"uni-id-token-expired":"The login status is invalid, token has expired","uni-id-check-token-failed":"Check token failed","uni-id-param-required":"Parameter required: {param}","uni-id-account-exists":"Account exists","uni-id-account-not-exists":"Account does not exists","uni-id-account-conflict":"User account conflict","uni-id-account-banned":"Account has been banned","uni-id-account-auditing":"Account audit in progress","uni-id-account-audit-failed":"Account audit failed","uni-id-account-closed":"Account has been closed"}};try{const e=require.resolve("uni-config-center/uni-id/lang/index.js");u=function(e,t){const n=Object.keys(e);n.push(...Object.keys(t));const i={};for(let r=0;r<n.length;r++){const o=n[r];i[o]=Object.assign({},e[o],t[o])}return i}(u,require(e))}catch(e){}var d=u;function l(e){return e.replace(/=/g,"").replace(/\+/g,"-").replace(/\//g,"_")}function h(e){return JSON.parse((t=function(e){var t=4-(e=e.toString()).length%4;if(4!==t)for(var n=0;n<t;++n)e+="=";return e.replace(/-/g,"+").replace(/_/g,"/")}(e),Buffer.from(t,"base64").toString("utf-8")));var t}function f(e){return l((t=JSON.stringify(e),Buffer.from(t,"utf-8").toString("base64")));var t}function p(e,n){return l(t.createHmac("sha256",n).update(e).digest("base64"))}const k=function(e,t){if("string"!=typeof e)throw new Error("Invalid token");const n=e.split(".");if(3!==n.length)throw new Error("Invalid token");const[i,r,o]=n;if(p(i+"."+r,t)!==o)throw new Error("Invalid token");const s=h(i);if("HS256"!==s.alg||"JWT"!==s.typ)throw new Error("Invalid token");const c=h(r);if(1e3*c.exp<Date.now()){const e=new Error("Token expired");throw e.name="TokenExpiredError",e}return c},g=function(e,t,n={}){const{expiresIn:i}=n;if(!i)throw new Error("expiresIn is required");const r=parseInt(Date.now()/1e3),o={...e,iat:r,exp:r+n.expiresIn},s=f({alg:"HS256",typ:"JWT"})+"."+f(o);return s+"."+p(s,t)},I=uniCloud.database(),_=I.command,C=I.collection("uni-id-users"),T=I.collection("uni-id-roles");class m{constructor({uniId:e}={}){this.uid=null,this.userRecord=null,this.userPermission=null,this.oldToken=null,this.oldTokenPayload=null,this.uniId=e,this.config=this.uniId._getConfig(),this.clientInfo=this.uniId._clientInfo,this.checkConfig()}checkConfig(){const{tokenExpiresIn:e,tokenExpiresThreshold:t}=this.config;if(t>=e)throw new Error("Config error, tokenExpiresThreshold should be less than tokenExpiresIn");t>e/2&&console.warn(`Please check whether the tokenExpiresThreshold configuration is set too large, tokenExpiresThreshold: ${t}, tokenExpiresIn: ${e}`)}get customToken(){return this.uniId.interceptorMap.get("customToken")}isTokenInDb(e){return o(e,"1.0.10")>=0}async getUserRecord(){if(this.userRecord)return this.userRecord;const e=await C.doc(this.uid).get();if(this.userRecord=e.data[0],!this.userRecord)throw{errCode:n.ACCOUNT_NOT_EXISTS};switch(this.userRecord.status){case void 0:case 0:break;case 1:throw{errCode:n.ACCOUNT_BANNED};case 2:throw{errCode:n.ACCOUNT_AUDITING};case 3:throw{errCode:n.ACCOUNT_AUDIT_FAILED};case 4:throw{errCode:n.ACCOUNT_CLOSED}}if(this.oldTokenPayload){if(this.isTokenInDb(this.oldTokenPayload.uniIdVersion)){if(-1===(this.userRecord.token||[]).indexOf(this.oldToken))throw{errCode:n.CHECK_TOKEN_FAILED}}if(this.userRecord.valid_token_date&&this.userRecord.valid_token_date>1e3*this.oldTokenPayload.iat)throw{errCode:n.TOKEN_EXPIRED}}return this.userRecord}async updateUserRecord(e){await C.doc(this.uid).update(e)}async getUserPermission(){if(this.userPermission)return this.userPermission;const e=(await this.getUserRecord()).role||[];if(0===e.length)return this.userPermission={role:[],permission:[]},this.userPermission;if(e.includes("admin"))return this.userPermission={role:e,permission:[]},this.userPermission;const t=await T.where({role_id:_.in(e)}).get(),n=(i=t.data.reduce((e,t)=>(t.permission&&e.push(...t.permission),e),[]),Array.from(new Set(i)));var i;return this.userPermission={role:e,permission:n},this.userPermission}async _createToken({uid:e,role:t,permission:i}={}){if(!t||!i){const e=await this.getUserPermission();t=e.role,i=e.permission}let r={uid:e,role:t,permission:i};if(this.uniId.interceptorMap.has("customToken")){const n=this.uniId.interceptorMap.get("customToken");if("function"!=typeof n)throw new Error("Invalid custom token file");r=await n({uid:e,role:t,permission:i})}const o=Date.now(),{tokenSecret:s,tokenExpiresIn:c,maxTokenLength:a=10}=this.config,u=g({...r,uniIdVersion:"1.0.16"},s,{expiresIn:c}),d=await this.getUserRecord(),l=(d.token||[]).filter(e=>{try{const t=this._checkToken(e);if(d.valid_token_date&&d.valid_token_date>1e3*t.iat)return!1}catch(e){if(e.errCode===n.TOKEN_EXPIRED)return!1}return!0});return l.push(u),l.length>a&&l.splice(0,l.length-a),await this.updateUserRecord({last_login_ip:this.clientInfo.clientIP,last_login_date:o,token:l}),{token:u,tokenExpired:o+1e3*c}}async createToken({uid:e,role:t,permission:i}={}){if(!e)throw{errCode:n.PARAM_REQUIRED,errMsgValue:{param:"uid"}};this.uid=e;const{token:r,tokenExpired:o}=await this._createToken({uid:e,role:t,permission:i});return{errCode:0,token:r,tokenExpired:o}}async refreshToken({token:e}={}){if(!e)throw{errCode:n.PARAM_REQUIRED,errMsgValue:{param:"token"}};this.oldToken=e;const t=this._checkToken(e);this.uid=t.uid,this.oldTokenPayload=t;const{uid:i}=t,{role:r,permission:o}=await this.getUserPermission(),{token:s,tokenExpired:c}=await this._createToken({uid:i,role:r,permission:o});return{errCode:0,token:s,tokenExpired:c}}_checkToken(e){const{tokenSecret:t}=this.config;let i;try{i=k(e,t)}catch(e){if("TokenExpiredError"===e.name)throw{errCode:n.TOKEN_EXPIRED};throw{errCode:n.CHECK_TOKEN_FAILED}}return i}async checkToken(e,{autoRefresh:t=!0}={}){if(!e)throw{errCode:n.PARAM_REQUIRED,errMsgValue:{param:"token"}};this.oldToken=e;const i=this._checkToken(e);this.uid=i.uid,this.oldTokenPayload=i;const{tokenExpiresThreshold:r}=this.config,{uid:o,role:s,permission:c}=i,a={role:s,permission:c};if(!s&&!c){const{role:e,permission:t}=await this.getUserPermission();a.role=e,a.permission=t}if(!r||!t){const e={code:0,errCode:0,...i,...a};return delete e.uniIdVersion,e}const u=Date.now();let d={};1e3*i.exp-u<1e3*r&&(d=await this._createToken({uid:o}));const l={code:0,errCode:0,...i,...a,...d};return delete l.uniIdVersion,l}}var E=Object.freeze({__proto__:null,checkToken:async function(e,{autoRefresh:t=!0}={}){return new m({uniId:this}).checkToken(e,{autoRefresh:t})},createToken:async function({uid:e,role:t,permission:n}={}){return new m({uniId:this}).createToken({uid:e,role:t,permission:n})},refreshToken:async function({token:e}={}){return new m({uniId:this}).refreshToken({token:e})}});const w=require("uni-config-center")({pluginId:"uni-id"});class x{constructor({context:e,clientInfo:t,config:n}={}){this._clientInfo=e?function(e){return{appId:e.APPID,platform:e.PLATFORM,locale:e.LOCALE,clientIP:e.CLIENTIP,deviceId:e.DEVICEID}}(e):t,this.config=n||this._getOriginConfig(),this.interceptorMap=new Map,w.hasFile("custom-token.js")&&this.setInterceptor("customToken",require(w.resolve("custom-token.js")));this._i18n=uniCloud.initI18n({locale:this._clientInfo.locale,fallbackLocale:"zh-Hans",messages:JSON.parse(JSON.stringify(d))}),d[this._i18n.locale]||this._i18n.setLocale("zh-Hans")}setInterceptor(e,t){this.interceptorMap.set(e,t)}_t(...e){return this._i18n.t(...e)}_parseOriginConfig(e){return Array.isArray(e)?e:e[0]?Object.values(e):e}_getOriginConfig(){if(w.hasFile("config.json")){let e;try{e=w.config()}catch(e){throw new Error("Invalid uni-id config file\n"+e.message)}return this._parseOriginConfig(e)}try{return this._parseOriginConfig(require("uni-id/config.json"))}catch(e){throw new Error("Invalid uni-id config file")}}_getAppConfig(){const e=this._getOriginConfig();return Array.isArray(e)?e.find(e=>e.dcloudAppid===this._clientInfo.appId)||e.find(e=>e.isDefaultConfig):e}_getPlatformConfig(){const e=this._getAppConfig();if(!e)throw new Error(`Config for current app (${this._clientInfo.appId}) was not found, please check your config file or client appId`);let t;switch("app-plus"===this._clientInfo.platform&&(this._clientInfo.platform="app"),"h5"===this._clientInfo.platform&&(this._clientInfo.platform="web"),this._clientInfo.platform){case"web":t="h5";break;case"app":t="app-plus"}const n=[{tokenExpiresIn:7200,tokenExpiresThreshold:1200,passwordErrorLimit:6,passwordErrorRetryTime:3600},e];t&&e[t]&&n.push(e[t]),n.push(e[this._clientInfo.platform]);const i=Object.assign(...n);return["tokenSecret","tokenExpiresIn"].forEach(e=>{if(!i||!i[e])throw new Error(`Config parameter missing, ${e} is required`)}),i}_getConfig(){return this._getPlatformConfig()}}for(const e in E)x.prototype[e]=E[e];function y(e){const t=new x(e);return new Proxy(t,{get(e,t){if(t in e&&0!==t.indexOf("_")){if("function"==typeof e[t])return(n=e[t],function(){let e;try{e=n.apply(this,arguments)}catch(e){if(a(e))return c.call(this,e),e;throw e}return i(e)?e.then(e=>(a(e)&&c.call(this,e),e),e=>{if(a(e))return c.call(this,e),e;throw e}):(a(e)&&c.call(this,e),e)}).bind(e);if("context"!==t&&"config"!==t)return e[t]}var n}})}x.prototype.createInstance=y;const A={createInstance:y};module.exports=A;
{
"name": "uni-id-common",
"version": "1.0.14",
"version": "1.0.16",
"description": "uni-id token生成、校验、刷新",
"homepage": "https://uniapp.dcloud.io/uniCloud/uni-id-common.html",
"repository": {
......
## 0.6.4(2023-09-01)
chore: 优化代码结构
## 0.6.3(2023-08-30)
- 修复 下载 wgt 时如果后缀名不正确,重命名后安装
## 0.6.2(2022-11-21)
- 处理 cloudfunctions 目录
## 0.6.1(2022-08-17)
- 修复 后台添加应用市场,但都没有启用的情况下报错的Bug (需要 uni-admin 1.9.3+)
## 0.6.0(2022-07-19)
......
{
"displayName": "升级中心 uni-upgrade-center - App",
"version": "0.6.1",
"version": "0.6.4",
"description": "uni升级中心 - 客户端检查更新",
"keywords": [
"uniCloud",
......@@ -10,7 +10,7 @@
],
"repository": "https://gitee.com/dcloud/uni-upgrade-center/tree/master/uni_modules/uni-upgrade-center-app",
"engines": {
"HBuilderX": "^3.1.0"
"HBuilderX": "^3.2.14"
},
"dcloudext": {
"sale": {
......
......@@ -28,22 +28,20 @@
<template v-else>
<template v-if="!downloadSuccess">
<view class="progress-box flex-column" v-if="downloading">
<progress class="progress" border-radius="35" :percent="downLoadPercent"
activeColor="#3DA7FF" show-info stroke-width="10" />
<progress class="progress" border-radius="35" :percent="downLoadPercent" activeColor="#3DA7FF" show-info
stroke-width="10" />
<view style="width:100%;font-size: 28rpx;display: flex;justify-content: space-around;">
<text>{{downLoadingText}}</text>
<text>({{downloadedSize}}/{{packageFileSize}}M)</text>
</view>
</view>
<button v-else class="content-button" style="border: none;color: #fff;" plain
@click="updateApp">
<button v-else class="content-button" style="border: none;color: #fff;" plain @click="updateApp">
{{downLoadBtnText}}
</button>
</template>
<button v-else-if="downloadSuccess && !installed" class="content-button"
style="border: none;color: #fff;" plain :loading="installing" :disabled="installing"
@click="installPackage">
<button v-else-if="downloadSuccess && !installed" class="content-button" style="border: none;color: #fff;"
plain :loading="installing" :disabled="installing" @click="installPackage">
{{installing ? '正在安装……' : '下载完成,立即安装'}}
</button>
......@@ -55,8 +53,8 @@
</view>
</view>
<image v-if="!is_mandatory" class="close-img" src="../images/app_update_close.png"
@click.stop="closeUpdate"></image>
<image v-if="!is_mandatory" class="close-img" src="../images/app_update_close.png" @click.stop="closeUpdate">
</image>
</view>
</view>
</template>
......@@ -282,15 +280,36 @@
success: res => {
if (res.statusCode == 200) {
this.downloadSuccess = true;
// fix: wgt 文件下载完成后后缀不是 wgt
if (this.isWGT && !res.tempFilePath.endsWith('.wgt')) {
const failCallback = (e) => {
console.log('[FILE RENAME FAIL]:', JSON.stringify(e));
}
plus.io.resolveLocalFileSystemURL(res.tempFilePath, (entry) => {
const originName = entry.name
entry.getParent((parent) => {
const newName = `new_wgt_${Date.now()}.wgt`
entry.copyTo(parent, newName, (result) => {
this.tempFilePath = res.tempFilePath.replace(originName, newName)
this.downLoadComplete()
}, failCallback)
}, failCallback)
}, failCallback);
} else {
this.tempFilePath = res.tempFilePath
// 强制更新,直接安装
if (this.is_mandatory) {
this.installPackage();
this.downLoadComplete()
}
}
}
});
downloadTask.onProgressUpdate(res => {
this.downLoadPercent = res.progress;
this.downloadedSize = (res.totalBytesWritten / Math.pow(1024, 2)).toFixed(2);
this.packageFileSize = (res.totalBytesExpectedToWrite / Math.pow(1024, 2)).toFixed(2);
});
},
complete: () => {
downLoadComplete() {
this.downloading = false;
this.downLoadPercent = 0
......@@ -298,14 +317,11 @@
this.packageFileSize = 0
downloadTask = null;
}
});
downloadTask.onProgressUpdate(res => {
this.downLoadPercent = res.progress;
this.downloadedSize = (res.totalBytesWritten / Math.pow(1024, 2)).toFixed(2);
this.packageFileSize = (res.totalBytesExpectedToWrite / Math.pow(1024, 2)).toFixed(2);
});
// 强制更新,直接安装
if (this.is_mandatory) {
this.installPackage();
}
},
installPackage() {
// #ifdef APP-PLUS
......@@ -313,6 +329,7 @@
if (this.isWGT) {
this.installing = true;
}
plus.runtime.install(this.tempFilePath, {
force: false
}, async res => {
......@@ -436,7 +453,8 @@
background-color: #fff;
box-sizing: border-box;
padding: 0 50rpx;
font-family: -apple-system-font, 'Helvetica Neue', Helvetica, sans-serif;
font-family: -apple-system-font, BlinkMacSystemFont, 'Droid Sans', 'Noto Sans', 'PingFang SC', 'Heiti SC',
'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', SimSun, sans-serif;
}
.text {
......
......@@ -2,13 +2,12 @@ export default function() {
// #ifdef APP-PLUS
return new Promise((resolve, reject) => {
plus.runtime.getProperty(plus.runtime.appid, function(widgetInfo) {
const data = {
let data = {
action: 'checkVersion',
appid: plus.runtime.appid,
appVersion: plus.runtime.version,
wgtVersion: widgetInfo.version
}
console.log("data: ",data);
uniCloud.callFunction({
name: 'uni-upgrade-center',
data,
......
......@@ -5,24 +5,41 @@ export function getToken() {
}
export function getAuthCache(key: string): string | null {
let value: string | null = null
// #ifdef APP-PLUS
value = plus.storage.getItem(key)
// #endif
// #ifndef APP-PLUS
try {
const value = uni.getStorageSync(key)
value = uni.getStorageSync(key)
if (value) {
return value
}
} catch (e) {}
// #endif
return null
return value
}
export function setAuthCache(key: string, value: string) {
// #ifdef APP-PLUS
plus.storage.setItem(key, value)
// #endif
// #ifndef APP-PLUS
try {
uni.setStorageSync(key, value)
} catch (e) {}
// #endif
}
export function clearAuthCache() {
// #ifdef APP-PLUS
plus.storage.clear()
// #endif
// #ifndef APP-PLUS
try {
uni.clearStorageSync()
} catch (e) {}
// #endif
}
......@@ -50,7 +50,7 @@ export function getEnv(): string {
/**
* 获取运行环境中文标识
* @returns
* @returns string
*/
export function getEnvText(): string {
switch (import.meta.env.MODE) {
......
// 将 KB 转换为其他单位
export function convertKB(kb: number) {
const bytes = kb * 1024
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let size = bytes
let unitIndex = 0
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024
unitIndex++
}
return { size: size.toFixed(2), unit: units[unitIndex] }
}
......@@ -12,7 +12,7 @@ export function getPendingUrl(config: AxiosRequestConfig) {
export class AxiosCanceler {
/**
* Add request
* @param {Object} config
* @param {object} config
*/
addPending(config: AxiosRequestConfig) {
this.removePending(config)
......@@ -39,7 +39,7 @@ export class AxiosCanceler {
/**
* Removal request
* @param {Object} config
* @param {object} config
*/
removePending(config: AxiosRequestConfig) {
const url = getPendingUrl(config)
......
......@@ -12,13 +12,14 @@ import { useI18n } from '/@/hooks/app/useI18n'
import { useMessage } from '/@/hooks/app/useMessage'
import { ContentTypeEnum, RequestEnum } from '/@/enums/httpEnum'
import { isString } from '/@/utils/is'
import { clearAuthCache, getToken } from '/@/utils/auth'
import { getToken, setAuthCache } from '/@/utils/auth'
import { deepMerge, setObjToUrlParams } from '/@/utils'
import { formatRequestDate, joinTimestamp } from './helper'
import { AxiosRetry } from '/@/utils/http/axios/axiosRetry'
import * as HTTP from '/@/api/types'
import { API_URL, API_URL_PREFIX } from '/@/utils/net'
import { handleResponseResource } from '/@/utils/proxy'
import { TOKEN_KEY } from '@/enums/cacheEnum'
const globSetting = useGlobSetting()
const urlPrefix = globSetting.urlPrefix
......@@ -63,16 +64,25 @@ const transform: AxiosTransform = {
// 如果不希望中断当前请求,请return数据,否则直接抛出异常即可
let timeoutMsg = ''
switch (status) {
case HTTP.Status.UNAUTHORIZED:
case HTTP.Status.UNAUTHORIZED: {
timeoutMsg = t('sys.api.timeoutMessage')
clearAuthCache()
// TODO: 跳转到登录页
// uni.reLaunch({
// url: '/pages/login/index',
// })
// 清空 token
setAuthCache(TOKEN_KEY, '')
// 判断当前页面是否为登录页,防止多个请求时重复跳转
const page = getCurrentPages()[0]
const loginPageRoute = `/pages/login/login`
if (page.route !== loginPageRoute) {
// 跳转到登录页
uni.reLaunch({
url: loginPageRoute,
})
return
}
break
}
default:
if (details) {
timeoutMsg = details
......
export default {
links: {
/**
* 公司信息
*/
company: {
name: '长沙壹润信息科技发展有限公司',
link: 'https://yiring.com',
},
/**
* 版权信息
*/
copyright: {
name: '长沙壹润信息科技发展有限公司',
link: 'https://yiring.com',
},
/**
* 服务协议地址
* @description TODO: 服务协议地址需要根据实际情况进行修改
*/
services: 'https://app.yiring.com/services.html',
/**
* 隐私政策地址
* @description TODO: 隐私政策地址需要根据实际情况进行修改
*/
privacy: 'https://app.yiring.com/privacy.html',
},
/**
* 跳转到指定页面
* @param page 页面全路径
*/
async to(page: string, options?: UniNamespace.NavigateToOptions) {
await uni.navigateTo({
url: page,
...options,
})
},
/**
* 从外部打开指定页面
* @param link 链接
*/
open(link: string) {
// #ifdef APP-PLUS
if (link?.startsWith('tel:')) {
uni.makePhoneCall({
phoneNumber: link.replace('tel:', ''),
})
} else {
plus.runtime.openURL(link)
}
// #endif
// #ifdef H5
window.open(link)
// #endif
},
/**
* 从内部打开指定页面
* @param link 链接
* @param title 页面标题
*/
webview(link: string, title: string) {
uni.navigateTo({
url: `/pages/common/webview/index?title=${encodeURIComponent(title)}&link=${encodeURIComponent(link)}`,
})
},
}
......@@ -230,8 +230,8 @@ class StompInstance {
/**
* 向服务器发送消息
* @param {string} destination 主题
* @param {Object | string} message 消息内容
* @param {Object} headers 消息头
* @param {object | string} message 消息内容
* @param {object} headers 消息头
*/
send(destination: string, message: Object | string, headers?: Object) {
if (this.client) {
......
......@@ -101,25 +101,25 @@ class UniWebSocket {
/**
* 连接开启
* @param {Object} _res
* @param {object} _res
*/
onopen(_res: object) {}
/**
* 连接关闭
* @param {Object} _res
* @param {object} _res
*/
onclose(_res: object) {}
/**
* 连接异常
* @param {Object} _res
* @param {object} _res
*/
onerror(_res: object) {}
/**
* 接收消息
* @param {Object} _res
* @param {object} _res
*/
onmessage(_res: object) {}
......
......@@ -22,3 +22,31 @@ export function checkUpgrade(toast = false) {
})
// #endif
}
/**
* 关闭 splashscreen 并检查更新
*/
export function closeSplashscreenAndChechUpgrade() {
return new Promise<void>((resolve) => {
// FIXED: pages 第一路由页面为登录页,加上 splashscreen 配置控制使得用户体验更好
// #ifdef APP-PLUS
nextTick(() => {
// FIX: 延时执行,解决安卓环境下可能意外看到登录页的问题
const { platform } = uni.getSystemInfoSync()
const isAndroid = platform === 'android'
setTimeout(
() => {
plus.navigator.closeSplashscreen()
// 检查更新
checkUpgrade()
resolve()
},
isAndroid ? 1000 : 50,
)
})
// #endif
// #ifndef APP-PLUS
resolve()
// #endif
})
}
......@@ -91,5 +91,5 @@ declare global {
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
}
......@@ -2,6 +2,8 @@ import type { AxiosRequestConfig } from 'axios'
declare global {
type Recordable<T = any> = Record<string, T>
type Nullable<T> = T | null
type NonNullable<T> = T extends null | undefined ? never : T
interface ImportMetaEnv extends ViteEnv {
__: unknown
......
......@@ -13,6 +13,7 @@ export default defineConfig({
{
'border-base': 'border border-[#eee]',
'flex-center': 'flex justify-center items-center',
ellipsis: 'text-ellipsis whitespace-nowrap overflow-hidden',
},
],
presets: [
......@@ -22,7 +23,7 @@ export default defineConfig({
presetWeappAttributify(),
// icon
presetIcons({
prefix: 'icon-',
prefix: ['i-', 'icon-'],
extraProperties: {
display: 'inline-flex',
},
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论