新增加Uniapp移动端

This commit is contained in:
ktianc 2022-12-04 21:42:59 +08:00
parent d7a4b9819c
commit 417bfcde05
424 changed files with 48651 additions and 114 deletions

141
README.md
View File

@ -20,7 +20,8 @@
Kinit 是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。
- 后端采用 Python 语言现代、快速(高性能) [FastAPI](https://fastapi.tiangolo.com/zh/) 异步框架 + [SQLAlchemy](https://www.sqlalchemy.org/) 异步操作 [MySQL](https://www.mysql.com/) 数据库。
- 前端采用 [vue-element-plus-admin](https://gitee.com/kailong110120130/vue-element-plus-admin) 、[Vue3](https://cn.vuejs.org/guide/introduction.html)、[Element Plus](https://element-plus.gitee.io/zh-CN/guide/design.html)、[TypeScript](https://www.tslang.cn/),等主流技术开发。
- PC端采用 [vue-element-plus-admin](https://gitee.com/kailong110120130/vue-element-plus-admin) 、[Vue3](https://cn.vuejs.org/guide/introduction.html)、[Element Plus](https://element-plus.gitee.io/zh-CN/guide/design.html)、[TypeScript](https://www.tslang.cn/)等主流技术开发。
- 移动端采用 [uni-app](https://uniapp.dcloud.net.cn/component/)[Vue2](https://v2.cn.vuejs.org/v2/guide/)[uView 2](https://www.uviewui.com/components/intro.html)为主要技术开发
- 新加入 [Typer](https://typer.tiangolo.com/) 命令行应用,简单化数据初始化,数据表模型迁移。
- 权限认证使用[(哈希)密码和 JWT Bearer 令牌的 OAuth2](https://fastapi.tiangolo.com/zh/tutorial/security/oauth2-jwt/),支持多终端认证系统。
- 支持加载动态权限菜单,多方式轻松权限控制,按钮级别权限控制。
@ -32,15 +33,15 @@ Kinit 是一套全部开源的快速开发平台,毫无保留给个人及企
[vue-element-plus-admin](https://gitee.com/kailong110120130/vue-element-plus-admin)一套基于vue3、element-plus、typescript4、vite3的后台集成方案
[django-vue-admin](https://gitee.com/liqianglog/django-vue-admin)基于RBAC模型的权限控制的一整套基础开发平台前后端分离后端采用 django+django-rest-framework前端采用 vue+ElementUI。
[RuoYi 若依官方网站](http://www.ruoyi.vip/)RuoYi 是一个后台管理系统基于经典技术组合Spring Boot、Apache Shiro、MyBatis、Thymeleaf主要目的让开发者注重专注业务降低技术难度从而节省人力成本缩短项目周期提高软件安全质量。
[django-vue-admin](https://gitee.com/liqianglog/django-vue-admin)基于RBAC模型的权限控制的一整套基础开发平台前后端分离后端采用 django+django-rest-framework前端采用 vue+ElementUI。
[Ant Design Pro](https://preview.pro.ant.design/dashboard/analysis):开箱即用的中台前端/设计解决方案
[Gin-Vue-Admin](https://demo.gin-vue-admin.com)基于vite+vue3+gin搭建的开发基础平台支持TS,JS混用集成jwt鉴权权限管理动态路由显隐可控组件分页封装多点登录拦截资源权限上传下载代码生成器表单生成器等开发必备功能。
[Vben Admin (vvbin.cn)](https://vvbin.cn/next)Vue Vben Admin 是一个免费开源的中后台模版。使用了最新的`vue3`,`vite2`,`TypeScript`等主流技术开发,开箱即用的中后台前端解决方案,也可用于学习参考。
[Vben Admin](https://doc.vvbin.cn/guide/introduction.html)Vue Vben Admin 是一个免费开源的中后台模版。使用了最新的`vue3`,`vite2`,`TypeScript`等主流技术开发,开箱即用的中后台前端解决方案,也可用于学习参考。
[中华人民共和国行政区划 (github.com)](https://github.com/modood/Administrative-divisions-of-China):省级(省份)、 地级(城市)、 县级(区县)、 乡级(乡镇街道)、 村级(村委会居委会) ,中国省市区镇村二级三级四级五级联动地址数据。
@ -48,11 +49,20 @@ Kinit 是一套全部开源的快速开发平台,毫无保留给个人及企
[小诺开源技术 (xiaonuo.vip)](https://www.xiaonuo.vip/):国内首个国密前后端分离快速开发平台
[my-web:](https://gitee.com/newgateway/my-web)MyWeb 是一个企业级中后台前端/设计解决方案的的项目工程模板,它可以帮助你快速搭建企业级中后台产品原型
[my-web](https://gitee.com/newgateway/my-web)MyWeb 是一个企业级中后台前端/设计解决方案的的项目工程模板,它可以帮助你快速搭建企业级中后台产品原型
## 在线体验
演示地址http://kinit.ktianc.top/
PC端演示地址http://admin.kinit.top
移动端演示地址http://h5.kinit.top
微信小程序端演示:
- 搜索kinit
- 扫码:
<img src="https://gitee.com/ktianc/kinit/raw/master/images/uni/gh_5566dcf85bf0_860.jpg" alt="image-20221010214526082" style="zoom:33%;" />
- 账号15020221010
- 密码kinit2022
@ -63,7 +73,7 @@ gitee地址(主推)https://gitee.com/ktianc/kinit
github地址https://github.com/vvandk/kinit
## 内置功能
## PC端内置功能
- [x] 菜单管理:配置系统菜单,操作权限,按钮权限标识、后端接口权限等。
@ -77,7 +87,7 @@ github地址https://github.com/vvandk/kinit
- [x] 文件上传对接阿里云OSS与本地存储。
- [x] 登录认证:目前支持用户使用手机号+密码方式登录。
- [x] 登录认证:目前支持用户使用手机号+密码方式或者手机验证码登录。
说明:新建用户密码默认为手机号后六位;
@ -99,8 +109,6 @@ github地址https://github.com/vvandk/kinit
- [x] 导入导出:灵活支持数据导入导出功能
- [x] 手机验证码登录功能
- [x] 简单适配手机端:
1. 工作台招呼语一行显示,多余显示省略号
@ -109,10 +117,22 @@ github地址https://github.com/vvandk/kinit
4. 表格工具栏更新,手机端取消文字显示
5. 表格操作按钮多的时候自动叠起
- [x] 已加入常见的`Redis``MYSQL`、`MongoDB`数据库异步操作。
- [x] 已加入常见的`Redis``MySQL`、`MongoDB`数据库异步操作。
- [x] 命令行操作:新加入 `Typer` 命令行应用,简单化数据初始化,数据表模型迁移。
## 移动端内置功能
- [x] 登录认证:目前支持用户使用手机号+密码方式登录。
说明:新建用户密码默认为手机号后六位;
说明:用户在第一次登录时,必须修改当前用户密码。
- [x] 导航栏:首页、我的、工作台
- [x] 我的基础功能:编辑资料、头像修改、密码修改、常见问题、关于我们等
## TODO
- [ ] 考虑支持多机部署方案,如果接口使用多机,那么用户是否支持统一认证
@ -123,8 +143,15 @@ github地址https://github.com/vvandk/kinit
## 前序准备
- [FastAPI](https://fastapi.tiangolo.com/zh/) - 熟悉后台接口 Web 框架
### 后端技术
- [Python3](https://www.python.org/downloads/windows/):熟悉 python3 基础语法
- [FastAPI](https://fastapi.tiangolo.com/zh/) - 熟悉后台接口 Web 框架.
- [Typer](https://typer.tiangolo.com/) - 熟悉命令行工具的使用
- [MySQL](https://www.mysql.com/) 和 [MongoDB](https://www.mongodb.com/) - 熟悉数据存储数据库
### PC端
- [node](https://gitee.com/link?target=http%3A%2F%2Fnodejs.org%2F) 和 [git](https://gitee.com/link?target=https%3A%2F%2Fgit-scm.com%2F) - 项目开发环境
- [Vite](https://gitee.com/link?target=https%3A%2F%2Fvitejs.dev%2F) - 熟悉 vite 特性
- [Vue3](https://gitee.com/link?target=https%3A%2F%2Fv3.vuejs.org%2F) - 熟悉 Vue 基础语法
@ -134,15 +161,26 @@ github地址https://github.com/vvandk/kinit
- [Element-Plus](https://gitee.com/link?target=https%3A%2F%2Felement-plus.org%2F) - element-plus 基本使用
- [Mock.js](https://gitee.com/link?target=https%3A%2F%2Fgithub.com%2Fnuysoft%2FMock) - mockjs 基本语法
### 移动端
- [uni-app](https://uniapp.dcloud.net.cn/component/) - 熟悉 uni-app 基本语法
- [Vue2](https://v2.cn.vuejs.org/v2/guide/) - 熟悉 Vue 基础语法
- [uView UI 2](https://www.uviewui.com/components/intro.html)uView UI 组件的基本使用
### 依赖包
#### 前端
#### PC
- [vue3-json-viewer](https://gitee.com/isfive/vue3-json-viewer)简单易用的json内容展示组件,适配vue3和vite。
- [vue3-slide-verify](https://github.com/monoplasty/vue3-slide-verify):滑块验证码插件 vue3 + typescript
- [vue3-slide-verify](https://github.com/monoplasty/vue3-slide-verify):滑块验证码插件 vue3 + typescript
- [SortableJS/vue.draggable.next](https://github.com/SortableJS/vue.draggable.next)Vue 组件 Vue.js 3.0 允许拖放和与视图模型数组同步。
- [高德地图API (amap.com)](https://lbs.amap.com/api/jsapi-v2/guide/webcli/map-vue1):地图 JSAPI 2.0 是高德开放平台免费提供的第四代 Web 地图渲染引擎, 以 WebGL 为主要绘图手段,本着“更轻、更快、更易用”的服务原则,广泛采用了各种前沿技术,交互体验、视觉体验大幅提升,同时提供了众多新增能力和特性。
#### 移动端
- [uni-read-pages](https://github.com/SilurianYang/uni-read-pages) :自动读取 `pages.json` 所有配置。
- [uni-simple-router](https://hhyang.cn/v2/start/quickstart.html) 在uni-app中使用vue-router的方式进行跳转路由路由拦截。
#### 后端
- [iP查询接口文档](https://user.ip138.com/ip/doc)IP查询第三方服务有1000次的免费次数
@ -169,11 +207,11 @@ Redis (推荐使用最新稳定版)
1. 安装依赖
```
cd kinit-api
pip3 install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
```
```
cd kinit-api
pip3 install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
```
2. 修改项目数据库配置信息
@ -235,23 +273,32 @@ pip3 install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
```python
# mysql+pymysql://数据库用户名:数据库密码@数据库地址:数据库端口/数据库名称
sqlalchemy.url = mysql+pymysql://root:123456@127.0.0.1/kinit
[dev]
# 开发环境
version_locations = %(here)s/alembic/versions_dev
sqlalchemy.url = sqlalchemy.url = mysql+pymysql://root:123456@127.0.0.1/kinit
[pro]
# 生产环境
version_locations = %(here)s/alembic/versions_pro
sqlalchemy.url = sqlalchemy.url = mysql+pymysql://root:123456@127.0.0.1/kinit
```
3. 创建数据库
```shell
mysql> create database kinit; # 创建数据库
mysql> use kinit; # 使用已创建的数据库
mysql> set names utf8; # 设置编码
```
```
mysql> create database kinit; # 创建数据库
mysql> use kinit; # 使用已创建的数据库
mysql> set names utf8; # 设置编码
```
4. 初始化数据库数据
```shell
# 进入项目根目录下执行
python3 main.py init
```
```
# 进入项目根目录下执行
python3 main.py init
```
5. 修改项目基本配置信息
@ -272,12 +319,12 @@ python3 main.py init
6. 启动
```shell
# 进入项目根目录下执行
python3 main.py run
```
```
# 进入项目根目录下执行
python3 main.py run
```
###
### PC
1. 安装依赖
@ -349,7 +396,7 @@ pnpm run build:pro
[MIT](https://gitee.com/kailong110120130/vue-element-plus-admin/blob/master/LICENSE)
## 演示图
## PC端演示图
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/1.png)
@ -357,12 +404,28 @@ pnpm run build:pro
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/3.png)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/6.png)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/6.jpg)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/5.png)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/7.png)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/7.jpg)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/8.png)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/8.jpg)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/9.png)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/9.jpg)
## 移动端演示图
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/uni/1670077811740.jpg)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/uni/1670077826257.jpg)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/uni/1670077835024.jpg)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/uni/1670077849753.jpg)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/uni/1670077860987.jpg)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/uni/1670077870240.jpg)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/uni/1670077878132.jpg)

View File

@ -2,17 +2,12 @@
vue-element-plus-admin 是一个基于 `element-plus` 免费开源的中后台模版。使用了最新的`vue3``vite3``TypeScript`等主流技术开发,开箱即用的中后台前端解决方案,可以用来作为项目的启动模版,也可用于学习参考。并且时刻关注着最新技术动向,尽可能的第一时间更新。
vue-element-plus-admin 的定位是后台集成方案,不太适合当基础模板来进行二次开发。因为集成了很多你可能用不到的功能,会造成不少的代码冗余。如果你的项目不关注这方面的问题,也可以直接基于它进行二次开发。
如需要基础模版,请切换到 `tempalte` 分支,`tempalte` 只简单集成了一些如:布局、动态菜单等常用布局功能,更适合开发者进行二次开发。
## 特性
- **最新技术栈**:使用 Vue3/vite3 等前端前沿技术开发
- **TypeScript**: 应用程序级 JavaScript 的语言
- **主题**: 可配置的主题
- **国际化**:内置完善的国际化方案
- **自定义数据** 内置 Mock 数据方案
- **权限** 内置完善的动态路由权限生成方案
- **组件** 二次封装了多个常用的组件
- **示例** 内置丰富的示例
@ -81,4 +76,22 @@ pnpm run build:pro
## 更新日志
[更新日志](./CHANGELOG.md)
[更新日志](./CHANGELOG.md)
## 演示图
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/1.png)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/2.png)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/3.png)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/6.jpg)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/5.png)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/7.jpg)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/8.jpg)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/9.jpg)

View File

@ -26,7 +26,7 @@ const setSystemConfig = async () => {
const res = await getSystemSettingsClassifysApi({ classify: 'web' })
if (res) {
appStore.setTitle(res.data.web_basic.web_title || import.meta.env.VITE_APP_TITLE)
appStore.setLogoImage(res.data.web_basic.web_logo || '/static/system/logo.png')
appStore.setLogoImage(res.data.web_basic.web_logo || '/media/system/logo.png')
appStore.setFooterContent(res.data.web_basic.web_copyright || 'Copyright ©2022-present K')
appStore.setIcpNumber(res.data.web_basic.web_icp_number || '')
addMeta(

View File

@ -2,6 +2,7 @@ export type UserLoginType = {
telephone: string
password: string
method: string
platform?: string
}
export type UserType = {

View File

@ -15,3 +15,7 @@ export const putSystemSettingsApi = (data: any): Promise<IResponse> => {
export const getSystemSettingsClassifysApi = (params: any): Promise<IResponse> => {
return request.get({ url: '/vadmin/system/settings/classifys/', params })
}
export const getSystemSettingsConfigValueApi = (params: any): Promise<IResponse> => {
return request.get({ url: '/vadmin/system/settings/config/value/', params })
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -23,7 +23,6 @@ const loginOut = () => {
})
.then(() => {
authStore.logout()
replace('/login')
})
.catch(() => {})
}

View File

@ -1,5 +1,6 @@
const config: {
result_code: number | string
unauthorized_code: number | string
default_headers: AxiosHeaders
request_timeout: number
} = {
@ -7,6 +8,10 @@ const config: {
*
*/
result_code: 200,
/**
* TOKEN失效
*/
unauthorized_code: 401,
/**
*

View File

@ -1,16 +1,15 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
import { useCache } from '@/hooks/web/useCache'
import { useAppStore } from '@/store/modules/app'
import { useAuthStore } from '@/store/modules/auth'
import qs from 'qs'
import { config } from './config'
import { ElMessage } from 'element-plus'
const { result_code, request_timeout } = config
const { result_code, unauthorized_code, request_timeout } = config
const appStore = useAppStore()
const authStore = useAuthStore()
const { wsCache } = useCache()
// 创建axios实例
@ -64,6 +63,10 @@ service.interceptors.response.use(
return response
} else if (response.data.code === result_code) {
return response.data
} else if (response.data.code === unauthorized_code) {
// 请重新登录
ElMessage.error(response.data.message)
authStore.logout()
} else {
ElMessage.error(response.data.message)
}

View File

@ -20,7 +20,7 @@ const { start, done } = useNProgress()
const { loadStart, loadDone } = usePageLoading()
const whiteList = ['/login'] // 不重定向白名单
const whiteList = ['/login', '/docs/privacy', '/docs/agreement'] // 不重定向白名单
router.beforeEach(async (to, from, next) => {
start()

View File

@ -47,6 +47,37 @@ export const constantRouterMap: AppRouteRecordRaw[] = [
title: '404',
noTagsView: true
}
},
{
path: '/docs',
name: 'Docs',
meta: {
hidden: true,
title: '在线文档',
noTagsView: true
},
children: [
{
path: 'privacy',
name: 'Privacy',
component: () => import('@/views/vadmin/system/docs/privacy.vue'),
meta: {
hidden: true,
title: '隐私政策',
noTagsView: true
}
},
{
path: 'agreement',
name: 'Agreement',
component: () => import('@/views/vadmin/system/docs/agreement.vue'),
meta: {
hidden: true,
title: '用户协议',
noTagsView: true
}
}
]
}
]
@ -61,7 +92,15 @@ const router = createRouter({
})
export const resetRouter = (): void => {
const resetWhiteNameList = ['Login', 'NoFind', 'Root', 'ResetPassword']
const resetWhiteNameList = [
'Login',
'NoFind',
'Root',
'ResetPassword',
'Docs',
'Privacy',
'Agreement'
]
router.getRoutes().forEach((route) => {
// 切记 name 不能重复
const { name } = route

View File

@ -49,6 +49,7 @@ export const useAuthStore = defineStore('auth', {
},
actions: {
async login(formData: UserLoginType) {
formData.platform = '0'
const res = await loginApi(formData)
if (res) {
wsCache.set(appStore.getToken, `${res.data.token_type} ${res.data.access_token}`)
@ -64,11 +65,13 @@ export const useAuthStore = defineStore('auth', {
this.isUser = false
tagsViewStore.delAllViews()
resetRouter()
window.location.href = '/login'
},
updateUser(data: UserState) {
this.user.gender = data.gender
this.user.name = data.name
this.user.nickname = data.nickname
this.user.telephone = data.telephone
wsCache.set(appStore.getUserInfo, this.user)
},
async getUserInfo() {

View File

@ -50,6 +50,10 @@ const user = authStore.getUser
<span class="pl-10px w-80px inline-block">性别:</span>
<span class="pl-10px">{{ selectDictLabel(genderOptions, user.gender as string) }}</span>
</div>
<div class="leading-relaxed">
<span class="pl-10px w-80px inline-block">角色:</span>
<span class="pl-10px">{{ user.roles?.map((item) => item.name).join(',') }}</span>
</div>
<div class="leading-relaxed">
<span class="pl-10px w-80px inline-block">创建时间:</span>
<span class="pl-10px">{{ user.create_datetime }}</span>

View File

@ -7,6 +7,7 @@ import { postCurrentUserUpdateInfo } from '@/api/vadmin/auth/user'
import { useValidator } from '@/hooks/web/useValidator'
import { useAuthStoreWithOut } from '@/store/modules/auth'
import { ElMessage } from 'element-plus'
import { FormSchema } from '@/types/form'
const { required } = useValidator()
@ -44,6 +45,20 @@ const schema = reactive<FormSchema[]>([
}
}
},
{
field: 'telephone',
label: '手机号',
component: 'Input',
colProps: {
span: 24
},
componentProps: {
style: {
width: '50%'
},
maxlength: 11
}
},
{
field: 'gender',
label: '性别',

View File

@ -7,6 +7,7 @@ import { postCurrentUserResetPassword } from '@/api/vadmin/auth/user'
import { useValidator } from '@/hooks/web/useValidator'
import { useAuthStoreWithOut } from '@/store/modules/auth'
import { ElMessage } from 'element-plus'
import { FormSchema } from '@/types/form'
const { required } = useValidator()
@ -46,7 +47,7 @@ const schema = reactive<FormSchema[]>([
},
{
field: 'password_two',
label: '再次输入新密码',
label: '确认密码',
component: 'InputPassword',
colProps: {
span: 24

View File

@ -31,7 +31,6 @@ import { useI18n } from '@/hooks/web/useI18n'
import { selectDictLabel, DictDetail } from '@/utils/dict'
import { useDictStore } from '@/store/modules/dict'
import { useAuthStoreWithOut } from '@/store/modules/auth'
import { useRouter } from 'vue-router'
import { RightToolbar } from '@/components/RightToolbar'
import { Search } from '@/components/Search'
import { useAppStore } from '@/store/modules/app'
@ -40,11 +39,9 @@ const appStore = useAppStore()
const { t } = useI18n()
const { replace } = useRouter()
const authStore = useAuthStoreWithOut()
let genderOptions = ref<DictDetail[]>([])
const genderOptions = ref<DictDetail[]>([])
const getOptions = async () => {
const dictStore = useDictStore()
@ -130,7 +127,6 @@ const save = async () => {
if (user.id === data.id && user.telephone !== data.telephone) {
dialogVisible.value = false
authStore.logout()
replace('/login')
return ElMessage.warning('认证已过期,请您重新登陆!')
}
}

View File

@ -0,0 +1,29 @@
<script setup lang="ts">
import { ref } from 'vue'
import { getSystemSettingsConfigValueApi } from '@/api/vadmin/system/settings'
const content = ref(null)
//
const getSystemConfig = async () => {
const res = await getSystemSettingsConfigValueApi({ config_key: 'web_agreement' })
if (res) {
content.value = res.data
}
}
getSystemConfig()
</script>
<template>
<div class="content-view" v-html="content"></div>
</template>
<style scoped lang="less">
.content-view {
padding: 20px;
overflow-y: scroll;
overflow-x: hidden;
height: 100%;
}
</style>

View File

@ -0,0 +1,29 @@
<script setup lang="ts">
import { ref } from 'vue'
import { getSystemSettingsConfigValueApi } from '@/api/vadmin/system/settings'
const content = ref(null)
//
const getSystemConfig = async () => {
const res = await getSystemSettingsConfigValueApi({ config_key: 'web_privacy' })
if (res) {
content.value = res.data
}
}
getSystemConfig()
</script>
<template>
<div class="content-view" v-html="content"></div>
</template>
<style scoped lang="less">
.content-view {
padding: 20px;
overflow-y: scroll;
overflow-x: hidden;
height: 100%;
}
</style>

View File

@ -8,12 +8,13 @@ export const columns = reactive<TableColumn[]>([
label: '编号',
show: true,
disabled: true,
width: '150px',
width: '120px',
span: 24
},
{
field: 'telephone',
label: '手机号',
width: '150px',
show: true,
disabled: true,
span: 24
@ -21,6 +22,21 @@ export const columns = reactive<TableColumn[]>([
{
field: 'status',
label: '登录状态',
width: '100px',
show: true,
span: 24
},
{
field: 'platform',
label: '登陆平台',
width: '150px',
show: true,
span: 24
},
{
field: 'login_method',
label: '认证方式',
width: '120px',
show: true,
span: 24
},
@ -29,6 +45,7 @@ export const columns = reactive<TableColumn[]>([
label: '登陆地址',
show: true,
disabled: true,
width: '150px',
span: 24
},
{

View File

@ -10,6 +10,8 @@ import { RightToolbar } from '@/components/RightToolbar'
import { Dialog } from '@/components/Dialog'
import Detail from './components/Detail.vue'
import { Search } from '@/components/Search'
import { selectDictLabel, DictDetail } from '@/utils/dict'
import { useDictStore } from '@/store/modules/dict'
const { register, elTableRef, tableObject, methods } = useTable({
getListApi: getRecordLoginListApi,
@ -25,6 +27,17 @@ const { register, elTableRef, tableObject, methods } = useTable({
const dialogVisible = ref(false)
const dialogTitle = ref('')
const platformOptions = ref<DictDetail[]>([])
const loginMethodOptions = ref<DictDetail[]>([])
const getOptions = async () => {
const dictStore = useDictStore()
const dictOptions = await dictStore.getDictObj(['sys_vadmin_platform', 'sys_vadmin_login_method'])
platformOptions.value = dictOptions.sys_vadmin_platform
loginMethodOptions.value = dictOptions.sys_vadmin_login_method
}
getOptions()
const view = (row: any) => {
dialogTitle.value = '登录详情'
@ -86,6 +99,14 @@ getList()
<template #status="{ row }">
<ElSwitch :value="row.status" size="small" disabled />
</template>
<template #platform="{ row }">
{{ selectDictLabel(platformOptions, row.platform) }}
</template>
<template #login_method="{ row }">
{{ selectDictLabel(loginMethodOptions, row.login_method) }}
</template>
</Table>
<Dialog v-model="dialogVisible" :title="dialogTitle" width="900px">

View File

@ -12,6 +12,10 @@ pydantic 数据模型代码生成器官方文档 Json -> Pydantichttps:
SQLAlchemy-Utilshttps://sqlalchemy-utils.readthedocs.io/en/latest/
alembic 中文文档https://hellowac.github.io/alembic_doc/zh/_front_matter.html
Typer 官方文档https://typer.tiangolo.com/
## 项目结构
@ -94,9 +98,11 @@ git commit -m "clear cached"
执行数据库迁移命令(终端执行)
```shell
# 执行命令:
# 执行命令(生产环境):
python main.py migrate
# 执行命令(测试环境):
python main.py migrate --env dev
```
生成迁移文件后会在alembic迁移目录中的version目录中多个迁移文件

View File

@ -1,6 +1,6 @@
# A generic, single database configuration.
[alembic]
[DEFAULT]
# path to migration scripts
script_location = alembic
@ -44,13 +44,21 @@ prepend_sys_path = .
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # default: use os.pathsep
version_path_separator = os
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# mysql+pymysql://username:password@host:post/name
[dev]
version_locations = %(here)s/alembic/versions_dev
sqlalchemy.url = mysql+pymysql://username:password@host:post/name
[pro]
version_locations = %(here)s/alembic/versions_pro
sqlalchemy.url = mysql+pymysql://username:password@host:post/name

View File

View File

@ -11,13 +11,13 @@ from fastapi.security import OAuth2PasswordBearer
"""
系统版本
"""
VERSION = "1.3.0"
VERSION = "1.4.0"
"""安全警告: 不要在生产中打开调试运行!"""
DEBUG = True
DEBUG = False
"""是否开启演示功能取消所有POST,DELETE,PUT操作权限"""
DEMO = False
DEMO = True
"""演示功能白名单"""
DEMO_WHITE_LIST_PATH = [
"/auth/login/",
@ -70,7 +70,7 @@ STATIC_ROOT静态文件目录绝对路径
官方文档https://fastapi.tiangolo.com/tutorial/static-files/
"""
STATIC_ENABLE = True
STATIC_URL = "/static"
STATIC_URL = "/media"
STATIC_ROOT = os.path.join(BASE_DIR, "static")

View File

@ -16,9 +16,11 @@ from sqlalchemy import select
from core.crud import DalBase
from sqlalchemy.ext.asyncio import AsyncSession
from core.validator import vali_telephone
from utils.aliyun_oss import AliyunOSS, BucketConf
from utils.aliyun_sms import AliyunSMS
from utils.excel.import_manage import ImportManage, FieldType
from utils.excel.write_xlsx import WriteXlsx
from utils.file_manage import FileManage
from .params import UserParams
from utils.tools import test_password
from . import models, schemas
@ -26,6 +28,7 @@ from application import settings
from utils.excel.excel_manage import ExcelManage
from apps.vadmin.system import crud as vadminSystemCRUD
import copy
from utils import status
class UserDal(DalBase):
@ -47,15 +50,13 @@ class UserDal(DalBase):
"""
unique = await self.get_data(telephone=data.telephone, v_return_none=True)
if unique:
raise ValueError("手机号已存在!")
raise CustomException("手机号已存在!", code=status.HTTP_ERROR)
password = data.telephone[5:12] if settings.DEFAULT_PASSWORD == "0" else settings.DEFAULT_PASSWORD
data.password = self.model.get_password_hash(password)
obj = self.model(**data.dict(exclude={'role_ids'}))
for data_id in data.role_ids:
obj.roles.append(await RoleDal(db=self.db).get_data(data_id=data_id))
self.db.add(obj)
await self.db.flush()
await self.db.refresh(obj)
await self.flush(obj)
if options:
obj = await self.get_data(obj.id, options=options)
if return_obj:
@ -75,21 +76,23 @@ class UserDal(DalBase):
raise CustomException(msg=result, code=400)
user.password = self.model.get_password_hash(data.password)
user.is_reset_password = True
self.db.add(user)
await self.db.flush()
await self.db.refresh(user)
await self.flush(user)
return True
async def update_current_info(self, user: models.VadminUser, data: schemas.UserUpdate):
"""
更新当前用户信息
"""
if data.telephone != user.telephone:
unique = await self.get_data(telephone=data.telephone, v_return_none=True)
if unique:
raise CustomException("手机号已存在!", code=status.HTTP_ERROR)
else:
user.telephone = data.telephone
user.name = data.name
user.nickname = data.nickname
user.gender = data.gender
self.db.add(user)
await self.db.flush()
await self.db.refresh(user)
await self.flush(user)
return self.out_dict(user)
async def export_query_list(self, header: list, params: UserParams):
@ -207,6 +210,18 @@ class UserDal(DalBase):
user["send_sms_msg"] = e.msg
return result
async def update_current_avatar(self, user: models.VadminUser, file: UploadFile):
"""
更新当前用户头像
"""
manage = FileManage(file, "avatar")
result = await AliyunOSS(BucketConf(**settings.ALIYUN_OSS)).upload_image(manage.path, file)
if not result:
raise CustomException(msg="上传失败", code=status.HTTP_ERROR)
user.avatar = result
await self.flush(user)
return result
class RoleDal(DalBase):
@ -218,9 +233,7 @@ class RoleDal(DalBase):
obj = self.model(**data.dict(exclude={'menu_ids'}))
for data_id in data.menu_ids:
obj.menus.append(await MenuDal(db=self.db).get_data(data_id=data_id))
self.db.add(obj)
await self.db.flush()
await self.db.refresh(obj)
await self.flush(obj)
if options:
obj = await self.get_data(obj.id, options=options)
if return_obj:

View File

@ -52,6 +52,7 @@ class UserOut(UserSimpleOut):
class UserUpdate(BaseModel):
name: str
telephone: Telephone
nickname: Optional[str] = None
gender: Optional[str] = "0"

View File

@ -45,9 +45,8 @@ async def login_for_access_token(request: Request, data: LoginForm, manage: Logi
return ErrorResponse(msg="请使用正确的登录方式")
if not result.status:
resp = {"message": result.msg}
telephone = data.telephone
await VadminLoginRecord.\
create_login_record(db, telephone, result.status, request, resp)
create_login_record(db, data, result.status, request, resp)
return ErrorResponse(msg=result.msg)
user = result.user
@ -58,7 +57,7 @@ async def login_for_access_token(request: Request, data: LoginForm, manage: Logi
"token_type": "bearer",
"is_reset_password": user.is_reset_password
}
await VadminLoginRecord.create_login_record(db, user.telephone, result.status, request, resp)
await VadminLoginRecord.create_login_record(db, data, result.status, request, resp)
return SuccessResponse(resp)

View File

@ -38,17 +38,17 @@ class AuthValidation:
if not settings.OAUTH_ENABLE:
return Auth(db=db)
if not token:
raise CustomException(msg="先登录!", code=status.HTTP_ERROR)
raise CustomException(msg="先登录!", code=status.HTTP_ERROR)
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
telephone: str = payload.get("sub")
if telephone is None:
raise CustomException(msg="无效 Token", code=status.HTTP_403_FORBIDDEN)
raise CustomException(msg="认证已过期,请您重新登陆", code=status.HTTP_401_UNAUTHORIZED)
except JWTError:
raise CustomException(msg="无效 Token", code=status.HTTP_403_FORBIDDEN)
raise CustomException(msg="认证已过期,请您重新登陆", code=status.HTTP_401_UNAUTHORIZED)
user = await self.func(telephone, db)
if user is None:
raise CustomException(msg="用户不存在!", code=status.HTTP_404_NOT_FOUND)
raise CustomException(msg="认证已过期,请您重新登陆", code=status.HTTP_401_UNAUTHORIZED)
elif not user.is_active:
raise CustomException(msg="用户已被冻结!", code=status.HTTP_403_FORBIDDEN)
elif user.is_cancel:

View File

@ -19,6 +19,7 @@ class LoginForm(BaseModel):
telephone: str
password: str
method: str = '0' # 认证方式0密码登录1短信登录
platform: str = '0' # 登录平台0PC端管理系统1移动端管理系统
# validators
_normalize_telephone = validator('telephone', allow_reuse=True)(vali_telephone)

View File

@ -64,18 +64,18 @@ async def post_user_current_update_info(data: schemas.UserUpdate, auth: Auth = D
return SuccessResponse(await crud.UserDal(auth.db).update_current_info(auth.user, data))
@app.post("/user/current/update/avatar/", summary="更新当前用户头像")
async def post_user_current_update_avatar(file: UploadFile, auth: Auth = Depends(login_auth)):
return SuccessResponse(await crud.UserDal(auth.db).update_current_avatar(auth.user, file))
@app.get("/user/current/info/", summary="获取当前用户基本信息")
async def get_user_current_info(auth: Auth = Depends(full_admin)):
result = schemas.UserSimpleOut.from_orm(auth.user).dict()
result = schemas.UserOut.from_orm(auth.user).dict()
result["permissions"] = await get_user_permissions(auth.user)
return SuccessResponse(result)
@app.get("/user/current/info/", summary="获取当前用户基本信息")
async def get_user_current_info(auth: Auth = Depends(login_auth)):
return SuccessResponse(schemas.UserSimpleOut.from_orm(auth.user).dict())
@app.post("/user/export/query/list/to/excel/", summary="导出用户查询列表为excel")
async def post_user_export_query_list(header: list = Body(..., title="表头与对应字段"), params: UserParams = Depends(),
auth: Auth = Depends(login_auth)):

View File

@ -8,6 +8,7 @@
import json
from application.settings import LOGIN_LOG_RECORD
from apps.vadmin.auth.utils.validation import LoginForm
from utils.ip_manage import IPManage
from sqlalchemy.ext.asyncio import AsyncSession
from db.db_base import BaseModel
@ -22,6 +23,8 @@ class VadminLoginRecord(BaseModel):
telephone = Column(String(50), index=True, nullable=False, comment="手机号")
status = Column(Boolean, default=True, comment="是否登录成功")
platform = Column(String(8), comment="登陆平台")
login_method = Column(String(8), comment="认证方式")
ip = Column(String(50), comment="登陆地址")
address = Column(String(255), comment="登陆地点")
country = Column(String(255), comment="国家")
@ -37,7 +40,7 @@ class VadminLoginRecord(BaseModel):
request = Column(TEXT, comment="请求信息")
@classmethod
async def create_login_record(cls, db: AsyncSession, telephone: str, status: bool, req: Request, resp: dict) -> None:
async def create_login_record(cls, db: AsyncSession, data: LoginForm, status: bool, req: Request, resp: dict):
"""
创建登录记录
@return:
@ -54,7 +57,8 @@ class VadminLoginRecord(BaseModel):
ip = IPManage(req.client.host)
location = await ip.parse()
params = json.dumps({"body": body, "headers": header})
obj = VadminLoginRecord(**location.dict(), telephone=telephone, status=status, browser=browser,
system=system, response=json.dumps(resp), request=params)
obj = VadminLoginRecord(**location.dict(), telephone=data.telephone, status=status, browser=browser,
system=system, response=json.dumps(resp), request=params, platform=data.platform,
login_method=data.method)
db.add(obj)
await db.flush()

View File

@ -30,6 +30,8 @@ class LoginRecord(BaseModel):
city: Optional[str] = None
county: Optional[str] = None
operator: Optional[str] = None
platform: Optional[str] = None
login_method: Optional[str] = None
class LoginRecordSimpleOut(LoginRecord):

View File

@ -151,3 +151,8 @@ async def put_settings_tabs_values(datas: dict = Body(...), auth: Auth = Depends
@app.get("/settings/classifys/", summary="获取系统配置分类下的所有显示标签信息")
async def get_settings_classifys(classify: str, db: AsyncSession = Depends(db_getter)):
return SuccessResponse(await crud.SettingsTabDal(db).get_classify_tab_values([classify]))
@app.get("/settings/config/value/", summary="根据config_key获取到指定value")
async def get_settings_config_value(config_key: str, db: AsyncSession = Depends(db_getter)):
return SuccessResponse((await crud.SettingsDal(db).get_data(config_key=config_key)).config_value)

View File

@ -129,9 +129,7 @@ class DalBase:
obj = self.model(**data)
else:
obj = self.model(**data.dict())
self.db.add(obj)
await self.db.flush()
await self.db.refresh(obj)
await self.flush(obj)
if options:
obj = await self.get_data(obj.id, options=options)
if return_obj:
@ -148,8 +146,7 @@ class DalBase:
obj_dict = jsonable_encoder(data)
for key, value in obj_dict.items():
setattr(obj, key, value)
await self.db.flush()
await self.db.refresh(obj)
await self.flush(obj)
if return_obj:
return obj
if schema:
@ -223,6 +220,16 @@ class DalBase:
sql = sql.where(attr == value)
return sql
async def flush(self, obj=None):
"""
刷新到数据库
"""
if obj:
self.db.add(obj)
await self.db.flush()
if obj:
await self.db.refresh(obj)
def out_dict(self, data):
"""
序列化

View File

@ -8,6 +8,7 @@
"""
FastApi 更新文档https://github.com/tiangolo/fastapi/releases
FastApi Githubhttps://github.com/tiangolo/fastapi
Typer 官方文档https://typer.tiangolo.com/
"""
from fastapi import FastAPI
@ -20,7 +21,7 @@ import importlib
from core.logger import logger
from core.exception import register_exception
import typer
from scripts.initialize.initialize import InitializeData
from scripts.initialize.initialize import InitializeData, Environment
import asyncio
@ -86,22 +87,26 @@ def run():
@shell_app.command()
def init():
def init(env: Environment = Environment.pro):
"""
初始化数据
@params name: 数据库环境
"""
print("开始初始化数据")
data = InitializeData()
asyncio.run(data.run())
asyncio.run(data.run(env))
@shell_app.command()
def migrate():
def migrate(env: Environment = Environment.pro):
"""
将模型迁移到数据库更新数据库表结构
@params name: 数据库环境
"""
print("开始更新数据库表")
InitializeData().migrate_model()
InitializeData().migrate_model(env)
if __name__ == '__main__':

View File

@ -5,6 +5,8 @@
# @File : initialize.py
# @IDE : PyCharm
# @desc : 简要说明
from enum import Enum
from core.database import db_getter
from utils.excel.excel_manage import ExcelManage
from application.settings import BASE_DIR, VERSION
@ -15,6 +17,11 @@ from sqlalchemy.sql.schema import Table
import subprocess
class Environment(str, Enum):
dev = "dev"
pro = "pro"
class InitializeData:
"""
初始化数据
@ -36,13 +43,13 @@ class InitializeData:
self.__get_sheet_data()
@classmethod
def migrate_model(cls):
def migrate_model(cls, env: Environment = Environment.pro):
"""
模型迁移映射到数据库
"""
subprocess.check_call(f'alembic revision --autogenerate -m "{VERSION}"', cwd=BASE_DIR)
subprocess.check_call('alembic upgrade head', cwd=BASE_DIR)
print(f"{VERSION} 数据库表迁移完成")
subprocess.check_call(f'alembic --name {env.value} revision --autogenerate -m "{VERSION}"', cwd=BASE_DIR)
subprocess.check_call(f'alembic --name {env.value} upgrade head', cwd=BASE_DIR)
print(f"环境:{env} {VERSION} 数据库表迁移完成")
def __serializer_data(self):
"""
@ -132,11 +139,11 @@ class InitializeData:
"""
await self.__generate_data("vadmin_system_dict_details", system_models.VadminDictDetails)
async def run(self):
async def run(self, env: Environment = Environment.pro):
"""
执行初始化工作
"""
self.migrate_model()
self.migrate_model(env)
await self.generate_menu()
await self.generate_role()
await self.generate_user()
@ -145,4 +152,4 @@ class InitializeData:
await self.generate_dict_type()
await self.generate_system_config()
await self.generate_dict_details()
print(f"{VERSION} 数据已初始化完成")
print(f"环境:{env} {VERSION} 数据已初始化完成")

View File

@ -9,5 +9,6 @@
HTTP_SUCCESS = 200
HTTP_ERROR = 400
HTTP_401_UNAUTHORIZED = 401
HTTP_403_FORBIDDEN = 403
HTTP_404_NOT_FOUND = 404

14
kinit-uni/.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
######################################################################
# Build Tools
/node_modules/*
######################################################################
# Development Tools
/.idea/*
/.vscode/*
/.hbuilderx/*
package-lock.json
yarn.lock

21
kinit-uni/App.vue Normal file
View File

@ -0,0 +1,21 @@
<script>
import config from './config'
import { getToken } from '@/common/utils/auth'
export default {
onLaunch: function() {
this.initApp()
},
methods: {
//
initApp() {
//
this.$store.dispatch('InitConfig')
}
}
}
</script>
<style lang="scss">
@import '@/static/scss/index.scss';
</style>

21
kinit-uni/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 若依
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

49
kinit-uni/README.md Normal file
View File

@ -0,0 +1,49 @@
## 若依平台简介
RuoYi App 移动解决方案采用uniapp框架一份代码多终端适配同时支持APP、小程序、H5实现了与[RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue)、[RuoYi-Cloud](https://gitee.com/y_project/RuoYi-Cloud)完美对接的移动解决方案!目前已经实现登录、我的、工作台、编辑资料、头像修改、密码修改、常见问题、关于我们等基础功能。
* 应用框架基于[uniapp](https://uniapp.dcloud.net.cn/)支持小程序、H5、Android和IOS。
* 前端组件加入了[uni-ui](https://github.com/dcloudio/uni-ui)全端兼容的高性能UI框架。
## 若依技术文档
- 官网网站:[http://ruoyi.vip](http://ruoyi.vip)
- 文档地址:[http://doc.ruoyi.vip](http://doc.ruoyi.vip)
- H5页体验[http://h5.ruoyi.vip](http://h5.ruoyi.vip)
- 小程序体验
<img src="https://oscimg.oschina.net/oscnet/up-26c76dc90b92acdbd9ac8cd5252f07c8ad9.jpg" alt="小程序演示"/>
## 二次开发
是的KINIT-UNI 是在若依-移动端的基础上进行的二次开发,在此感谢若依团队!
二次开发中我们重新将接口请求改为了使用 `luch-request`,项目结构也有所改动,并且加入了 `uView UI` 组件,`uni-simple-router` 路由拦截。
开发环境HBuilder X
## 依赖插件
- [uni-read-pages](https://github.com/SilurianYang/uni-read-pages) :自动读取 `pages.json` 所有配置。
- [uni-simple-router](https://hhyang.cn/v2/start/quickstart.html) 在uni-app中使用vue-router的方式进行跳转路由路由拦截。
## 依赖组件
### color UI
- 文档地址http://docs.xzeu.com/
- 源码地址https://github.com/weilanwl/coloruicss
- 微信小程序:#小程序://ColorUI组件库/0YmCxm5PUBuChYJ
该项目已在 2019 年停止维护但因若依组件中有多处使用所以在这里不做移除新加入uView UI框架两者不冲突
### uView UI
- 源码地址https://github.com/umicro/uView2.0
- 文档地址https://uviewui.com
uView UI是[uni-app](https://uniapp.dcloud.io/)全面兼容nvue的uni-app生态框架全面的组件和便捷的工具会让您信手拈来如鱼得水

View File

@ -0,0 +1,17 @@
import request from '@/common/request/request.js'
// 登录方法
export function login(telephone, password, method) {
const data = {
telephone,
password,
method,
platform: '1'
}
return request.post(`/auth/login/`, data)
}
// 获取用户详细信息
export function getInfo() {
return request.get(`/vadmin/auth/user/current/info/`)
}

View File

@ -0,0 +1,16 @@
import request from '@/common/request/request'
// 更新当前用户基本信息
export function updateCurrentUser(data) {
return request.post(`/vadmin/auth/user/current/update/info/`, data)
}
// 重置当前用户密码
export function postCurrentUserResetPassword(data) {
return request.post(`/vadmin/auth/user/current/reset/password/`, data)
}
// 更新当前用户头像
export function postCurrentUserUploadAvatar(filePath) {
return request.upload(`/vadmin/auth/user/current/update/avatar/`, {filePath: filePath, name: 'file'})
}

View File

@ -0,0 +1,6 @@
import request from '@/common/request/request.js'
// 获取多个字典类型下的字典元素列表
export function getDictTypeDetailsApi(data) {
return request.post(`/vadmin/system/dict/types/details/`, data)
}

View File

@ -0,0 +1,6 @@
import request from '@/common/request/request'
// 获取系统配置分类
export function getSystemSettingsClassifysApi(params) {
return request.get(`/vadmin/system/settings/classifys/`, {params: params})
}

View File

@ -0,0 +1,6 @@
export default {
"401": "认证失败,无法访问系统资源",
"403": "当前操作没有权限",
"404": "访问资源不存在",
"default": "系统未知错误,请反馈给管理员"
};

View File

@ -0,0 +1,78 @@
import luchRequest from '@/components/luch-request' // 使用npm
import config from '@/config.js';
import errorCode from "@/common/request/errorCode";
import { getToken } from '@/common/utils/auth'
import { toast, showConfirm } from '@/common/utils/common'
import store from '@/store'
// luch-request插件官网https://www.quanzhan.co/luch-request/guide/3.x/#%E5%85%A8%E5%B1%80%E8%AF%B7%E6%B1%82%E9%85%8D%E7%BD%AE
// 创建luchRequest实例
console.log(config.baseUrl)
const http = new luchRequest({
baseURL: config.baseUrl,
timeout: 20000, // 请求超时时间
dataType: 'json',
custom: {
loading: true
},
sslVerify: true,
header: {}
})
// 请求拦截器
http.interceptors.request.use(
config => {
// 在发送请求之前
let token = getToken()
if (token) {
// 添加头信息token验证
config.header["Authorization"] = token
}
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截器
http.interceptors.response.use(res => {
// console.log("响应拦截器:", res)
// 未设置状态码则默认成功状态
const code = res.data.code || 200;
// 获取错误信息
const msg = res.data.message || errorCode[code] || errorCode["default"];
if (code === 500) {
toast(msg)
return Promise.reject(new Error(msg));
} else if (code === 401) {
showConfirm("登录状态已过期,您可以继续留在该页面,或者重新登录?").then(res => {
if (res.confirm) {
store.dispatch('LogOut')
}
})
return Promise.reject("error");
} else if (code !== 200) {
toast(msg)
return Promise.reject("error");
} else {
return res.data;
}
},
error => {
console.log("请求状态码服务器直接报错", error);
let { errMsg } = error;
if (errMsg == "request:fail") {
errMsg = "接口连接异常";
} else if (errMsg == "request:fail timeout") {
errMsg = "接口连接超时";
} else {
errMsg = error.data.message;
}
toast(errMsg)
return Promise.reject(error);
}
);
export default http

View File

@ -0,0 +1,13 @@
const TokenKey = 'App-Token'
export function getToken() {
return uni.getStorageSync(TokenKey)
}
export function setToken(token) {
return uni.setStorageSync(TokenKey, token)
}
export function removeToken() {
return uni.removeStorageSync(TokenKey)
}

View File

@ -0,0 +1,54 @@
/**
* 显示消息提示框
* @param content 提示的标题
*/
export function toast(content) {
uni.showToast({
icon: 'none',
title: content
})
}
/**
* 显示模态弹窗
* @param content 提示的标题
*/
export function showConfirm(content) {
return new Promise((resolve, reject) => {
uni.showModal({
title: '提示',
content: content,
cancelText: '取消',
confirmText: '确定',
success: function(res) {
resolve(res)
}
})
})
}
/**
* 参数处理
* @param params 参数
*/
export function tansParams(params) {
let result = ''
for (const propName of Object.keys(params)) {
const value = params[propName]
var part = encodeURIComponent(propName) + "="
if (value !== null && value !== "" && typeof (value) !== "undefined") {
if (typeof value === 'object') {
for (const key of Object.keys(value)) {
if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') {
let params = propName + '[' + key + ']'
var subPart = encodeURIComponent(params) + "="
result += subPart + encodeURIComponent(value[key]) + "&"
}
}
} else {
result += part + encodeURIComponent(value) + "&"
}
}
}
return result
}

View File

@ -0,0 +1,12 @@
const constant = {
avatar: 'vuex_avatar',
name: 'vuex_name',
nickname: 'vuex_nickname',
telephone: 'vuex_telephone',
isUser: 'vuex_isUser',
roles: 'vuex_roles',
create_datetime: 'vuex_createDatetime',
permissions: 'vuex_permissions'
}
export default constant

View File

@ -0,0 +1,80 @@
const TokenKey = 'Admin-Token'
// 获取客户端token
export function getToken() {
try {
const value = uni.getStorageSync(TokenKey);
if (value) {
return value;
}
return ""
} catch (e) {
// error
return ""
}
}
// 设置客户端token
export function setToken(token) {
uni.setStorage({
key: TokenKey,
data: token,
success: function (res) {
console.log('成功存储token');
},
fail:function(e){
console.log(e)
console.log("存储token失败");
}
});
}
// 删除客户端token
export function removeToken() {
uni.removeStorage({
key: TokenKey,
success: function (res) {
console.log('成功删除token');
}
});
}
// 获取客户端
export function getStorage(key) {
try {
const value = uni.getStorageSync(key);
if (value) {
// console.log("成功获取到 Storage", value);
return value;
}
return ""
} catch (e) {
// error
return ""
}
}
// 设置客户端 Storage
export function setStorage(key, value) {
uni.setStorage({
key: key,
data: value,
success: function (res) {
console.log('成功存储');
},
fail:function(e){
console.log(e)
console.log("存储失败");
}
});
}
// 删除客户端 Storage
export function removeStorage(key) {
uni.removeStorage({
key: key,
success: function (res) {
console.log('成功删除Storage');
}
});
}

View File

@ -0,0 +1,30 @@
var log = wx.getRealtimeLogManager ? wx.getRealtimeLogManager() : null
module.exports = {
debug() {
if (!log) return
log.debug.apply(log, arguments)
},
info() {
if (!log) return
log.info.apply(log, arguments)
},
warn() {
if (!log) return
log.warn.apply(log, arguments)
},
error() {
if (!log) return
log.error.apply(log, arguments)
},
setFilterMsg(msg) { // 从基础库2.7.3开始支持
if (!log || !log.setFilterMsg) return
if (typeof msg !== 'string') return
log.setFilterMsg(msg)
},
addFilterMsg(msg) { // 从基础库2.8.1开始支持
if (!log || !log.addFilterMsg) return
if (typeof msg !== 'string') return
log.addFilterMsg(msg)
}
}

View File

@ -0,0 +1,51 @@
import store from '@/store'
/**
* 字符权限校验
* @param {Array} value 校验值
* @returns {Boolean}
*/
export function checkPermi(value) {
if (value && value instanceof Array && value.length > 0) {
const permissions = store.getters && store.getters.permissions
const permissionDatas = value
const all_permission = "*:*:*"
const hasPermission = permissions.some(permission => {
return all_permission === permission || permissionDatas.includes(permission)
})
if (!hasPermission) {
return false
}
return true
} else {
console.error(`need roles! Like checkPermi="['system:user:add','system:user:edit']"`)
return false
}
}
/**
* 角色权限校验
* @param {Array} value 校验值
* @returns {Boolean}
*/
export function checkRole(value) {
if (value && value instanceof Array && value.length > 0) {
const roles = store.getters && store.getters.roles
const permissionRoles = value
const super_admin = "admin"
const hasRole = roles.some(role => {
return super_admin === role || permissionRoles.includes(role)
})
if (!hasRole) {
return false
}
return true
} else {
console.error(`need roles! Like checkRole="['admin','editor']"`)
return false
}
}

View File

@ -0,0 +1,39 @@
/**
uniapp 上传文件到后台接口
官方文档https://uniapp.dcloud.io/api/request/network-file.html#uploadfile
博客https://www.jianshu.com/p/71ad2f45120c
*/
import { API_BASE_URL } from '@/common/setting/index'
import { getToken, removeToken, getStorage } from '@/common/utils/cookies'
// 单个文件上传
export function uploadFile(api, file, data={}) {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: API_BASE_URL + api,
filePath: file,
name: 'file',
timeout: 60000,
formData: data,
header: {
ossign: getStorage("ossign"),
Authorization: getToken()
},
success: (res) => {
let data = JSON.parse(res.data);
if (data.code !== 200) {
reject(data);
}
resolve(data);
},
fail: (err) => {
console.log("上传失败", err);
reject(err);
}
});
})
}

View File

@ -0,0 +1,191 @@
/**
* 通用js方法封装处理
* Copyright (c) 2019 ruoyi
*/
/**
* 获取任意日期
* date: 时间戳
* AddDayCount: 在时间戳的基础上加减天数
* 示例
* getDate(new Date(),-3).fullDate # 三天前的日期
* getDate(new Date()).fullDate # 今天的日期
* getDate(new Date(), 3).fullDate # 三天后的日期
*/
export function getDate(date, AddDayCount = 0) {
if (!date) {
date = new Date()
}
if (typeof date !== 'object') {
date = date.replace(/-/g, '/')
}
const dd = new Date(date)
dd.setDate(dd.getDate() + AddDayCount) // 获取AddDayCount天后的日期
const y = dd.getFullYear()
const m = dd.getMonth() + 1 < 10 ? '0' + (dd.getMonth() + 1) : dd.getMonth() + 1 // 获取当前月份的日期不足10补0
const d = dd.getDate() < 10 ? '0' + dd.getDate() : dd.getDate() // 获取当前几号不足10补0
return {
fullDate: y + '-' + m + '-' + d,
year: y,
month: m,
date: d,
day: dd.getDay()
}
}
// 日期格式化
export function parseTime(time, pattern) {
if (arguments.length === 0 || !time) {
return null;
}
const format = pattern || "{y}-{m}-{d} {h}:{i}:{s}";
let date;
if (typeof time === "object") {
date = time;
} else {
if ((typeof time === "string") && (/^[0-9]+$/.test(time))) {
time = parseInt(time);
} else if (typeof time === "string") {
time = time.replace(new RegExp(/-/gm), "/");
}
if ((typeof time === "number") && (time.toString().length === 10)) {
time = time * 1000;
}
date = new Date(time);
}
const formatObj = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay()
};
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
let value = formatObj[key];
// Note: getDay() returns 0 on Sunday
if (key === "a") { return ["", "", "", "", "", "", ""][value]; }
if (result.length > 0 && value < 10) {
value = "0" + value;
}
return value || 0;
});
return time_str;
}
// 表单重置
export function resetForm(refName) {
if (this.$refs[refName]) {
this.$refs[refName].resetFields();
}
}
// 添加日期范围
export function addDateRange(params, dateRange, propName) {
const search = JSON.parse(JSON.stringify(params));
if (dateRange != null && dateRange !== "" && dateRange.length !== 0) {
search.as = JSON.stringify({ create_datetime__range: dateRange });
}
return search;
}
// 回显数据字典
export function selectDictLabel(datas, value) {
var actions = [];
Object.keys(datas).some((key) => {
if (String(datas[key].dictValue) === ("" + String(value))) {
actions.push(datas[key].dictLabel);
return true;
}
});
return actions.join("");
}
// 获取字典默认值
export function selectDictDefault(datas) {
var actions = [];
Object.keys(datas).some((key) => {
if (datas[key].is_default === true) {
actions.push(datas[key].dictValue);
return true;
}
});
if (!actions[0] && datas[0]) {
actions.push(datas[0].dictValue);
}
return actions.join("");
}
// 回显数据字典字符串数组
export function selectDictLabels(datas, value, separator) {
var actions = [];
var currentSeparator = undefined === separator ? "," : separator;
var temp = value.split(currentSeparator);
Object.keys(value.split(currentSeparator)).some((val) => {
Object.keys(datas).some((key) => {
if (datas[key].dictValue == ("" + temp[val])) {
actions.push(datas[key].dictLabel + currentSeparator);
}
});
});
return actions.join("").substring(0, actions.join("").length - 1);
}
// 转换字符串undefined,null等转化为""
export function praseStrEmpty(str) {
if (!str || str == "undefined" || str == "null") {
return "";
}
return str;
}
// js模仿微信朋友圈计算时间显示几天/几小时/几分钟/刚刚
//datetime 格式为2019-11-22 12:23:59样式
export function timeConversion(datetime) { //dateTimeStamp是一个时间毫秒注意时间戳是秒的形式在这个毫秒的基础上除以1000就是十位数的时间戳13位数的都是时间毫秒
// var dateTimeStamp = new Date(datetime.replace(/ /, 'T')).getTime()-8 * 60 * 60 * 1000;//这里要减去中国的时区8小时
var dateTimeStamp = new Date(datetime.replace(/ /, 'T')).getTime();//这里不减去中国的时区8小时
var minute = 1000 * 60; //把分半个月一个月用毫秒表示
var hour = minute * 60;
var day = hour * 24;
var week = day * 7;
var halfamonth = day * 15;
var month = day * 30;
var now = new Date().getTime(); //获取当前时间毫秒
var diffValue = now - dateTimeStamp; //时间差
if (diffValue < 0) {
return '刚刚';
}
var minC = diffValue / minute; //计算时间差的分
var hourC = diffValue / hour;
var dayC = diffValue / day;
var weekC = diffValue / week;
var monthC = diffValue / month;
var result = "2";
if (monthC >= 1 && monthC <= 3) {
result = " " + parseInt(monthC) + "月前"
} else if (weekC >= 1 && weekC <= 3) {
result = " " + parseInt(weekC) + "周前"
} else if (dayC >= 1 && dayC <= 6) {
result = " " + parseInt(dayC) + "天前"
} else if (hourC >= 1 && hourC <= 23) {
result = " " + parseInt(hourC) + "小时前"
} else if (minC >= 1 && minC <= 59) {
result = " " + parseInt(minC) + "分钟前"
} else if (diffValue >= 0 && diffValue <= minute) {
result = "刚刚"
} else {
var datetime = new Date();
datetime.setTime(dateTimeStamp);
var Nyear = datetime.getFullYear(); {}
var Nmonth = datetime.getMonth() + 1 < 10 ? "0" + (datetime.getMonth() + 1) : datetime.getMonth() + 1;
var Ndate = datetime.getDate() < 10 ? "0" + datetime.getDate() : datetime.getDate();
var Nhour = datetime.getHours() < 10 ? "0" + datetime.getHours() : datetime.getHours();
var Nminute = datetime.getMinutes() < 10 ? "0" + datetime.getMinutes() : datetime.getMinutes();
var Nsecond = datetime.getSeconds() < 10 ? "0" + datetime.getSeconds() : datetime.getSeconds();
result = Nyear + "-" + Nmonth + "-" + Ndate
}
return result;
}

View File

@ -0,0 +1,33 @@
import constant from './constant'
// 存储变量名
let storageKey = 'storage_data'
// 存储节点变量名
let storageNodeKeys = [constant.avatar, constant.name, constant.roles, constant.permissions]
// 存储的数据
let storageData = uni.getStorageSync(storageKey) || {}
const storage = {
set: function(key, value) {
if (storageNodeKeys.indexOf(key) != -1) {
let tmp = uni.getStorageSync(storageKey)
tmp = tmp ? tmp : {}
tmp[key] = value
uni.setStorageSync(storageKey, tmp)
}
},
get: function(key) {
return storageData[key] || ""
},
remove: function(key) {
delete storageData[key]
uni.setStorageSync(storageKey, storageData)
},
clean: function() {
uni.removeStorageSync(storageKey)
}
}
export default storage

View File

@ -0,0 +1,68 @@
import store from '@/store'
import config from '@/config'
import { getToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import { toast, showConfirm, tansParams } from '@/common/utils/common'
let timeout = 10000
const baseUrl = config.baseUrl
const upload = config => {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false
config.header = config.header || {}
if (getToken() && !isToken) {
config.header['Authorization'] = 'Bearer ' + getToken()
}
// get请求映射params参数
if (config.params) {
let url = config.url + '?' + tansParams(config.params)
url = url.slice(0, -1)
config.url = url
}
return new Promise((resolve, reject) => {
uni.uploadFile({
timeout: config.timeout || timeout,
url: baseUrl + config.url,
filePath: config.filePath,
name: config.name || 'file',
header: config.header,
formData: config.formData,
success: (res) => {
let result = JSON.parse(res.data)
const code = result.code || 200
const msg = errorCode[code] || result.msg || errorCode['default']
if (code === 200) {
resolve(result)
} else if (code == 401) {
showConfirm("登录状态已过期,您可以继续留在该页面,或者重新登录?").then(res => {
if (res.confirm) {
store.dispatch('LogOut')
}
})
reject('无效的会话,或者会话已过期,请重新登录。')
} else if (code === 500) {
toast(msg)
reject('500')
} else if (code !== 200) {
toast(msg)
reject(code)
}
},
fail: (error) => {
let { message } = error
if (message == 'Network Error') {
message = '后端接口连接异常'
} else if (message.includes('timeout')) {
message = '系统接口请求超时'
} else if (message.includes('Request failed with status code')) {
message = '系统接口' + message.substr(message.length - 3) + '异常'
}
toast(message)
reject(error)
}
})
})
}
export default upload

View File

@ -0,0 +1,57 @@
## 3.0.7 (2021-09-04)
1. Bug Fix: 修复通过 `Request.config` 设置全局参数,多个实例`config`存在共同引用bug
## 3.0.6 (2021-05-10)
1. New Feature: APP端 增加`responseType`配置项
## 3.0.5 (2021-01-10)
### Features
* [重要] APP不再支持`CONNECT``HEAD``TRACE`请求方式。[uni.request](https://uniapp.dcloud.io/api/request/request)
* [重要]全局默认`timeout``30000`ms,改为`60000`ms
* [重要]增加`index.d.ts`文件支持。感谢`Mr_Mao`的支持。github:`https://github.com/TuiMao233`
* [重要]网络请求相关接口 uni.request、uni.uploadFile、uni.downloadFile 支持 timeout 参数。
* [重要]返回结果response 增加`fullPath`参数。
## 3.0.4 (2020-07-05)
1. New Feature: request 方法增加 ` firstIpv4 `配置项
1. New Feature: 增加 ` middleware `通用请求方法
## 3.0.3 (2020-06-16)
1. Bug Fix: 修复` params ` 选项对数组格式化错误bug
## 3.0.2 (2020-06-04)
1. Bug Fix: 修复文件上传和request 配置缺少字段bug
## 3.0.1 (2020-06-02)
1. Bug Fix: 请求方式都为` GET `的bug
## 3.0.0 (2020-06-01)
1. New Feature: 支持多拦截器
1. New Feature: 支持局部配置自定义验证器
## 2.0.1 (2020-05-01)
1. Bug Fix: 修复多实例全局配置共用问题
## 2.0.0 (2020-04-24)
1. New Feature: 增加 request ` withCredentials `选项仅h5端支持
1. New Feature: h5端 upload 增加 ` files ` ` file `选项。[uni.uploadFile](https://uniapp.dcloud.io/api/request/network-file?id=uploadfile "uni.uploadFile")
1. Enhancement: ` params ` 选项参数格式化方法使用axios 格式化方法
1. Bug Fix: 对upload 返回data 为空字符串的情况容错
1. Change: 修改header与全局合并方式。当前header = Object.assign(全局,局部)
## 0.0.0 (2019-05)
1. luch-request created

View File

@ -0,0 +1,132 @@
{
"_from": "luch-request",
"_id": "luch-request@3.0.7",
"_inBundle": false,
"_integrity": "sha512-rYTgAO0CiAMjuotvwhtEREQ6mh/qdgYO2a+cKDv2VPnww/MdNaHMGMpsPY6VvJCOtfH7kV9lDujjuihhlu1xmg==",
"_location": "/luch-request",
"_phantomChildren": {},
"_requested": {
"type": "tag",
"registry": true,
"raw": "luch-request",
"name": "luch-request",
"escapedName": "luch-request",
"rawSpec": "",
"saveSpec": null,
"fetchSpec": "latest"
},
"_requiredBy": [
"#USER",
"/"
],
"_resolved": "https://registry.npmjs.org/luch-request/-/luch-request-3.0.7.tgz",
"_shasum": "5894fc3d3ff62622b042c84953c1192ccdeafaa5",
"_spec": "luch-request",
"_where": "E:\\ktianc\\base\\vvandk",
"author": {
"name": "luch"
},
"bugs": {
"url": "https://github.com/lei-mu/luch-request/issues"
},
"bundleDependencies": false,
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
},
"ghooks": {
"commit-msg": "validate-commit-msg"
},
"validate-commit-msg": {
"types": [
"feat",
"fix",
"docs",
"style",
"refactor",
"perf",
"test",
"build",
"ci",
"chore",
"revert"
],
"scope": {
"required": false,
"allowed": [
"*"
],
"validate": false,
"multiple": false
},
"warnOnFail": false,
"maxSubjectLength": 100,
"subjectPattern": ".+",
"subjectPatternErrorMsg": "subject does not match subject pattern!",
"helpMessage": "",
"autoFix": false
}
},
"dependencies": {
"@dcloudio/types": "^2.0.16"
},
"deprecated": false,
"description": "基于Promise实现uni-app request 请求插件",
"devDependencies": {
"@babel/core": "^7.9.0",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-transform-arrow-functions": "^7.8.3",
"@babel/preset-env": "^7.9.5",
"@vuepress/plugin-active-header-links": "^1.5.0",
"@vuepress/plugin-pwa": "^1.5.2",
"archiver": "^4.0.1",
"babel-eslint": "^10.1.0",
"babel-preset-es2015": "^6.24.1",
"commitizen": "^4.2.4",
"conventional-changelog-cli": "^2.1.1",
"cz-conventional-changelog": "^3.2.0",
"eslint": "^6.8.0",
"grunt": "^1.1.0",
"grunt-babel": "^8.0.0",
"load-grunt-tasks": "^5.1.0",
"node-zip": "^1.1.1",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-copy": "^3.3.0",
"rollup-plugin-eslint": "^7.0.0",
"rollup-plugin-json": "^4.0.0",
"rollup-plugin-live-server": "^1.0.3",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-replace": "^2.2.0",
"rollup-plugin-uglify": "^6.0.4",
"rollup-plugin-zip": "^1.0.0",
"validate-commit-msg": "^2.14.0",
"vuepress": "^1.4.1"
},
"homepage": "https://www.quanzhan.co/luch-request/",
"keywords": [
"uni-app",
"request",
"Promise",
"luch",
"luch-request"
],
"license": "MIT",
"main": "src/lib/luch-request.js",
"name": "luch-request",
"repository": {
"type": "git",
"url": "git+https://github.com/lei-mu/luch-request.git"
},
"scripts": {
"build": "rimraf DCloud && rollup -c --environment NODE_ENV:production",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -w -r 0",
"dev": "rollup -c",
"docs": "vuepress dev docs",
"docs:build": "vuepress build docs",
"test": "echo \"Error: no test specified\" && exit 1",
"watch": "rollup -c -w",
"zipD": "node node/zipDCloudPlugin.js && node node/zipDCloudDemo.js"
},
"version": "3.0.7"
}

View File

@ -0,0 +1,260 @@
# luch-request
[![npm](https://img.shields.io/npm/l/luch-request "npm")](https://www.npmjs.com/package/luch-request "npm")
[![npm](https://img.shields.io/npm/v/luch-request "npm")](https://www.npmjs.com/package/luch-request "npm")
[![github](https://img.shields.io/github/package-json/v/lei-mu/luch-request "github")](https://github.com/lei-mu/luch-request "github")
[![github stars](https://img.shields.io/github/stars/lei-mu/luch-request.svg "github stars")](https://github.com/lei-mu/luch-request "github stars")
[![github forks](https://img.shields.io/github/forks/lei-mu/luch-request.svg "github forks")](https://github.com/lei-mu/luch-request "github forks")
- 基于 Promise 对象实现更简单的 request 使用方式,支持请求和响应拦截
- 支持全局挂载
- 支持多个全局配置实例
- 支持自定义验证器
- 支持文件上传/下载
- 支持task 操作
- 支持自定义参数
- 支持多拦截器
- 对参数的处理比uni.request 更强
安装
------------
###### 使用npm
``` javascript
npm i luch-request -S
```
使用npm前阅读[快速上手](https://www.quanzhan.co/luch-request/handbook/#npm "快速上手")
###### github
[github](https://github.com/lei-mu/luch-request "github")
安装依赖后 ` npm run build ` 使用DCloud/luch-request 文件夹即可
###### DCloud插件市场:
[DCloud插件市场](https://ext.dcloud.net.cn/plugin?id=392 "DCloud插件市场")
Example
------------
创建实例
``` javascript
import Request from '@/utils/luch-request/index.js' // 下载的插件
// import Request from 'luch-request' // 使用npm
const http = new Request();
```
执行` GET `请求
``` javascript
http.get('/user/login', {params: {userName: 'name', password: '123456'}}).then(res => {
}).catch(err => {
})
// 局部修改配置,局部配置优先级高于全局配置
http.get('/user/login', {
params: {userName: 'name', password: '123456'}, /* 会加在url上 */
header: {}, /* 会与全局header合并如有同名属性局部覆盖全局 */
dataType: 'json',
// 注如果局部custom与全局custom有同名属性则后面的属性会覆盖前面的属性相当于Object.assign(全局,局部)
custom: {auth: true}, // 可以加一些自定义参数在拦截器等地方使用。比如这里我加了一个auth可在拦截器里拿到如果true就传token
// #ifndef MP-ALIPAY
responseType: 'text',
// #endif
// #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN
timeout: 60000, // H5(HBuilderX 2.9.9+)、APP(HBuilderX 2.9.9+)、微信小程序2.10.0)、支付宝小程序
// #endif
// #ifdef APP-PLUS
sslVerify: true, // 验证 ssl 证书 仅5+App安卓端支持HBuilderX 2.3.3+
// #endif
// #ifdef H5
withCredentials: false, // 跨域请求时是否携带凭证cookies仅H5支持HBuilderX 2.6.15+
// #endif
// 返回当前请求的task, options。请勿在此处修改options。非必填
getTask: (task, options) => {
// setTimeout(() => {
// task.abort()
// }, 500)
},
// 自定义验证器。statusCode必存在。非必填
validateStatus: function validateStatus(statusCode) {
return statusCode >= 200 && statusCode < 300
}
}).then(res => {
}).catch(err => {
})
```
执行` POST `请求
``` javascript
http.post('/user/login', {userName: 'name', password: '123456'} ).then(res => {
}).catch(err => {
})
// 局部修改配置,局部配置优先级高于全局配置
http.post('/user/login', {userName: 'name', password: '123456'}, {
params: {}, /* 会加在url上 */
header: {}, /* 会与全局header合并如有同名属性局部覆盖全局 */
dataType: 'json',
// 注如果局部custom与全局custom有同名属性则后面的属性会覆盖前面的属性相当于Object.assign(全局,局部)
custom: {auth: true}, // 可以加一些自定义参数在拦截器等地方使用。比如这里我加了一个auth可在拦截器里拿到如果true就传token
// #ifndef MP-ALIPAY
responseType: 'text',
// #endif
// #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN
timeout: 60000, // H5(HBuilderX 2.9.9+)、APP(HBuilderX 2.9.9+)、微信小程序2.10.0)、支付宝小程序
// #endif
// #ifdef APP-PLUS
sslVerify: true, // 验证 ssl 证书 仅5+App安卓端支持HBuilderX 2.3.3+
// #endif
// #ifdef H5
withCredentials: false, // 跨域请求时是否携带凭证cookies仅H5支持HBuilderX 2.6.15+
// #endif
// 返回当前请求的task, options。请勿在此处修改options。非必填
getTask: (task, options) => {
// setTimeout(() => {
// task.abort()
// }, 500)
},
// 自定义验证器。statusCode必存在。非必填
validateStatus: function validateStatus(statusCode) {
return statusCode >= 200 && statusCode < 300
}
}).then(res => {
}).catch(err => {
})
```
执行` upload `请求
``` javascript
http.upload('api/upload/img', {
params: {}, /* 会加在url上 */
// #ifdef APP-PLUS || H5
files: [], // 需要上传的文件列表。使用 files 时filePath 和 name 不生效。App、H5 2.6.15+
// #endif
// #ifdef MP-ALIPAY
fileType: 'image/video/audio', // 仅支付宝小程序,且必填。
// #endif
filePath: '', // 要上传文件资源的路径。
// 注如果局部custom与全局custom有同名属性则后面的属性会覆盖前面的属性相当于Object.assign(全局,局部)
custom: {auth: true}, // 可以加一些自定义参数在拦截器等地方使用。比如这里我加了一个auth可在拦截器里拿到如果true就传token
name: 'file', // 文件对应的 key , 开发者在服务器端通过这个 key 可以获取到文件二进制内容
// #ifdef H5 || APP-PLUS
timeout: 60000, // H5(HBuilderX 2.9.9+)、APP(HBuilderX 2.9.9+)
// #endif
header: {}, /* 会与全局header合并如有同名属性局部覆盖全局 */
formData: {}, // HTTP 请求中其他额外的 form data
// 返回当前请求的task, options。请勿在此处修改options。非必填
getTask: (task, options) => {
// task.onProgressUpdate((res) => {
// console.log('上传进度' + res.progress);
// console.log('已经上传的数据长度' + res.totalBytesSent);
// console.log('预期需要上传的数据总长度' + res.totalBytesExpectedToSend);
//
// // 测试条件,取消上传任务。
// if (res.progress > 50) {
// uploadTask.abort();
// }
// });
}
}).then(res => {
// 返回的res.data 已经进行JSON.parse
}).catch(err => {
})
```
luch-request Guide
------------
[luch-request 官网地址](https://www.quanzhan.co/luch-request/ "luch-request 官网地址")
<br>
[github](https://github.com/lei-mu/luch-request "github")
友情链接
------------
#### vue-admin-beautiful
**vue-admin-beautiful** ——<a href="https://github.com/chuzhixin/vue-admin-beautiful" target="_blank">企业级、通用型中后台前端解决方案基于vue/cli 4 最新版,同时支持电脑,手机,平板)</a>
**vue-admin-beautiful** ——<a href="http://beautiful.panm.cn/vue-admin-beautiful/#/index" target="_blank">在线演示</a>
#### uView
<a href="https://uviewui.com" target="_blank">uView 文档</a> ——超棒的移动跨端框架,文档详细,上手容易
常见问题
------------
1. 为什么会请求两次?
- 总有些小白问这些很那啥的问题有两种可能一种是post三次握手还有一种可能是`本地访问接口时跨域请求所以浏览器会先发一个option 去预测能否成功,然后再发一个真正的请求`自己观察请求头Request Method百度简单请求
2. 如何跨域?
- 问的人不少,可以先百度了解一下。<a href="https://ask.dcloud.net.cn/article/35267" target="_blank">如何跨域</a>
3. post 怎么传不了数组的参数啊?
- <a href="https://uniapp.dcloud.io/api/request/request" target="_blank">uni-request</a> <br>
可以点击看一下uni-request 的api 文档data支持的文件类型只有<code>Object/String/ArrayBuffer</code>这个真跟我没啥关系 0.0
4. TypeError: undefined is not an object (evaluating 'this.$http.get')
- 不知道为啥问的人这么多?太基础了,百度学习一下 export default 和export头大。
- `import { http } from '@/utils/luch-request/index.js'`
5. 什么参数需要在` setConfig ` 设置?什么参数需要在` request ` 拦截器设置?
- ` setConfig ` 适用于设置一些静态的/默认的参数比如header 里的一些默认值、默认全局参数(全局请求配置)。` token ` 并不适合在这里设置。
- ` interceptors.request ` 拦截器适用范围较广,但我仍然建议把一些静态的东西放在 ` setConfig ` 里。拦截器会在每次请求调用,而 ` setConfig ` 仅在调用时修改一遍。
tip
------------
- nvue 不支持全局挂载
- 当前的hbuilderx 版本号beat-3.0.4 alpha-3.0.4
- 推荐使用下载插件的方式使用。如果本插件完全满足你的需求可直接使用 ` npm `安装
- license: MIT
issue
------------
- DCloud: 有任何问题或者建议可以=> <a href="https://ask.dcloud.net.cn/question/74922" target="_blank">issue提交</a>,先给个五星好评QAQ!!
- github: [Issues](https://github.com/lei-mu/luch-request/issues "Issues")
作者想说
------------
- 写代码很容易为了让你们看懂写文档真的很lei 0.0
- 最近发现有插件与我雷同当初接触uni-app 就发现插件市场虽然有封装的不错的request库但是都没有对多全局配置做处理都是通过修改源码的方式配置。我首先推出通过class类并仿照axios的api实现request请求库并起名仿axios封装request网络请求库支持拦截器全局配置。他们虽然修改了部分代码但是功能与性能并没有优化反而使代码很冗余。希望能推出新的功能和性能更加强悍的请求库。2019-05
- 任何形式的‘参考’、‘借鉴’,请标明作者
```javascript
<a href="https://ext.dcloud.net.cn/plugin?id=392">luch-request</a>
```
- 关于问问题
1. 首先请善于利用搜索引擎不管百度还是Google遇到问题请先自己尝试解决。自己尝试过无法解决再问。
2. 不要问类似为什么我的xx无法使用这种问题。请仔细阅读文档检查代码或者说明运行环境把相关代码贴至评论或者发送至我的邮箱还可以点击上面的issue提交在里面提问可能我在里面已经回答了。
3. 我的代码如果真的出现bug,或者你有好的建议、需求可以提issue,我看到后会立即解决
- 如何问问题
1. 问问题之前请换位思考,如果自己要解决这个问题,需要哪些信息
2. 仔细阅读文档,检查代码
3. 说明运行环境比如app端 ios、android 版本号、手机机型、普遍现象还是个别现象(越详细越好)
4. 发出代码片段或者截图至邮箱(很重要)
5. 或者可以在上方的'issue提交' 里发出详细的问题描述
6. 以上都觉得解决不了你的问题可以加QQ:`370306150`
个人网站
------------
- 欢迎大家都来踩一踩<a href="https://www.quanzhan.co/" target="_blank">luch的博客</a> 0.0
土豪赞赏
------------
[![wechat 打赏](https://oss.quanzhan.co/images/common/my-wechat-qrcode.png?x-oss-process=image/resize,m_lfit,h_150,w_150 "wechat 打赏")](https://www.quanzhan.co/luch-request/acknowledgement/#前言 "wechat 打赏")
[![支付宝 打赏](https://oss.quanzhan.co/images/common/my-alipay-qrcode.jpg?x-oss-process=image/resize,m_lfit,h_150,w_150 "支付宝 打赏")](https://www.quanzhan.co/luch-request/acknowledgement/#前言 "支付宝 打赏")
[打赏事宜具体说明](https://www.quanzhan.co/luch-request/acknowledgement/#前言 "打赏事宜具体说明")
###### 您的鼓励是我更新的动力
#### 创作不易,五星好评你懂得!

View File

@ -0,0 +1,99 @@
import buildURL from '../helpers/buildURL'
import buildFullPath from '../core/buildFullPath'
import settle from '../core/settle'
import { isUndefined } from "../utils"
/**
* 返回可选值存在的配置
* @param {Array} keys - 可选值数组
* @param {Object} config2 - 配置
* @return {{}} - 存在的配置项
*/
const mergeKeys = (keys, config2) => {
let config = {}
keys.forEach(prop => {
if (!isUndefined(config2[prop])) {
config[prop] = config2[prop]
}
})
return config
}
export default (config) => {
return new Promise((resolve, reject) => {
let fullPath = buildURL(buildFullPath(config.baseURL, config.url), config.params)
const _config = {
url: fullPath,
header: config.header,
complete: (response) => {
config.fullPath = fullPath
response.config = config
try {
// 对可能字符串不是json 的情况容错
if (typeof response.data === 'string') {
response.data = JSON.parse(response.data)
}
// eslint-disable-next-line no-empty
} catch (e) {
}
settle(resolve, reject, response)
}
}
let requestTask
if (config.method === 'UPLOAD') {
delete _config.header['content-type']
delete _config.header['Content-Type']
let otherConfig = {
// #ifdef MP-ALIPAY
fileType: config.fileType,
// #endif
filePath: config.filePath,
name: config.name
}
const optionalKeys = [
// #ifdef APP-PLUS || H5
'files',
// #endif
// #ifdef H5
'file',
// #endif
// #ifdef H5 || APP-PLUS
'timeout',
// #endif
'formData'
]
requestTask = uni.uploadFile({..._config, ...otherConfig, ...mergeKeys(optionalKeys, config)})
} else if (config.method === 'DOWNLOAD') {
// #ifdef H5 || APP-PLUS
if (!isUndefined(config['timeout'])) {
_config['timeout'] = config['timeout']
}
// #endif
requestTask = uni.downloadFile(_config)
} else {
const optionalKeys = [
'data',
'method',
// #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN
'timeout',
// #endif
'dataType',
// #ifndef MP-ALIPAY
'responseType',
// #endif
// #ifdef APP-PLUS
'sslVerify',
// #endif
// #ifdef H5
'withCredentials',
// #endif
// #ifdef APP-PLUS
'firstIpv4',
// #endif
]
requestTask = uni.request({..._config,...mergeKeys(optionalKeys, config)})
}
if (config.getTask) {
config.getTask(requestTask, config)
}
})
}

View File

@ -0,0 +1,51 @@
'use strict'
function InterceptorManager() {
this.handlers = []
}
/**
* Add a new interceptor to the stack
*
* @param {Function} fulfilled The function to handle `then` for a `Promise`
* @param {Function} rejected The function to handle `reject` for a `Promise`
*
* @return {Number} An ID used to remove interceptor later
*/
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
})
return this.handlers.length - 1
}
/**
* Remove an interceptor from the stack
*
* @param {Number} id The ID that was returned by `use`
*/
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null
}
}
/**
* Iterate over all the registered interceptors
*
* This method is particularly useful for skipping over any
* interceptors that may have become `null` calling `eject`.
*
* @param {Function} fn The function to call for each interceptor
*/
InterceptorManager.prototype.forEach = function forEach(fn) {
this.handlers.forEach(h => {
if (h !== null) {
fn(h)
}
})
}
export default InterceptorManager

View File

@ -0,0 +1,200 @@
/**
* @Class Request
* @description luch-request http请求插件
* @version 3.0.7
* @Author lu-ch
* @Date 2021-09-04
* @Email webwork.s@qq.com
* 文档: https://www.quanzhan.co/luch-request/
* github: https://github.com/lei-mu/luch-request
* DCloud: http://ext.dcloud.net.cn/plugin?id=392
* HBuilderX: beat-3.0.4 alpha-3.0.4
*/
import dispatchRequest from './dispatchRequest'
import InterceptorManager from './InterceptorManager'
import mergeConfig from './mergeConfig'
import defaults from './defaults'
import { isPlainObject } from '../utils'
import clone from '../utils/clone'
export default class Request {
/**
* @param {Object} arg - 全局配置
* @param {String} arg.baseURL - 全局根路径
* @param {Object} arg.header - 全局header
* @param {String} arg.method = [GET|POST|PUT|DELETE|CONNECT|HEAD|OPTIONS|TRACE] - 全局默认请求方式
* @param {String} arg.dataType = [json] - 全局默认的dataType
* @param {String} arg.responseType = [text|arraybuffer] - 全局默认的responseType支付宝小程序不支持
* @param {Object} arg.custom - 全局默认的自定义参数
* @param {Number} arg.timeout - 全局默认的超时时间单位 ms默认60000H5(HBuilderX 2.9.9+)APP(HBuilderX 2.9.9+)微信小程序2.10.0支付宝小程序
* @param {Boolean} arg.sslVerify - 全局默认的是否验证 ssl 证书默认true.仅App安卓端支持HBuilderX 2.3.3+
* @param {Boolean} arg.withCredentials - 全局默认的跨域请求时是否携带凭证cookies默认false仅H5支持HBuilderX 2.6.15+
* @param {Boolean} arg.firstIpv4 - 全DNS解析时优先使用ipv4默认false App-Android 支持 (HBuilderX 2.8.0+)
* @param {Function(statusCode):Boolean} arg.validateStatus - 全局默认的自定义验证器默认statusCode >= 200 && statusCode < 300
*/
constructor(arg = {}) {
if (!isPlainObject(arg)) {
arg = {}
console.warn('设置全局参数必须接收一个Object')
}
this.config = clone({...defaults, ...arg})
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
}
}
/**
* @Function
* @param {Request~setConfigCallback} f - 设置全局默认配置
*/
setConfig(f) {
this.config = f(this.config)
}
middleware(config) {
config = mergeConfig(this.config, config)
let chain = [dispatchRequest, undefined]
let promise = Promise.resolve(config)
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected)
})
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected)
})
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift())
}
return promise
}
/**
* @Function
* @param {Object} config - 请求配置项
* @prop {String} options.url - 请求路径
* @prop {Object} options.data - 请求参数
* @prop {Object} [options.responseType = config.responseType] [text|arraybuffer] - 响应的数据类型
* @prop {Object} [options.dataType = config.dataType] - 如果设为 json会尝试对返回的数据做一次 JSON.parse
* @prop {Object} [options.header = config.header] - 请求header
* @prop {Object} [options.method = config.method] - 请求方法
* @returns {Promise<unknown>}
*/
request(config = {}) {
return this.middleware(config)
}
get(url, options = {}) {
return this.middleware({
url,
method: 'GET',
...options
})
}
post(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'POST',
...options
})
}
// #ifndef MP-ALIPAY
put(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'PUT',
...options
})
}
// #endif
// #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU
delete(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'DELETE',
...options
})
}
// #endif
// #ifdef H5 || MP-WEIXIN
connect(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'CONNECT',
...options
})
}
// #endif
// #ifdef H5 || MP-WEIXIN || MP-BAIDU
head(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'HEAD',
...options
})
}
// #endif
// #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU
options(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'OPTIONS',
...options
})
}
// #endif
// #ifdef H5 || MP-WEIXIN
trace(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'TRACE',
...options
})
}
// #endif
upload(url, config = {}) {
config.url = url
config.method = 'UPLOAD'
return this.middleware(config)
}
download(url, config = {}) {
config.url = url
config.method = 'DOWNLOAD'
return this.middleware(config)
}
}
/**
* setConfig回调
* @return {Object} - 返回操作后的config
* @callback Request~setConfigCallback
* @param {Object} config - 全局默认config
*/

View File

@ -0,0 +1,20 @@
'use strict'
import isAbsoluteURL from '../helpers/isAbsoluteURL'
import combineURLs from '../helpers/combineURLs'
/**
* Creates a new URL by combining the baseURL with the requestedURL,
* only when the requestedURL is not already an absolute URL.
* If the requestURL is absolute, this function returns the requestedURL untouched.
*
* @param {string} baseURL The base URL
* @param {string} requestedURL Absolute or relative URL to combine
* @returns {string} The combined full path
*/
export default function buildFullPath(baseURL, requestedURL) {
if (baseURL && !isAbsoluteURL(requestedURL)) {
return combineURLs(baseURL, requestedURL)
}
return requestedURL
}

View File

@ -0,0 +1,30 @@
/**
* 默认的全局配置
*/
export default {
baseURL: '',
header: {},
method: 'GET',
dataType: 'json',
// #ifndef MP-ALIPAY
responseType: 'text',
// #endif
custom: {},
// #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN
timeout: 60000,
// #endif
// #ifdef APP-PLUS
sslVerify: true,
// #endif
// #ifdef H5
withCredentials: false,
// #endif
// #ifdef APP-PLUS
firstIpv4: false,
// #endif
validateStatus: function validateStatus(status) {
return status >= 200 && status < 300
}
}

View File

@ -0,0 +1,6 @@
import adapter from '../adapters/index'
export default (config) => {
return adapter(config)
}

View File

@ -0,0 +1,103 @@
import {deepMerge, isUndefined} from '../utils'
/**
* 合并局部配置优先的配置如果局部有该配置项则用局部如果全局有该配置项则用全局
* @param {Array} keys - 配置项
* @param {Object} globalsConfig - 当前的全局配置
* @param {Object} config2 - 局部配置
* @return {{}}
*/
const mergeKeys = (keys, globalsConfig, config2) => {
let config = {}
keys.forEach(prop => {
if (!isUndefined(config2[prop])) {
config[prop] = config2[prop]
} else if (!isUndefined(globalsConfig[prop])) {
config[prop] = globalsConfig[prop]
}
})
return config
}
/**
*
* @param globalsConfig - 当前实例的全局配置
* @param config2 - 当前的局部配置
* @return - 合并后的配置
*/
export default (globalsConfig, config2 = {}) => {
const method = config2.method || globalsConfig.method || 'GET'
let config = {
baseURL: globalsConfig.baseURL || '',
method: method,
url: config2.url || '',
params: config2.params || {},
custom: {...(globalsConfig.custom || {}), ...(config2.custom || {})},
header: deepMerge(globalsConfig.header || {}, config2.header || {})
}
const defaultToConfig2Keys = ['getTask', 'validateStatus']
config = {...config, ...mergeKeys(defaultToConfig2Keys, globalsConfig, config2)}
// eslint-disable-next-line no-empty
if (method === 'DOWNLOAD') {
// #ifdef H5 || APP-PLUS
if (!isUndefined(config2.timeout)) {
config['timeout'] = config2['timeout']
} else if (!isUndefined(globalsConfig.timeout)) {
config['timeout'] = globalsConfig['timeout']
}
// #endif
} else if (method === 'UPLOAD') {
delete config.header['content-type']
delete config.header['Content-Type']
const uploadKeys = [
// #ifdef APP-PLUS || H5
'files',
// #endif
// #ifdef MP-ALIPAY
'fileType',
// #endif
// #ifdef H5
'file',
// #endif
'filePath',
'name',
// #ifdef H5 || APP-PLUS
'timeout',
// #endif
'formData',
]
uploadKeys.forEach(prop => {
if (!isUndefined(config2[prop])) {
config[prop] = config2[prop]
}
})
// #ifdef H5 || APP-PLUS
if (isUndefined(config.timeout) && !isUndefined(globalsConfig.timeout)) {
config['timeout'] = globalsConfig['timeout']
}
// #endif
} else {
const defaultsKeys = [
'data',
// #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN
'timeout',
// #endif
'dataType',
// #ifndef MP-ALIPAY
'responseType',
// #endif
// #ifdef APP-PLUS
'sslVerify',
// #endif
// #ifdef H5
'withCredentials',
// #endif
// #ifdef APP-PLUS
'firstIpv4',
// #endif
]
config = {...config, ...mergeKeys(defaultsKeys, globalsConfig, config2)}
}
return config
}

View File

@ -0,0 +1,16 @@
/**
* Resolve or reject a Promise based on response status.
*
* @param {Function} resolve A function that resolves the promise.
* @param {Function} reject A function that rejects the promise.
* @param {object} response The response.
*/
export default function settle(resolve, reject, response) {
const validateStatus = response.config.validateStatus
const status = response.statusCode
if (status && (!validateStatus || validateStatus(status))) {
resolve(response)
} else {
reject(response)
}
}

View File

@ -0,0 +1,69 @@
'use strict'
import * as utils from './../utils'
function encode(val) {
return encodeURIComponent(val).
replace(/%40/gi, '@').
replace(/%3A/gi, ':').
replace(/%24/g, '$').
replace(/%2C/gi, ',').
replace(/%20/g, '+').
replace(/%5B/gi, '[').
replace(/%5D/gi, ']')
}
/**
* Build a URL by appending params to the end
*
* @param {string} url The base of the url (e.g., http://www.google.com)
* @param {object} [params] The params to be appended
* @returns {string} The formatted url
*/
export default function buildURL(url, params) {
/*eslint no-param-reassign:0*/
if (!params) {
return url
}
var serializedParams
if (utils.isURLSearchParams(params)) {
serializedParams = params.toString()
} else {
var parts = []
utils.forEach(params, function serialize(val, key) {
if (val === null || typeof val === 'undefined') {
return
}
if (utils.isArray(val)) {
key = key + '[]'
} else {
val = [val]
}
utils.forEach(val, function parseValue(v) {
if (utils.isDate(v)) {
v = v.toISOString()
} else if (utils.isObject(v)) {
v = JSON.stringify(v)
}
parts.push(encode(key) + '=' + encode(v))
})
})
serializedParams = parts.join('&')
}
if (serializedParams) {
var hashmarkIndex = url.indexOf('#')
if (hashmarkIndex !== -1) {
url = url.slice(0, hashmarkIndex)
}
url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams
}
return url
}

View File

@ -0,0 +1,14 @@
'use strict'
/**
* Creates a new URL by combining the specified URLs
*
* @param {string} baseURL The base URL
* @param {string} relativeURL The relative URL
* @returns {string} The combined URL
*/
export default function combineURLs(baseURL, relativeURL) {
return relativeURL
? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '')
: baseURL
}

View File

@ -0,0 +1,14 @@
'use strict'
/**
* Determines whether the specified URL is absolute
*
* @param {string} url The URL to test
* @returns {boolean} True if the specified URL is absolute, otherwise false
*/
export default function isAbsoluteURL(url) {
// A URL is considered absolute if it begins with "<scheme>://" or "//" (protocol-relative URL).
// RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed
// by any combination of letters, digits, plus, period, or hyphen.
return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url)
}

View File

@ -0,0 +1,116 @@
type AnyObject = Record<string | number | symbol, any>
type HttpPromise<T> = Promise<HttpResponse<T>>;
type Tasks = UniApp.RequestTask | UniApp.UploadTask | UniApp.DownloadTask
export interface RequestTask {
abort: () => void;
offHeadersReceived: () => void;
onHeadersReceived: () => void;
}
export interface HttpRequestConfig<T = Tasks> {
/** 请求基地址 */
baseURL?: string;
/** 请求服务器接口地址 */
url?: string;
/** 请求查询参数,自动拼接为查询字符串 */
params?: AnyObject;
/** 请求体参数 */
data?: AnyObject;
/** 文件对应的 key */
name?: string;
/** HTTP 请求中其他额外的 form data */
formData?: AnyObject;
/** 要上传文件资源的路径。 */
filePath?: string;
/** 需要上传的文件列表。使用 files 时filePath 和 name 不生效App、H5 2.6.15+ */
files?: Array<{
name?: string;
file?: File;
uri: string;
}>;
/** 要上传的文件对象仅H52.6.15+)支持 */
file?: File;
/** 请求头信息 */
header?: AnyObject;
/** 请求方式 */
method?: "GET" | "POST" | "PUT" | "DELETE" | "CONNECT" | "HEAD" | "OPTIONS" | "TRACE" | "UPLOAD" | "DOWNLOAD";
/** 如果设为 json会尝试对返回的数据做一次 JSON.parse */
dataType?: string;
/** 设置响应的数据类型,支付宝小程序不支持 */
responseType?: "text" | "arraybuffer";
/** 自定义参数 */
custom?: AnyObject;
/** 超时时间仅微信小程序2.10.0)、支付宝小程序支持 */
timeout?: number;
/** DNS解析时优先使用ipv4仅 App-Android 支持 (HBuilderX 2.8.0+) */
firstIpv4?: boolean;
/** 验证 ssl 证书 仅5+App安卓端支持HBuilderX 2.3.3+ */
sslVerify?: boolean;
/** 跨域请求时是否携带凭证cookies仅H5支持HBuilderX 2.6.15+ */
withCredentials?: boolean;
/** 返回当前请求的task, options。请勿在此处修改options。 */
getTask?: (task: T, options: HttpRequestConfig<T>) => void;
/** 全局自定义验证器 */
validateStatus?: (statusCode: number) => boolean | void;
}
export interface HttpResponse<T = any> {
config: HttpRequestConfig;
statusCode: number;
cookies: Array<string>;
data: T;
errMsg: string;
header: AnyObject;
}
export interface HttpUploadResponse<T = any> {
config: HttpRequestConfig;
statusCode: number;
data: T;
errMsg: string;
}
export interface HttpDownloadResponse extends HttpResponse {
tempFilePath: string;
}
export interface HttpError {
config: HttpRequestConfig;
statusCode?: number;
cookies?: Array<string>;
data?: any;
errMsg: string;
header?: AnyObject;
}
export interface HttpInterceptorManager<V, E = V> {
use(
onFulfilled?: (config: V) => Promise<V> | V,
onRejected?: (config: E) => Promise<E> | E
): void;
eject(id: number): void;
}
export abstract class HttpRequestAbstract {
constructor(config?: HttpRequestConfig);
config: HttpRequestConfig;
interceptors: {
request: HttpInterceptorManager<HttpRequestConfig, HttpRequestConfig>;
response: HttpInterceptorManager<HttpResponse, HttpError>;
}
middleware<T = any>(config: HttpRequestConfig): HttpPromise<T>;
request<T = any>(config: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
get<T = any>(url: string, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
upload<T = any>(url: string, config?: HttpRequestConfig<UniApp.UploadTask>): HttpPromise<T>;
delete<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
head<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
post<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
put<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
connect<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
options<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
trace<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
download(url: string, config?: HttpRequestConfig<UniApp.DownloadTask>): Promise<HttpDownloadResponse>;
setConfig(onSend: (config: HttpRequestConfig) => HttpRequestConfig): void;
}
declare class HttpRequest extends HttpRequestAbstract { }
export default HttpRequest;

View File

@ -0,0 +1,2 @@
import Request from './core/Request'
export default Request

View File

@ -0,0 +1,135 @@
'use strict'
// utils is a library of generic helper functions non-specific to axios
var toString = Object.prototype.toString
/**
* Determine if a value is an Array
*
* @param {Object} val The value to test
* @returns {boolean} True if value is an Array, otherwise false
*/
export function isArray (val) {
return toString.call(val) === '[object Array]'
}
/**
* Determine if a value is an Object
*
* @param {Object} val The value to test
* @returns {boolean} True if value is an Object, otherwise false
*/
export function isObject (val) {
return val !== null && typeof val === 'object'
}
/**
* Determine if a value is a Date
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a Date, otherwise false
*/
export function isDate (val) {
return toString.call(val) === '[object Date]'
}
/**
* Determine if a value is a URLSearchParams object
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a URLSearchParams object, otherwise false
*/
export function isURLSearchParams (val) {
return typeof URLSearchParams !== 'undefined' && val instanceof URLSearchParams
}
/**
* Iterate over an Array or an Object invoking a function for each item.
*
* If `obj` is an Array callback will be called passing
* the value, index, and complete array for each item.
*
* If 'obj' is an Object callback will be called passing
* the value, key, and complete object for each property.
*
* @param {Object|Array} obj The object to iterate
* @param {Function} fn The callback to invoke for each item
*/
export function forEach (obj, fn) {
// Don't bother if no value provided
if (obj === null || typeof obj === 'undefined') {
return
}
// Force an array if not already something iterable
if (typeof obj !== 'object') {
/*eslint no-param-reassign:0*/
obj = [obj]
}
if (isArray(obj)) {
// Iterate over array values
for (var i = 0, l = obj.length; i < l; i++) {
fn.call(null, obj[i], i, obj)
}
} else {
// Iterate over object keys
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
fn.call(null, obj[key], key, obj)
}
}
}
}
/**
* 是否为boolean
* @param val
* @returns {boolean}
*/
export function isBoolean(val) {
return typeof val === 'boolean'
}
/**
* 是否为真正的对象{} new Object
* @param {any} obj - 检测的对象
* @returns {boolean}
*/
export function isPlainObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]'
}
/**
* Function equal to merge with the difference being that no reference
* to original objects is kept.
*
* @see merge
* @param {Object} obj1 Object to merge
* @returns {Object} Result of all merge properties
*/
export function deepMerge(/* obj1, obj2, obj3, ... */) {
let result = {}
function assignValue(val, key) {
if (typeof result[key] === 'object' && typeof val === 'object') {
result[key] = deepMerge(result[key], val)
} else if (typeof val === 'object') {
result[key] = deepMerge({}, val)
} else {
result[key] = val
}
}
for (let i = 0, l = arguments.length; i < l; i++) {
forEach(arguments[i], assignValue)
}
return result
}
export function isUndefined (val) {
return typeof val === 'undefined'
}

View File

@ -0,0 +1,264 @@
/* eslint-disable */
var clone = (function() {
'use strict';
function _instanceof(obj, type) {
return type != null && obj instanceof type;
}
var nativeMap;
try {
nativeMap = Map;
} catch(_) {
// maybe a reference error because no `Map`. Give it a dummy value that no
// value will ever be an instanceof.
nativeMap = function() {};
}
var nativeSet;
try {
nativeSet = Set;
} catch(_) {
nativeSet = function() {};
}
var nativePromise;
try {
nativePromise = Promise;
} catch(_) {
nativePromise = function() {};
}
/**
* Clones (copies) an Object using deep copying.
*
* This function supports circular references by default, but if you are certain
* there are no circular references in your object, you can save some CPU time
* by calling clone(obj, false).
*
* Caution: if `circular` is false and `parent` contains circular references,
* your program may enter an infinite loop and crash.
*
* @param `parent` - the object to be cloned
* @param `circular` - set to true if the object to be cloned may contain
* circular references. (optional - true by default)
* @param `depth` - set to a number if the object is only to be cloned to
* a particular depth. (optional - defaults to Infinity)
* @param `prototype` - sets the prototype to be used when cloning an object.
* (optional - defaults to parent prototype).
* @param `includeNonEnumerable` - set to true if the non-enumerable properties
* should be cloned as well. Non-enumerable properties on the prototype
* chain will be ignored. (optional - false by default)
*/
function clone(parent, circular, depth, prototype, includeNonEnumerable) {
if (typeof circular === 'object') {
depth = circular.depth;
prototype = circular.prototype;
includeNonEnumerable = circular.includeNonEnumerable;
circular = circular.circular;
}
// maintain two arrays for circular references, where corresponding parents
// and children have the same index
var allParents = [];
var allChildren = [];
var useBuffer = typeof Buffer != 'undefined';
if (typeof circular == 'undefined')
circular = true;
if (typeof depth == 'undefined')
depth = Infinity;
// recurse this function so we don't reset allParents and allChildren
function _clone(parent, depth) {
// cloning null always returns null
if (parent === null)
return null;
if (depth === 0)
return parent;
var child;
var proto;
if (typeof parent != 'object') {
return parent;
}
if (_instanceof(parent, nativeMap)) {
child = new nativeMap();
} else if (_instanceof(parent, nativeSet)) {
child = new nativeSet();
} else if (_instanceof(parent, nativePromise)) {
child = new nativePromise(function (resolve, reject) {
parent.then(function(value) {
resolve(_clone(value, depth - 1));
}, function(err) {
reject(_clone(err, depth - 1));
});
});
} else if (clone.__isArray(parent)) {
child = [];
} else if (clone.__isRegExp(parent)) {
child = new RegExp(parent.source, __getRegExpFlags(parent));
if (parent.lastIndex) child.lastIndex = parent.lastIndex;
} else if (clone.__isDate(parent)) {
child = new Date(parent.getTime());
} else if (useBuffer && Buffer.isBuffer(parent)) {
if (Buffer.from) {
// Node.js >= 5.10.0
child = Buffer.from(parent);
} else {
// Older Node.js versions
child = new Buffer(parent.length);
parent.copy(child);
}
return child;
} else if (_instanceof(parent, Error)) {
child = Object.create(parent);
} else {
if (typeof prototype == 'undefined') {
proto = Object.getPrototypeOf(parent);
child = Object.create(proto);
}
else {
child = Object.create(prototype);
proto = prototype;
}
}
if (circular) {
var index = allParents.indexOf(parent);
if (index != -1) {
return allChildren[index];
}
allParents.push(parent);
allChildren.push(child);
}
if (_instanceof(parent, nativeMap)) {
parent.forEach(function(value, key) {
var keyChild = _clone(key, depth - 1);
var valueChild = _clone(value, depth - 1);
child.set(keyChild, valueChild);
});
}
if (_instanceof(parent, nativeSet)) {
parent.forEach(function(value) {
var entryChild = _clone(value, depth - 1);
child.add(entryChild);
});
}
for (var i in parent) {
var attrs = Object.getOwnPropertyDescriptor(parent, i);
if (attrs) {
child[i] = _clone(parent[i], depth - 1);
}
try {
var objProperty = Object.getOwnPropertyDescriptor(parent, i);
if (objProperty.set === 'undefined') {
// no setter defined. Skip cloning this property
continue;
}
child[i] = _clone(parent[i], depth - 1);
} catch(e){
if (e instanceof TypeError) {
// when in strict mode, TypeError will be thrown if child[i] property only has a getter
// we can't do anything about this, other than inform the user that this property cannot be set.
continue
} else if (e instanceof ReferenceError) {
//this may happen in non strict mode
continue
}
}
}
if (Object.getOwnPropertySymbols) {
var symbols = Object.getOwnPropertySymbols(parent);
for (var i = 0; i < symbols.length; i++) {
// Don't need to worry about cloning a symbol because it is a primitive,
// like a number or string.
var symbol = symbols[i];
var descriptor = Object.getOwnPropertyDescriptor(parent, symbol);
if (descriptor && !descriptor.enumerable && !includeNonEnumerable) {
continue;
}
child[symbol] = _clone(parent[symbol], depth - 1);
Object.defineProperty(child, symbol, descriptor);
}
}
if (includeNonEnumerable) {
var allPropertyNames = Object.getOwnPropertyNames(parent);
for (var i = 0; i < allPropertyNames.length; i++) {
var propertyName = allPropertyNames[i];
var descriptor = Object.getOwnPropertyDescriptor(parent, propertyName);
if (descriptor && descriptor.enumerable) {
continue;
}
child[propertyName] = _clone(parent[propertyName], depth - 1);
Object.defineProperty(child, propertyName, descriptor);
}
}
return child;
}
return _clone(parent, depth);
}
/**
* Simple flat clone using prototype, accepts only objects, usefull for property
* override on FLAT configuration object (no nested props).
*
* USE WITH CAUTION! This may not behave as you wish if you do not know how this
* works.
*/
clone.clonePrototype = function clonePrototype(parent) {
if (parent === null)
return null;
var c = function () {};
c.prototype = parent;
return new c();
};
// private utility functions
function __objToStr(o) {
return Object.prototype.toString.call(o);
}
clone.__objToStr = __objToStr;
function __isDate(o) {
return typeof o === 'object' && __objToStr(o) === '[object Date]';
}
clone.__isDate = __isDate;
function __isArray(o) {
return typeof o === 'object' && __objToStr(o) === '[object Array]';
}
clone.__isArray = __isArray;
function __isRegExp(o) {
return typeof o === 'object' && __objToStr(o) === '[object RegExp]';
}
clone.__isRegExp = __isRegExp;
function __getRegExpFlags(re) {
var flags = '';
if (re.global) flags += 'g';
if (re.ignoreCase) flags += 'i';
if (re.multiline) flags += 'm';
return flags;
}
clone.__getRegExpFlags = __getRegExpFlags;
return clone;
})();
export default clone

View File

@ -0,0 +1,167 @@
<template>
<view class="uni-section">
<view class="uni-section-header" @click="onClick">
<view class="uni-section-header__decoration" v-if="type" :class="type" />
<slot v-else name="decoration"></slot>
<view class="uni-section-header__content">
<text :style="{'font-size':titleFontSize,'color':titleColor}" class="uni-section__content-title" :class="{'distraction':!subTitle}">{{ title }}</text>
<text v-if="subTitle" :style="{'font-size':subTitleFontSize,'color':subTitleColor}" class="uni-section-header__content-sub">{{ subTitle }}</text>
</view>
<view class="uni-section-header__slot-right">
<slot name="right"></slot>
</view>
</view>
<view class="uni-section-content" :style="{padding: _padding}">
<slot />
</view>
</view>
</template>
<script>
/**
* Section 标题栏
* @description 标题栏
* @property {String} type = [line|circle|square] 标题装饰类型
* @value line 竖线
* @value circle 圆形
* @value square 正方形
* @property {String} title 主标题
* @property {String} titleFontSize 主标题字体大小
* @property {String} titleColor 主标题字体颜色
* @property {String} subTitle 副标题
* @property {String} subTitleFontSize 副标题字体大小
* @property {String} subTitleColor 副标题字体颜色
* @property {String} padding 默认插槽 padding
*/
export default {
name: 'UniSection',
emits:['click'],
props: {
type: {
type: String,
default: ''
},
title: {
type: String,
required: true,
default: ''
},
titleFontSize: {
type: String,
default: '14px'
},
titleColor:{
type: String,
default: '#333'
},
subTitle: {
type: String,
default: ''
},
subTitleFontSize: {
type: String,
default: '12px'
},
subTitleColor: {
type: String,
default: '#999'
},
padding: {
type: [Boolean, String],
default: false
}
},
computed:{
_padding(){
if(typeof this.padding === 'string'){
return this.padding
}
return this.padding?'10px':''
}
},
watch: {
title(newVal) {
if (uni.report && newVal !== '') {
uni.report('title', newVal)
}
}
},
methods: {
onClick() {
this.$emit('click')
}
}
}
</script>
<style lang="scss" >
$uni-primary: #2979ff !default;
.uni-section {
background-color: #fff;
.uni-section-header {
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
padding: 12px 10px;
font-weight: normal;
&__decoration{
margin-right: 6px;
background-color: $uni-primary;
&.line {
width: 4px;
height: 12px;
border-radius: 10px;
}
&.circle {
width: 8px;
height: 8px;
border-top-right-radius: 50px;
border-top-left-radius: 50px;
border-bottom-left-radius: 50px;
border-bottom-right-radius: 50px;
}
&.square {
width: 8px;
height: 8px;
}
}
&__content {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
flex: 1;
color: #333;
.distraction {
flex-direction: row;
align-items: center;
}
&-sub {
margin-top: 2px;
}
}
&__slot-right{
font-size: 14px;
}
}
.uni-section-content{
font-size: 14px;
}
}
</style>

18
kinit-uni/config.js Normal file
View File

@ -0,0 +1,18 @@
// 应用全局配置
module.exports = {
// 测试环境
// baseUrl: 'http://127.0.0.1:9000',
// 生产环境
baseUrl: 'https://api.kinit.ktianc.top',
// 应用信息
appInfo: {
// 应用版本
version: "1.0.0",
// 官方网站
siteUrl: "https://gitee.com/ktianc/kinit",
// 隐私政策不支持本地路径
privacy: "http://kinit.ktianc.top/docs/privacy",
// 用户协议不支持本地路径
agreement: "http://kinit.ktianc.top/docs/agreement"
}
}

57
kinit-uni/main.js Normal file
View File

@ -0,0 +1,57 @@
import Vue from 'vue'
import App from './App'
import store from './store' // store
import plugins from './plugins' // plugins
import {router,RouterMount} from './permission.js' // 路由拦截
import uView from "uview-ui"
Vue.use(uView)
Vue.use(router)
Vue.use(plugins)
// 调用setConfig方法方法内部会进行对象属性深度合并可以放心嵌套配置
// 文档https://www.uviewui.com/components/setting.html
// 配置后很多组件的默认尺寸就变了需要手动调整不熟悉不建议开启
// 需要在Vue.use(uView)之后执行
uni.$u.setConfig({
// 修改$u.config对象的属性
config: {
// 修改默认单位为rpx相当于执行 uni.$u.config.unit = 'rpx'
unit: 'rpx'
},
// 修改$u.props对象的属性
props: {
// 修改radio组件的size参数的默认值相当于执行 uni.$u.props.radio.size = 30
radio: {
size: 33,
labelSize: 30
},
button: {
loadingSize: 28
},
text: {
size: 30,
color: '#000'
}
// 其他组件属性配置
// ......
}
})
Vue.config.productionTip = false
Vue.prototype.$store = store
App.mpType = 'app'
const app = new Vue({
...App
})
//v1.3.5 H5端 你应该去除原有的app.$mount();使用路由自带的渲染方式
// #ifdef H5
RouterMount(app, router, '#app')
// #endif
// #ifndef H5
app.$mount(); //为了兼容小程序及app端必须这样写才有效果
// #endif

65
kinit-uni/manifest.json Normal file
View File

@ -0,0 +1,65 @@
{
"name" : "kinit-uni",
"appid" : "__UNI__9D006F9",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
"app-plus" : {
"usingComponents" : true,
"nvueCompiler" : "uni-app",
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
"modules" : {},
"distribute" : {
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"ios" : {},
"sdkConfigs" : {}
}
},
"quickapp" : {},
"mp-weixin" : {
"appid" : "wxa510d7a34a124349",
"setting" : {
"urlCheck" : false,
"es6" : false,
"minified" : true,
"postcss" : true
},
"optimization" : {
"subPackages" : true
},
"usingComponents" : true
},
"vueVersion" : "2",
"h5" : {
"template" : "static/index.html",
"title" : "Kinit",
"router" : {
"mode" : "hash",
"base" : "./"
}
}
}

BIN
kinit-uni/node_modules.zip Normal file

Binary file not shown.

7
kinit-uni/package.json Normal file
View File

@ -0,0 +1,7 @@
{
"dependencies": {
"uni-read-pages": "^1.0.5",
"uni-simple-router": "^2.0.8-beta.3",
"uview-ui": "^2.0.34"
}
}

144
kinit-uni/pages.json Normal file
View File

@ -0,0 +1,144 @@
{
"easycom": {
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"
},
"pages": [
{
"path": "pages/login",
"meta":{
"loginAuth": false
},
"style": {
"navigationBarTitleText": "登录"
}
}, {
"path": "pages/index",
"meta":{
"loginAuth": true
},
"style": {
"navigationBarTitleText": "Kinit 移动端框架"
}
}, {
"path": "pages/work/index",
"meta":{
"loginAuth": true
},
"style": {
"navigationBarTitleText": "工作台"
}
}, {
"path": "pages/mine/index",
"meta":{
"loginAuth": true
},
"style": {
"navigationBarTitleText": "我的"
}
}, {
"path": "pages/mine/avatar/index",
"meta":{
"loginAuth": true
},
"style": {
"navigationBarTitleText": "修改头像"
}
}, {
"path": "pages/mine/info/index",
"meta":{
"loginAuth": true
},
"style": {
"navigationBarTitleText": "个人信息"
}
}, {
"path": "pages/mine/info/edit",
"meta":{
"loginAuth": true
},
"style": {
"navigationBarTitleText": "编辑资料"
}
}, {
"path": "pages/mine/pwd/index",
"meta":{
"loginAuth": true
},
"style": {
"navigationBarTitleText": "修改密码"
}
}, {
"path": "pages/mine/setting/index",
"meta":{
"loginAuth": true
},
"style": {
"navigationBarTitleText": "应用设置"
}
}, {
"path": "pages/mine/help/index",
"meta":{
"loginAuth": true
},
"style": {
"navigationBarTitleText": "常见问题"
}
}, {
"path": "pages/mine/about/index",
"meta":{
"loginAuth": true
},
"style": {
"navigationBarTitleText": "关于我们"
}
}, {
"path": "pages/common/webview/index",
"meta":{
"loginAuth": false
},
"style": {
"navigationBarTitleText": "浏览网页"
}
}, {
"path": "pages/common/textview/index",
"meta":{
"loginAuth": false
},
"style": {
"navigationBarTitleText": "浏览文本"
}
}
],
"tabBar": {
"color": "#000000",
"selectedColor": "#000000",
"borderStyle": "white",
"backgroundColor": "#ffffff",
"list": [{
"pagePath": "pages/index",
"iconPath": "static/images/tabbar/home.png",
"selectedIconPath": "static/images/tabbar/home_.png",
"text": "首页"
}, {
"pagePath": "pages/work/index",
"iconPath": "static/images/tabbar/work.png",
"selectedIconPath": "static/images/tabbar/work_.png",
"text": "工作台"
}, {
"pagePath": "pages/mine/index",
"iconPath": "static/images/tabbar/mine.png",
"selectedIconPath": "static/images/tabbar/mine_.png",
"text": "我的"
}
]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "Kinit",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8",
"app-plus":{
"titleNView": false
}
}
}

View File

@ -0,0 +1,48 @@
<template>
<view>
<uni-card class="view-title" :title="title">
<text class="uni-body view-content">{{ content }}</text>
</uni-card>
</view>
</template>
<script>
export default {
data() {
return {
title: '',
content: ''
}
},
onLoad(options) {
if (options.query) {
this.title = options.query.title
this.content = options.query.content
} else {
this.title = options.title
this.content = options.content
}
uni.setNavigationBarTitle({
title: options.title
})
}
}
</script>
<style scoped>
page {
background-color: #ffffff;
}
.view-title {
font-weight: bold;
}
.view-content {
font-size: 26rpx;
padding: 12px 5px 0;
color: #333;
line-height: 24px;
font-weight: normal;
}
</style>

View File

@ -0,0 +1,35 @@
<template>
<view v-if="params.url">
<!-- 不支持本地路径 -->
<web-view :webview-styles="webviewStyles" :src="`${params.url}`"></web-view>
</view>
</template>
<script>
export default {
data() {
return {
params: {},
webviewStyles: {
progress: {
color: "#FF3333"
}
}
}
},
props: {
src: {
type: [String],
default: null
}
},
onLoad(event) {
this.params = event
if (event.title) {
uni.setNavigationBarTitle({
title: event.title
})
}
}
}
</script>

52
kinit-uni/pages/index.vue Normal file
View File

@ -0,0 +1,52 @@
<template>
<view class="content">
<image v-if="logo" class="logo" :src="logoImage"></image>
<view class="text-area">
<text class="title">Hello {{ name }}</text>
</view>
</view>
</template>
<script>
export default {
computed: {
name() {
return this.$store.state.auth.name
},
logo() {
return this.$store.state.app.logo
},
logoImage() {
return this.$store.state.app.logoImage
}
},
}
</script>
<style lang="scss">
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.logo {
height: 200rpx;
width: 200rpx;
margin-top: 200rpx;
margin-left: auto;
margin-right: auto;
margin-bottom: 50rpx;
}
.text-area {
display: flex;
justify-content: center;
.title {
font-size: 36rpx;
color: #8f8f94;
}
}
}
</style>

168
kinit-uni/pages/login.vue Normal file
View File

@ -0,0 +1,168 @@
<template>
<view class="normal-login-container">
<view class="logo-content align-center justify-center flex">
<image v-if="logo" style="width: 100rpx;height: 100rpx;" :src="logoImage" mode="widthFix">
</image>
<text class="title">{{ title }}</text>
</view>
<view class="login-form-content">
<view class="input-item flex align-center">
<view class="iconfont icon-user icon"></view>
<input v-model="loginForm.telephone" class="input" type="text" placeholder="请输入手机号" maxlength="30" />
</view>
<view class="input-item flex align-center">
<view class="iconfont icon-password icon"></view>
<input v-model="loginForm.password" type="password" class="input" placeholder="请输入密码" maxlength="20" />
</view>
<view class="action-btn">
<button @click="handleLogin" class="login-btn cu-btn block bg-blue lg round">登录</button>
</view>
</view>
<view class="xieyi text-center">
<text class="text-grey1">登录即代表同意</text>
<text @click="handleUserAgrement" class="text-blue">用户协议</text>
<text @click="handlePrivacy" class="text-blue">隐私协议</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
loginForm: {
telephone: "15020221010",
password: "kinit2022",
method: '0'
}
}
},
computed: {
title() {
return this.$store.state.app.title
},
logo() {
return this.$store.state.app.logo
},
logoImage() {
return this.$store.state.app.logoImage
},
privacy() {
return this.$store.state.app.privacy
},
agreement() {
return this.$store.state.app.agreement
}
},
methods: {
//
handlePrivacy() {
const title = '隐私政策'
this.$tab.navigateTo(`/pages/common/webview/index?title=${title}&url=${this.privacy}`)
},
//
handleUserAgrement() {
const title = '用户协议'
this.$tab.navigateTo(`/pages/common/webview/index?title=${title}&url=${this.agreement}`)
},
//
async handleLogin() {
if (this.loginForm.telephone === "") {
this.$modal.msgError("请输入您的手机号")
} else if (this.loginForm.password === "") {
this.$modal.msgError("请输入您的密码")
}else {
this.$modal.loading("登录中,请耐心等待...")
this.pwdLogin()
}
},
//
async pwdLogin() {
this.$store.dispatch('Login', this.loginForm).then(() => {
this.$modal.closeLoading()
this.loginSuccess()
})
},
//
loginSuccess(result) {
//
this.$store.dispatch('GetInfo').then(res => {
this.$tab.reLaunch('/pages/index')
})
}
}
}
</script>
<style lang="scss">
page {
background-color: #ffffff;
}
.normal-login-container {
width: 100%;
.logo-content {
width: 100%;
font-size: 21px;
text-align: center;
padding-top: 15%;
image {
border-radius: 4px;
}
.title {
margin-left: 10px;
}
}
.login-form-content {
text-align: center;
margin: 20px auto;
margin-top: 15%;
width: 80%;
.input-item {
margin: 20px auto;
background-color: #f5f6f7;
height: 45px;
border-radius: 20px;
.icon {
font-size: 38rpx;
margin-left: 10px;
color: #999;
}
.input {
width: 100%;
font-size: 14px;
line-height: 20px;
text-align: left;
padding-left: 15px;
}
}
.login-btn {
margin-top: 40px;
height: 45px;
}
.xieyi {
color: #333;
margin-top: 20px;
}
}
.easyinput {
width: 100%;
}
}
.login-code-img {
height: 45px;
}
</style>

View File

@ -0,0 +1,89 @@
<template>
<view class="about-container">
<view class="header-section text-center">
<image style="width: 150rpx;height: 150rpx;" :src="logoImage" mode="widthFix">
</image>
<uni-title type="h2" :title="title"></uni-title>
</view>
<view class="content-section">
<view class="menu-list">
<view class="list-cell list-cell-arrow">
<view class="menu-item-box">
<view>版本信息</view>
<view class="text-right">v{{version}}</view>
</view>
</view>
<view class="list-cell list-cell-arrow">
<view class="menu-item-box">
<view>官方邮箱</view>
<view class="text-right">kinit@xx.com</view>
</view>
</view>
<view class="list-cell list-cell-arrow">
<view class="menu-item-box">
<view>服务热线</view>
<view class="text-right">400-999-9999</view>
</view>
</view>
<view class="list-cell list-cell-arrow">
<view class="menu-item-box">
<view>公司网站</view>
<view class="text-right">
<uni-link :href="siteUrl" :text="siteUrl" showUnderLine="false"></uni-link>
</view>
</view>
</view>
</view>
</view>
<view class="copyright">
<view>{{ footerContent }}</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {}
},
computed: {
version() {
return this.$store.state.app.version
},
title() {
return this.$store.state.app.title
},
logoImage() {
return this.$store.state.app.logoImage
},
siteUrl() {
return this.$store.state.app.siteUrl
},
footerContent() {
return this.$store.state.app.footerContent
}
},
}
</script>
<style lang="scss">
page {
background-color: #f8f8f8;
}
.copyright {
margin-top: 50rpx;
text-align: center;
line-height: 60rpx;
color: #999;
}
.header-section {
display: flex;
padding: 30rpx 0 0;
flex-direction: column;
align-items: center;
}
</style>

View File

@ -0,0 +1,630 @@
<template>
<view class="container">
<view class="page-body uni-content-info">
<view class='cropper-content'>
<view v-if="isShowImg" class="uni-corpper" :style="'width:'+cropperInitW+'px;height:'+cropperInitH+'px;background:#000'">
<view class="uni-corpper-content" :style="'width:'+cropperW+'px;height:'+cropperH+'px;left:'+cropperL+'px;top:'+cropperT+'px'">
<image :src="imageSrc" :style="'width:'+cropperW+'px;height:'+cropperH+'px'"></image>
<view class="uni-corpper-crop-box" @touchstart.stop="contentStartMove" @touchmove.stop="contentMoveing" @touchend.stop="contentTouchEnd"
:style="'left:'+cutL+'px;top:'+cutT+'px;right:'+cutR+'px;bottom:'+cutB+'px'">
<view class="uni-cropper-view-box">
<view class="uni-cropper-dashed-h"></view>
<view class="uni-cropper-dashed-v"></view>
<view class="uni-cropper-line-t" data-drag="top" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-line-r" data-drag="right" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-line-b" data-drag="bottom" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-line-l" data-drag="left" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-point point-t" data-drag="top" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-point point-tr" data-drag="topTight"></view>
<view class="uni-cropper-point point-r" data-drag="right" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-point point-rb" data-drag="rightBottom" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-point point-b" data-drag="bottom" @touchstart.stop="dragStart" @touchmove.stop="dragMove" @touchend.stop="dragEnd"></view>
<view class="uni-cropper-point point-bl" data-drag="bottomLeft"></view>
<view class="uni-cropper-point point-l" data-drag="left" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-point point-lt" data-drag="leftTop"></view>
</view>
</view>
</view>
</view>
</view>
<view class='cropper-config'>
<button type="primary reverse" @click="getImage" style='margin-top: 30rpx;'> 选择头像 </button>
<button type="warn" @click="getImageInfo" style='margin-top: 30rpx;'> 提交 </button>
</view>
<canvas canvas-id="myCanvas" :style="'position:absolute;border: 1px solid red; width:'+imageW+'px;height:'+imageH+'px;top:-9999px;left:-9999px;'"></canvas>
</view>
</view>
</template>
<script>
import config from '@/config'
import store from "@/store"
import { postCurrentUserUploadAvatar } from "@/common/request/api/vadmin/auth/user.js"
const baseUrl = config.baseUrl
let sysInfo = uni.getSystemInfoSync()
let SCREEN_WIDTH = sysInfo.screenWidth
let PAGE_X, // x
PAGE_Y, // y
PR = sysInfo.pixelRatio, // dpi
T_PAGE_X, // x
T_PAGE_Y, // Y
CUT_L, // left
CUT_T, // top
CUT_R, //
CUT_B, //
CUT_W, //
CUT_H, //
IMG_RATIO, //
IMG_REAL_W, //
IMG_REAL_H, //
DRAFG_MOVE_RATIO = 1, //,
INIT_DRAG_POSITION = 100, //
DRAW_IMAGE_W = sysInfo.screenWidth //
export default {
/**
* 页面的初始数据
*/
data() {
return {
imageSrc: store.getters.avatar,
isShowImg: false,
//
cropperInitW: SCREEN_WIDTH,
cropperInitH: SCREEN_WIDTH,
//
cropperW: SCREEN_WIDTH,
cropperH: SCREEN_WIDTH,
// left top
cropperL: 0,
cropperT: 0,
transL: 0,
transT: 0,
//
scaleP: 0,
imageW: 0,
imageH: 0,
//
cutL: 0,
cutT: 0,
cutB: SCREEN_WIDTH,
cutR: '100%',
qualityWidth: DRAW_IMAGE_W,
innerAspectRadio: DRAFG_MOVE_RATIO
}
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady: function () {
this.loadImage()
},
methods: {
setData: function (obj) {
let that = this
Object.keys(obj).forEach(function (key) {
that.$set(that.$data, key, obj[key])
})
},
getImage: function () {
var _this = this
uni.chooseImage({
success: function (res) {
_this.setData({
imageSrc: res.tempFilePaths[0],
})
_this.loadImage()
},
})
},
loadImage: function () {
var _this = this
uni.getImageInfo({
src: _this.imageSrc,
success: function success(res) {
IMG_RATIO = 1 / 1
if (IMG_RATIO >= 1) {
IMG_REAL_W = SCREEN_WIDTH
IMG_REAL_H = SCREEN_WIDTH / IMG_RATIO
} else {
IMG_REAL_W = SCREEN_WIDTH * IMG_RATIO
IMG_REAL_H = SCREEN_WIDTH
}
let minRange = IMG_REAL_W > IMG_REAL_H ? IMG_REAL_W : IMG_REAL_H
INIT_DRAG_POSITION = minRange > INIT_DRAG_POSITION ? INIT_DRAG_POSITION : minRange
//
if (IMG_RATIO >= 1) {
let cutT = Math.ceil((SCREEN_WIDTH / IMG_RATIO - (SCREEN_WIDTH / IMG_RATIO - INIT_DRAG_POSITION)) / 2)
let cutB = cutT
let cutL = Math.ceil((SCREEN_WIDTH - SCREEN_WIDTH + INIT_DRAG_POSITION) / 2)
let cutR = cutL
_this.setData({
cropperW: SCREEN_WIDTH,
cropperH: SCREEN_WIDTH / IMG_RATIO,
// left right
cropperL: Math.ceil((SCREEN_WIDTH - SCREEN_WIDTH) / 2),
cropperT: Math.ceil((SCREEN_WIDTH - SCREEN_WIDTH / IMG_RATIO) / 2),
cutL: cutL,
cutT: cutT,
cutR: cutR,
cutB: cutB,
//
imageW: IMG_REAL_W,
imageH: IMG_REAL_H,
scaleP: IMG_REAL_W / SCREEN_WIDTH,
qualityWidth: DRAW_IMAGE_W,
innerAspectRadio: IMG_RATIO
})
} else {
let cutL = Math.ceil((SCREEN_WIDTH * IMG_RATIO - (SCREEN_WIDTH * IMG_RATIO)) / 2)
let cutR = cutL
let cutT = Math.ceil((SCREEN_WIDTH - INIT_DRAG_POSITION) / 2)
let cutB = cutT
_this.setData({
cropperW: SCREEN_WIDTH * IMG_RATIO,
cropperH: SCREEN_WIDTH,
// left right
cropperL: Math.ceil((SCREEN_WIDTH - SCREEN_WIDTH * IMG_RATIO) / 2),
cropperT: Math.ceil((SCREEN_WIDTH - SCREEN_WIDTH) / 2),
cutL: cutL,
cutT: cutT,
cutR: cutR,
cutB: cutB,
//
imageW: IMG_REAL_W,
imageH: IMG_REAL_H,
scaleP: IMG_REAL_W / SCREEN_WIDTH,
qualityWidth: DRAW_IMAGE_W,
innerAspectRadio: IMG_RATIO
})
}
_this.setData({
isShowImg: true
})
uni.hideLoading()
}
})
},
// touchStart
contentStartMove(e) {
PAGE_X = e.touches[0].pageX
PAGE_Y = e.touches[0].pageY
},
// touchMove
contentMoveing(e) {
var _this = this
var dragLengthX = (PAGE_X - e.touches[0].pageX) * DRAFG_MOVE_RATIO
var dragLengthY = (PAGE_Y - e.touches[0].pageY) * DRAFG_MOVE_RATIO
//
if (dragLengthX > 0) {
if (this.cutL - dragLengthX < 0) dragLengthX = this.cutL
} else {
if (this.cutR + dragLengthX < 0) dragLengthX = -this.cutR
}
if (dragLengthY > 0) {
if (this.cutT - dragLengthY < 0) dragLengthY = this.cutT
} else {
if (this.cutB + dragLengthY < 0) dragLengthY = -this.cutB
}
this.setData({
cutL: this.cutL - dragLengthX,
cutT: this.cutT - dragLengthY,
cutR: this.cutR + dragLengthX,
cutB: this.cutB + dragLengthY
})
PAGE_X = e.touches[0].pageX
PAGE_Y = e.touches[0].pageY
},
contentTouchEnd() {
},
//
getImageInfo() {
var _this = this
uni.showLoading({
title: '图片生成中...',
})
//
const ctx = uni.createCanvasContext('myCanvas')
ctx.drawImage(_this.imageSrc, 0, 0, IMG_REAL_W, IMG_REAL_H)
ctx.draw(true, () => {
// * canvasT = (_this.cutT / _this.cropperH) * (_this.imageH / pixelRatio)
var canvasW = ((_this.cropperW - _this.cutL - _this.cutR) / _this.cropperW) * IMG_REAL_W
var canvasH = ((_this.cropperH - _this.cutT - _this.cutB) / _this.cropperH) * IMG_REAL_H
var canvasL = (_this.cutL / _this.cropperW) * IMG_REAL_W
var canvasT = (_this.cutT / _this.cropperH) * IMG_REAL_H
uni.canvasToTempFilePath({
x: canvasL,
y: canvasT,
width: canvasW,
height: canvasH,
destWidth: canvasW,
destHeight: canvasH,
quality: 0.5,
canvasId: 'myCanvas',
success: function (res) {
uni.hideLoading()
postCurrentUserUploadAvatar(res.tempFilePath).then(response => {
store.commit('SET_AVATAR', response.data)
uni.showToast({ title: "修改成功", icon: 'success' })
uni.navigateBack()
})
}
})
})
},
// touchStart
dragStart(e) {
T_PAGE_X = e.touches[0].pageX
T_PAGE_Y = e.touches[0].pageY
CUT_L = this.cutL
CUT_R = this.cutR
CUT_B = this.cutB
CUT_T = this.cutT
},
// touchMove
dragMove(e) {
var _this = this
var dragType = e.target.dataset.drag
switch (dragType) {
case 'right':
var dragLength = (T_PAGE_X - e.touches[0].pageX) * DRAFG_MOVE_RATIO
if (CUT_R + dragLength < 0) dragLength = -CUT_R
this.setData({
cutR: CUT_R + dragLength
})
break
case 'left':
var dragLength = (T_PAGE_X - e.touches[0].pageX) * DRAFG_MOVE_RATIO
if (CUT_L - dragLength < 0) dragLength = CUT_L
if ((CUT_L - dragLength) > (this.cropperW - this.cutR)) dragLength = CUT_L - (this.cropperW - this.cutR)
this.setData({
cutL: CUT_L - dragLength
})
break
case 'top':
var dragLength = (T_PAGE_Y - e.touches[0].pageY) * DRAFG_MOVE_RATIO
if (CUT_T - dragLength < 0) dragLength = CUT_T
if ((CUT_T - dragLength) > (this.cropperH - this.cutB)) dragLength = CUT_T - (this.cropperH - this.cutB)
this.setData({
cutT: CUT_T - dragLength
})
break
case 'bottom':
var dragLength = (T_PAGE_Y - e.touches[0].pageY) * DRAFG_MOVE_RATIO
if (CUT_B + dragLength < 0) dragLength = -CUT_B
this.setData({
cutB: CUT_B + dragLength
})
break
case 'rightBottom':
var dragLengthX = (T_PAGE_X - e.touches[0].pageX) * DRAFG_MOVE_RATIO
var dragLengthY = (T_PAGE_Y - e.touches[0].pageY) * DRAFG_MOVE_RATIO
if (CUT_B + dragLengthY < 0) dragLengthY = -CUT_B
if (CUT_R + dragLengthX < 0) dragLengthX = -CUT_R
let cutB = CUT_B + dragLengthY
let cutR = CUT_R + dragLengthX
this.setData({
cutB: cutB,
cutR: cutR
})
break
default:
break
}
}
}
}
</script>
<style>
/* pages/uni-cropper/index.wxss */
.uni-content-info {
/* position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: block;
align-items: center;
flex-direction: column; */
}
.cropper-config {
padding: 20rpx 40rpx;
}
.cropper-content {
min-height: 750rpx;
width: 100%;
}
.uni-corpper {
position: relative;
overflow: hidden;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
box-sizing: border-box;
}
.uni-corpper-content {
position: relative;
}
.uni-corpper-content image {
display: block;
width: 100%;
min-width: 0 !important;
max-width: none !important;
height: 100%;
min-height: 0 !important;
max-height: none !important;
image-orientation: 0deg !important;
margin: 0 auto;
}
/* 移动图片效果 */
.uni-cropper-drag-box {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
cursor: move;
background: rgba(0, 0, 0, 0.6);
z-index: 1;
}
/* 内部的信息 */
.uni-corpper-crop-box {
position: absolute;
background: rgba(255, 255, 255, 0.3);
z-index: 2;
}
.uni-corpper-crop-box .uni-cropper-view-box {
position: relative;
display: block;
width: 100%;
height: 100%;
overflow: visible;
outline: 1rpx solid #69f;
outline-color: rgba(102, 153, 255, .75)
}
/* 横向虚线 */
.uni-cropper-dashed-h {
position: absolute;
top: 33.33333333%;
left: 0;
width: 100%;
height: 33.33333333%;
border-top: 1rpx dashed rgba(255, 255, 255, 0.5);
border-bottom: 1rpx dashed rgba(255, 255, 255, 0.5);
}
/* 纵向虚线 */
.uni-cropper-dashed-v {
position: absolute;
left: 33.33333333%;
top: 0;
width: 33.33333333%;
height: 100%;
border-left: 1rpx dashed rgba(255, 255, 255, 0.5);
border-right: 1rpx dashed rgba(255, 255, 255, 0.5);
}
/* 四个方向的线 为了之后的拖动事件*/
.uni-cropper-line-t {
position: absolute;
display: block;
width: 100%;
background-color: #69f;
top: 0;
left: 0;
height: 1rpx;
opacity: 0.1;
cursor: n-resize;
}
.uni-cropper-line-t::before {
content: '';
position: absolute;
top: 50%;
right: 0rpx;
width: 100%;
-webkit-transform: translate3d(0, -50%, 0);
transform: translate3d(0, -50%, 0);
bottom: 0;
height: 41rpx;
background: transparent;
z-index: 11;
}
.uni-cropper-line-r {
position: absolute;
display: block;
background-color: #69f;
top: 0;
right: 0rpx;
width: 1rpx;
opacity: 0.1;
height: 100%;
cursor: e-resize;
}
.uni-cropper-line-r::before {
content: '';
position: absolute;
top: 0;
left: 50%;
width: 41rpx;
-webkit-transform: translate3d(-50%, 0, 0);
transform: translate3d(-50%, 0, 0);
bottom: 0;
height: 100%;
background: transparent;
z-index: 11;
}
.uni-cropper-line-b {
position: absolute;
display: block;
width: 100%;
background-color: #69f;
bottom: 0;
left: 0;
height: 1rpx;
opacity: 0.1;
cursor: s-resize;
}
.uni-cropper-line-b::before {
content: '';
position: absolute;
top: 50%;
right: 0rpx;
width: 100%;
-webkit-transform: translate3d(0, -50%, 0);
transform: translate3d(0, -50%, 0);
bottom: 0;
height: 41rpx;
background: transparent;
z-index: 11;
}
.uni-cropper-line-l {
position: absolute;
display: block;
background-color: #69f;
top: 0;
left: 0;
width: 1rpx;
opacity: 0.1;
height: 100%;
cursor: w-resize;
}
.uni-cropper-line-l::before {
content: '';
position: absolute;
top: 0;
left: 50%;
width: 41rpx;
-webkit-transform: translate3d(-50%, 0, 0);
transform: translate3d(-50%, 0, 0);
bottom: 0;
height: 100%;
background: transparent;
z-index: 11;
}
.uni-cropper-point {
width: 5rpx;
height: 5rpx;
background-color: #69f;
opacity: .75;
position: absolute;
z-index: 3;
}
.point-t {
top: -3rpx;
left: 50%;
margin-left: -3rpx;
cursor: n-resize;
}
.point-tr {
top: -3rpx;
left: 100%;
margin-left: -3rpx;
cursor: n-resize;
}
.point-r {
top: 50%;
left: 100%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
.point-rb {
left: 100%;
top: 100%;
-webkit-transform: translate3d(-50%, -50%, 0);
transform: translate3d(-50%, -50%, 0);
cursor: n-resize;
width: 36rpx;
height: 36rpx;
background-color: #69f;
position: absolute;
z-index: 1112;
opacity: 1;
}
.point-b {
left: 50%;
top: 100%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
.point-bl {
left: 0%;
top: 100%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
.point-l {
left: 0%;
top: 50%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
.point-lt {
left: 0%;
top: 0%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
/* 裁剪框预览内容 */
.uni-cropper-viewer {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.uni-cropper-viewer image {
position: absolute;
z-index: 2;
}
</style>

View File

@ -0,0 +1,105 @@
<template>
<view class="help-container">
<view v-for="(item, findex) in list" :key="findex" :title="item.title" class="list-title">
<view class="text-title">
<view>{{ item.title }}</view>
</view>
<view class="childList">
<view v-for="(child, zindex) in item.childList" :key="zindex" class="question" hover-class="hover"
@click="handleText(child)">
<view class="text-item">{{ child.title }}</view>
<view class="line" v-if="zindex !== item.childList.length - 1"></view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
list: [{
title: 'KINIT 问题',
childList: [{
title: 'KINIT-UNI 是使用若依-移动端进行的二次开发吗?',
content: '是的,是在若依-移动端的基础上进行的二次开发,在此感谢若依团队!二次开发中我们重新将接口请求改为 luch-request 组件,项目结构也有所改动,并且加入了 uView UI 组件uni-simple-router 路由拦截。'
},{
title: 'KINIT 开源吗?',
content: '开源'
}, {
title: 'KINIT 可以商用吗?',
content: '可以'
}, {
title: 'KINIT 源码地址多少?',
content: 'https://gitee.com/ktianc/kinit'
}]
},
{
title: '其他问题',
childList: [{
title: '如何退出登录?',
content: '请点击[我的] - [应用设置] - [退出登录]即可退出登录',
}, {
title: '如何修改用户头像?',
content: '请点击[我的] - [选择头像] - [点击提交]即可更换用户头像',
}, {
title: '如何修改登录密码?',
content: '请点击[我的] - [应用设置] - [修改密码]即可修改登录密码',
}]
}
]
}
},
methods: {
handleText(item) {
this.$tab.navigateTo(`/pages/common/textview/index?title=${item.title}&content=${item.content}`)
}
}
}
</script>
<style lang="scss">
page {
background-color: #f8f8f8;
}
.help-container {
margin-bottom: 100rpx;
padding: 30rpx;
}
.list-title {
margin-bottom: 30rpx;
}
.childList {
background: #ffffff;
box-shadow: 0px 0px 10rpx rgba(193, 193, 193, 0.2);
border-radius: 16rpx;
margin-top: 10rpx;
}
.line {
width: 100%;
height: 1rpx;
background-color: #F5F5F5;
}
.text-title {
color: #303133;
font-size: 32rpx;
font-weight: bold;
margin-left: 10rpx;
}
.text-item {
font-size: 28rpx;
padding: 24rpx;
}
.question {
color: #606266;
font-size: 28rpx;
}
</style>

View File

@ -0,0 +1,242 @@
<template>
<view class="mine-container" :style="{height: `${windowHeight}px`}">
<!--顶部个人信息栏-->
<view class="header-section">
<view class="flex padding justify-between">
<view class="flex align-center">
<view v-if="!avatar" class="cu-avatar xl round bg-white">
<view class="iconfont icon-people text-gray icon"></view>
</view>
<image v-if="avatar" @click="handleToAvatar" :src="avatar" class="cu-avatar xl round" mode="aspectFill">
</image>
<view v-if="!name" @click="handleToLogin" class="login-tip">
点击登录
</view>
<view v-if="name" @click="handleToInfo" class="user-info">
<view class="u_title">
用户名{{ name }}
</view>
</view>
</view>
<view @click="handleToInfo" class="flex align-center">
<text style="font-size: 30rpx;">个人信息</text>
<view class="iconfont icon-right1"></view>
</view>
</view>
</view>
<view class="content-section">
<view class="mine-actions grid col-4 text-center">
<view class="action-item" @click="handleJiaoLiuQun">
<view class="iconfont icon-xitongjiaose text-pink icon"></view>
<text class="text">交流群</text>
</view>
<view class="action-item" @click="handleBuilding">
<view class="iconfont icon-kefu text-blue icon"></view>
<text class="text">在线客服</text>
</view>
<view class="action-item" @click="handleBuilding">
<view class="iconfont icon-yaoqingdaoshi text-mauve icon"></view>
<text class="text">反馈社区</text>
</view>
<view class="action-item" @click="praiseMe">
<view class="iconfont icon-dianzan text-green icon"></view>
<view style="height: 0px;" :animation="animationData" class="praise-me animation-opacity"> +1 </view>
<text class="text">点赞我们</text>
</view>
</view>
<view class="menu-list">
<view class="list-cell list-cell-arrow" @click="handleToEditInfo">
<view class="menu-item-box">
<view class="iconfont icon-user menu-icon"></view>
<view>编辑资料</view>
</view>
</view>
<view class="list-cell list-cell-arrow" @click="handleHelp">
<view class="menu-item-box">
<view class="iconfont icon-wenti1-copy menu-icon"></view>
<view>常见问题</view>
</view>
</view>
<view class="list-cell list-cell-arrow" @click="handleAbout">
<view class="menu-item-box">
<view class="iconfont icon-aixin menu-icon"></view>
<view>关于我们</view>
</view>
</view>
<view class="list-cell list-cell-arrow" @click="handleToSetting">
<view class="menu-item-box">
<view class="iconfont icon-shezhi2 menu-icon"></view>
<view>应用设置</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import storage from '@/common/utils/storage'
export default {
data() {
return {
animation: "",
animationData: {}
}
},
onLoad() {
// 1
this.animation = uni.createAnimation();
this.animationData = {};
},
onUnload() {
// 5
this.animationData = {};
},
computed: {
version() {
return this.$store.state.app.version
},
name() {
return this.$store.state.auth.name
},
avatar() {
return this.$store.state.auth.avatar
},
windowHeight() {
return uni.getSystemInfoSync().windowHeight - 50
}
},
methods: {
handleToInfo() {
this.$tab.navigateTo('/pages/mine/info/index')
},
handleToEditInfo() {
this.$tab.navigateTo('/pages/mine/info/edit')
},
handleToSetting() {
this.$tab.navigateTo('/pages/mine/setting/index')
},
handleToLogin() {
this.$tab.reLaunch('/pages/login')
},
handleToAvatar() {
this.$tab.navigateTo('/pages/mine/avatar/index')
},
handleLogout() {
this.$modal.confirm('确定注销并退出系统吗?').then(() => {
this.$store.dispatch('LogOut').then(() => {
this.$tab.reLaunch('/pages/index')
})
})
},
handleHelp() {
this.$tab.navigateTo('/pages/mine/help/index')
},
handleAbout() {
this.$tab.navigateTo('/pages/mine/about/index')
},
handleJiaoLiuQun() {
this.$modal.showToast('模块建设中~')
},
handleBuilding() {
this.$modal.showToast('模块建设中~')
},
//
praiseMe() {
// 2 step()
this.animation.translateY(-90).opacity(1).step({
duration: 400
});
// 3 exportanimation
this.animationData = this.animation.export();
// 4
setTimeout(()=> {
this.animation.translateY(0).opacity(0).step({
duration: 0
});
this.animationData = this.animation.export();
}, 300)
},
}
}
</script>
<style lang="scss" scoped>
.praise-me {
font-size: 14px;
color: #feab2a;
}
.animation-opacity {
font-weight: bold;
opacity: 0;
}
page {
background-color: #f5f6f7;
}
.mine-container {
width: 100%;
height: 100%;
.header-section {
padding: 15px 15px 45px 15px;
background-color: #3c96f3;
color: white;
.login-tip {
font-size: 18px;
margin-left: 10px;
}
.cu-avatar {
border: 2px solid #eaeaea;
.icon {
font-size: 40px;
}
}
.user-info {
margin-left: 15px;
.u_title {
font-size: 37rpx;
line-height: 30px;
}
}
}
.content-section {
position: relative;
top: -50px;
.mine-actions {
margin: 15px 15px;
padding: 20px 0px;
border-radius: 8px;
background-color: white;
.action-item {
.icon {
font-size: 28px;
}
.text {
display: block;
font-size: 13px;
margin: 8px 0px;
}
}
}
}
}
</style>

View File

@ -0,0 +1,159 @@
<template>
<view class="container">
<view style="padding: 20px;">
<u--form
labelPosition="left"
labelWidth="100px"
:model="form"
:rules="rules"
ref="formRef"
>
<u-form-item
label="用户姓名"
prop="name"
borderBottom
:required="true"
>
<u--input
v-model="form.name"
placeholder="请输入用户姓名"
border="none"
></u--input>
</u-form-item>
<u-form-item
label="用户昵称"
prop="nickname"
borderBottom
:required="false"
>
<u--input
v-model="form.nickname"
placeholder="请输入用户昵称"
border="none"
></u--input>
</u-form-item>
<u-form-item
label="手机号码"
prop="telephone"
borderBottom
:required="true"
>
<u--input
v-model="form.telephone"
placeholder="请输入手机号码"
border="none"
></u--input>
</u-form-item>
<u-form-item
label="用户性别"
prop="gender"
borderBottom
:required="false"
>
<u-radio-group v-model="form.gender">
<u-radio
:customStyle="{marginRight: '16px'}"
v-for="(item, index) in genderOptions"
:key="index"
:label="item.label"
:name="item.value"
>
</u-radio>
</u-radio-group>
</u-form-item>
</u--form>
<view style="margin-top: 20px;">
<u-button :loading="btnLoading" type="primary" @click="submit" text="提交"></u-button>
</view>
</view>
</view>
</template>
<script>
import { getInfo } from '@/common/request/api/login'
import { updateCurrentUser } from '@/common/request/api/vadmin/auth/user.js'
export default {
data() {
return {
btnLoading: false,
form: {
name: "",
nickname: "",
telephone: "",
gender: ""
},
rules: {
name: {
type: 'string',
required: true,
message: '请填写姓名',
trigger: ['blur', 'change']
},
telephone: [
{
type: 'string',
required: true,
message: '请填写正确手机号',
trigger: ['blur', 'change']
},
{
validator: (rule, value, callback) => {
// truefalse
// uni.$u.test.mobile()truefalse
return uni.$u.test.mobile(value);
},
message: '手机号码不正确',
// blurchange
trigger: ['change','blur'],
}
]
},
genderOptions: []
}
},
onLoad() {
this.$store.dispatch('getDicts', ["sys_vadmin_gender"]).then(result => {
this.genderOptions = result.sys_vadmin_gender
})
// this.resetForm()
this.getUser()
},
onReady() {
//onReady uni-app
this.$refs.formRef.setRules(this.rules)
},
methods: {
resetForm() {
this.form = {
name: "",
nickname: "",
telephone: "",
gender: ""
}
},
getUser() {
getInfo().then(res => {
this.form = res.data
})
},
submit(ref) {
this.$refs.formRef.validate().then(res => {
this.btnLoading = true
updateCurrentUser(this.form).then(res => {
this.$store.dispatch('UpdateInfo', res.data)
this.$modal.msgSuccess("更新成功");
}).finally(() => {
this.btnLoading = false
})
})
}
}
}
</script>
<style lang="scss">
page {
background-color: #ffffff;
}
</style>

View File

@ -0,0 +1,49 @@
<template>
<view class="container">
<u-cell-group>
<u-cell title="姓名" :value="name">
<u-icon slot="icon" class="iconfont icon-user"></u-icon>
</u-cell>
<u-cell title="昵称" :value="nickname">
<u-icon slot="icon" class="iconfont icon-user"></u-icon>
</u-cell>
<u-cell title="手机号码" :value="telephone">
<u-icon slot="icon" class="iconfont icon-dianhua"></u-icon>
</u-cell>
<u-cell title="角色" :value="roles.join(',')">
<u-icon slot="icon" class="iconfont icon-xitongjiaose"></u-icon>
</u-cell>
<u-cell title="创建日期" :value="createDatetime">
<u-icon slot="icon" class="iconfont icon-jiaofuriqi"></u-icon>
</u-cell>
</u-cell-group>
</view>
</template>
<script>
export default {
computed: {
name() {
return this.$store.state.auth.name
},
nickname() {
return this.$store.state.auth.nickname
},
telephone() {
return this.$store.state.auth.telephone
},
roles() {
return this.$store.state.auth.roles
},
createDatetime() {
return this.$store.state.auth.createDatetime
}
},
}
</script>
<style lang="scss">
page {
background-color: #ffffff;
}
</style>

View File

@ -0,0 +1,79 @@
<template>
<view class="pwd-retrieve-container">
<uni-forms ref="form" :value="form" labelWidth="80px">
<uni-forms-item name="newPassword" label="新密码">
<uni-easyinput type="password" v-model="form.password" placeholder="请输入新密码" />
</uni-forms-item>
<uni-forms-item name="confirmPassword" label="确认密码">
<uni-easyinput type="password" v-model="form.password_two" placeholder="请确认新密码" />
</uni-forms-item>
<button :loading="btnLoading" type="primary" @click="submit">提交</button>
</uni-forms>
</view>
</template>
<script>
import { postCurrentUserResetPassword } from '@/common/request/api/vadmin/auth/user.js'
export default {
data() {
return {
btnLoading: false,
form: {
password: undefined,
password_two: undefined
},
rules: {
password: {
rules: [{
required: true,
errorMessage: '新密码不能为空',
},
{
minLength: 8,
maxLength: 20,
errorMessage: '长度在 8 到 20 个字符'
}
]
},
password_two: {
rules: [{
required: true,
errorMessage: '确认密码不能为空'
}, {
validateFunction: (rule, value, data) => data.password === value,
errorMessage: '两次输入的密码不一致'
}
]
}
}
}
},
onReady() {
this.$refs.form.setRules(this.rules)
},
methods: {
submit() {
this.$refs.form.validate().then(res => {
this.btnLoading = true
postCurrentUserResetPassword(this.form).then(response => {
this.$modal.msgSuccess("修改成功")
}).finally(() => {
this.btnLoading = false
})
})
}
}
}
</script>
<style lang="scss">
page {
background-color: #ffffff;
}
.pwd-retrieve-container {
padding-top: 36rpx;
padding: 15px;
}
</style>

View File

@ -0,0 +1,76 @@
<template>
<view class="setting-container" :style="{height: `${windowHeight}px`}">
<view class="menu-list">
<view class="list-cell list-cell-arrow" @click="handleToPwd">
<view class="menu-item-box">
<view class="iconfont icon-password menu-icon"></view>
<view>修改密码</view>
</view>
</view>
<view class="list-cell list-cell-arrow" @click="handleToUpgrade">
<view class="menu-item-box">
<view class="iconfont icon-refresh menu-icon"></view>
<view>检查更新</view>
</view>
</view>
<view class="list-cell list-cell-arrow" @click="handleCleanTmp">
<view class="menu-item-box">
<view class="iconfont icon-clean menu-icon"></view>
<view>清理缓存</view>
</view>
</view>
</view>
<view class="cu-list menu">
<view class="cu-item item-box">
<view class="content text-center" @click="handleLogout">
<text class="text-black">退出登录</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
windowHeight: uni.getSystemInfoSync().windowHeight
}
},
methods: {
handleToPwd() {
this.$tab.navigateTo('/pages/mine/pwd/index')
},
handleToUpgrade() {
this.$modal.showToast('模块建设中~')
},
handleCleanTmp() {
this.$modal.showToast('模块建设中~')
},
handleLogout() {
this.$modal.confirm('确定注销并退出系统吗?').then(() => {
this.$store.dispatch('LogOut')
})
}
}
}
</script>
<style lang="scss">
.page {
background-color: #f8f8f8;
}
.item-box {
background-color: #FFFFFF;
margin: 30rpx;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 10rpx;
border-radius: 8rpx;
color: #303133;
font-size: 32rpx;
}
</style>

View File

@ -0,0 +1,100 @@
<template>
<view class="work-container">
<!-- 轮播图 -->
<u-swiper
:list="images"
indicator
indicatorMode="line"
circular
:height="`${windowWidth / 2.5}px`"
></u-swiper>
<!-- 宫格组件 -->
<view class="grid-body">
<u-grid
:border="false"
col="3"
@click="changeGrid"
>
<u-grid-item
v-for="(item, index) in baseList"
:key="index"
>
<view class="grid-item">
<view :class="'iconfont ' + item.icon + ' grid-icon'"></view>
<u--text :text="item.title" align="center" lineHeight="32px"></u--text>
</view>
</u-grid-item>
</u-grid>
</view>
</view>
</template>
<script>
export default {
data() {
return {
windowWidth: uni.getSystemInfoSync().windowWidth,
images: [
'https://ktianc.oss-cn-beijing.aliyuncs.com/kinit/system/banner/2022-11-14/1.jpg',
'/static/images/banner/banner03.jpg',
'/static/images/banner/banner03.jpg'
],
baseList: [
{
icon: 'icon-user1',
title: '用户管理'
},
{
icon: 'icon-users',
title: '角色管理'
},
{
icon: 'icon-caidan3',
title: '菜单管理'
},
{
icon: 'icon-shezhitianchong',
title: '系统配置'
},
{
icon: 'icon-changguizidian',
title: '字典管理'
},
{
icon: 'icon-rizhi',
title: '日志管理'
},
]
}
},
methods: {
changeGrid(e) {
this.$modal.showToast('模块建设中~')
}
}
}
</script>
<style lang="scss">
page {
background-color: #fff;
min-height: 100%;
height: auto;
}
</style>
<style lang="scss" scoped>
.grid-body {
margin-top: 60rpx;
.grid-item {
margin-bottom: 30rpx;
text-align: center;
}
.grid-icon {
font-size: 40rpx;
}
}
</style>

38
kinit-uni/permission.js Normal file
View File

@ -0,0 +1,38 @@
import { getToken } from '@/common/utils/auth'
import store from '@/store'
import {RouterMount, createRouter} from 'uni-simple-router';
// 登录页面
const loginPage = "/pages/login"
const router = createRouter({
platform: process.env.VUE_APP_PLATFORM,
routes: [...ROUTES]
});
//全局路由前置守卫
router.beforeEach((to, from, next) => {
if (to.meta.loginAuth) {
if (getToken()) {
if (!store.state.auth.isUser) {
store.dispatch('GetInfo')
}
if (to.path === loginPage) {
uni.reLaunch({ url: "/" })
}
} else {
uni.reLaunch({ url: loginPage })
}
}
next();
});
// 全局路由后置守卫
router.afterEach((to, from) => {
// console.log('跳转结束')
})
export {
router,
RouterMount
}

60
kinit-uni/plugins/auth.js Normal file
View File

@ -0,0 +1,60 @@
import store from '@/store'
function authPermission(permission) {
const all_permission = "*:*:*"
const permissions = store.getters && store.getters.permissions
if (permission && permission.length > 0) {
return permissions.some(v => {
return all_permission === v || v === permission
})
} else {
return false
}
}
function authRole(role) {
const super_admin = "admin"
const roles = store.getters && store.getters.roles
if (role && role.length > 0) {
return roles.some(v => {
return super_admin === v || v === role
})
} else {
return false
}
}
export default {
// 验证用户是否具备某权限
hasPermi(permission) {
return authPermission(permission)
},
// 验证用户是否含有指定权限只需包含其中一个
hasPermiOr(permissions) {
return permissions.some(item => {
return authPermission(item)
})
},
// 验证用户是否含有指定权限必须全部拥有
hasPermiAnd(permissions) {
return permissions.every(item => {
return authPermission(item)
})
},
// 验证用户是否具备某角色
hasRole(role) {
return authRole(role)
},
// 验证用户是否含有指定角色只需包含其中一个
hasRoleOr(roles) {
return roles.some(item => {
return authRole(item)
})
},
// 验证用户是否含有指定角色必须全部拥有
hasRoleAnd(roles) {
return roles.every(item => {
return authRole(item)
})
}
}

Some files were not shown because too many files have changed in this diff Show More