mirror of https://github.com/veypi/OneAuth.git
add user role auth page
parent
bc3f5e0b0c
commit
bcbfc0380a
@ -0,0 +1,12 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/veypi/OneBD"
|
||||
"github.com/veypi/OneBD/rfc"
|
||||
)
|
||||
|
||||
func Router(r OneBD.Router) {
|
||||
r.Set("/", appHandlerP, rfc.MethodPost, rfc.MethodGet)
|
||||
r.Set("/:uuid", appHandlerP, rfc.MethodGet, rfc.MethodPatch)
|
||||
r.Set("/:uuid/user/:id", auHandlerP, rfc.MethodAll)
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"OneAuth/cfg"
|
||||
"OneAuth/libs/app"
|
||||
"OneAuth/libs/auth"
|
||||
"OneAuth/libs/base"
|
||||
"OneAuth/libs/oerr"
|
||||
"OneAuth/models"
|
||||
"github.com/veypi/OneBD"
|
||||
)
|
||||
|
||||
var auHandlerP = OneBD.NewHandlerPool(func() OneBD.Handler {
|
||||
h := &appUserHandler{}
|
||||
return h
|
||||
})
|
||||
|
||||
type appUserHandler struct {
|
||||
base.ApiHandler
|
||||
uuid string
|
||||
}
|
||||
|
||||
func (h *appUserHandler) Init(m OneBD.Meta) error {
|
||||
h.uuid = m.Params("uuid")
|
||||
if h.uuid == "-" {
|
||||
h.uuid = ""
|
||||
}
|
||||
return h.ApiHandler.Init(m)
|
||||
}
|
||||
|
||||
func (h *appUserHandler) Get() (interface{}, error) {
|
||||
id := h.Meta().ParamsInt("id")
|
||||
if h.uuid == "" && id == 0 {
|
||||
return nil, oerr.ApiArgsMissing
|
||||
}
|
||||
if uint(id) != h.Payload.ID && !h.Payload.GetAuth(auth.User, h.uuid).CanRead() {
|
||||
return nil, oerr.NoAuth
|
||||
}
|
||||
au := &models.AppUser{
|
||||
UserID: uint(id),
|
||||
AppUUID: h.uuid,
|
||||
}
|
||||
list := make([]*models.AppUser, 0, 10)
|
||||
err := cfg.DB().Preload("User").Where(au).Find(&list).Error
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (h *appUserHandler) Post() (interface{}, error) {
|
||||
id := h.Meta().ParamsInt("id")
|
||||
if h.uuid == "" || id <= 0 {
|
||||
return nil, oerr.ApiArgsMissing
|
||||
}
|
||||
status := models.AUOK
|
||||
target := &models.App{}
|
||||
err := cfg.DB().Where("uuid = ?", h.uuid).First(target).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if target.EnableRegister {
|
||||
status = models.AUApply
|
||||
}
|
||||
au, err := app.AddUser(cfg.DB(), h.uuid, uint(id), target.InitRoleID, status)
|
||||
return au, err
|
||||
}
|
||||
|
||||
func (h *appUserHandler) Update() (interface{}, error) {
|
||||
id := h.Meta().ParamsInt("id")
|
||||
if h.uuid == "" || id <= 0 {
|
||||
return nil, oerr.ApiArgsMissing
|
||||
}
|
||||
props := struct {
|
||||
Status string `json:"status"`
|
||||
}{}
|
||||
err := h.Meta().ReadJson(&props)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if uint(id) != h.Payload.ID && !h.Payload.GetAuth(auth.User, h.uuid).CanUpdate() {
|
||||
return nil, oerr.NoAuth
|
||||
}
|
||||
au := &models.AppUser{
|
||||
UserID: uint(id),
|
||||
AppUUID: h.uuid,
|
||||
}
|
||||
err = cfg.DB().Where(au).Update("status", props.Status).Error
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (h *appUserHandler) Delete() (interface{}, error) {
|
||||
id := h.Meta().ParamsInt("id")
|
||||
if h.uuid == "" && id <= 0 {
|
||||
return nil, oerr.ApiArgsMissing
|
||||
}
|
||||
if uint(id) != h.Payload.ID && h.Payload.GetAuth(auth.User, h.uuid).CanDelete() {
|
||||
return nil, oerr.NoAuth
|
||||
}
|
||||
au := &models.AppUser{
|
||||
AppUUID: h.uuid,
|
||||
UserID: uint(id),
|
||||
}
|
||||
list := make([]*models.AppUser, 0, 10)
|
||||
err := cfg.DB().Where(au).Delete(&list).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = cfg.DB().Delete(&list).Error
|
||||
return nil, err
|
||||
}
|
@ -1,13 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<link rel="icon" href="/favicon.ico"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="loader-wrapper">
|
||||
<div class="lds-ripple">
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="load_title">Loading...
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
<style>
|
||||
#loader-wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 999999;
|
||||
background: #999;
|
||||
}
|
||||
|
||||
#loader-wrapper .load_title {
|
||||
font-family: 'Open Sans';
|
||||
color: #FFF;
|
||||
font-size: 19px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
z-index: 9999999999999;
|
||||
position: absolute;
|
||||
top: 70%;
|
||||
opacity: 1;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
#loader-wrapper .load_title span {
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
font-size: 13px;
|
||||
color: #FFF;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.lds-ripple {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
left: calc(50% - 40px);
|
||||
top: calc(70% - 100px);
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.lds-ripple div {
|
||||
position: absolute;
|
||||
border: 4px solid #fff;
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
}
|
||||
|
||||
.lds-ripple div:nth-child(2) {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
|
||||
@keyframes lds-ripple {
|
||||
0% {
|
||||
top: 36px;
|
||||
left: 36px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</html>
|
||||
|
@ -0,0 +1,16 @@
|
||||
import {Interface} from './interface'
|
||||
import ajax from './ajax'
|
||||
import {BaseUrl} from './setting'
|
||||
|
||||
export default (uuid: string) => {
|
||||
return {
|
||||
local: BaseUrl + 'app/' + uuid + '/auth/',
|
||||
get(id: number) {
|
||||
return new Interface(ajax.get, this.local + id)
|
||||
},
|
||||
list(appUUID: string, user_id: number) {
|
||||
return new Interface(ajax.get, this.local, {uuid: appUUID, id: user_id})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,59 @@
|
||||
import {store} from "@/store";
|
||||
|
||||
export type SuccessFunction<T> = (e: any) => void;
|
||||
export type FailedFunction<T> = (e: any) => void;
|
||||
|
||||
const Code = {
|
||||
42011: '无操作权限',
|
||||
22031: '资源不存在 或 您无权操作该资源'
|
||||
}
|
||||
|
||||
export class Interface {
|
||||
private readonly method: Function
|
||||
private readonly api: string
|
||||
private readonly data: any
|
||||
|
||||
constructor(method: Function, api: string, data?: any) {
|
||||
this.method = method
|
||||
this.api = api
|
||||
this.data = data
|
||||
}
|
||||
|
||||
Start(success: SuccessFunction<any>, fail?: FailedFunction<any>) {
|
||||
const newFail = function (data: any) {
|
||||
if (data) {
|
||||
if (data.code === 40001) {
|
||||
// no login
|
||||
store.commit('user/logout')
|
||||
return
|
||||
// @ts-ignore
|
||||
} else if (data.code === 42011 && window.$msg) {
|
||||
// @ts-ignore
|
||||
window.$msg.warning('无权限')
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// @ts-ignore
|
||||
if (data && data.code && Code[data.code]) {
|
||||
}
|
||||
if (fail) {
|
||||
fail(data.err)
|
||||
}
|
||||
}
|
||||
|
||||
const newSuccess = function (data: any) {
|
||||
if (Number(data.status) === 1) {
|
||||
if (success) {
|
||||
success(data.content)
|
||||
}
|
||||
} else {
|
||||
newFail(data)
|
||||
if (data.code === 41001) {
|
||||
store.commit('user/logout')
|
||||
// bus.$emit('log_out')
|
||||
}
|
||||
}
|
||||
}
|
||||
this.method(this.api, this.data, newSuccess, newFail)
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import {BaseUrl} from './setting'
|
||||
import {Interface} from './interface'
|
||||
import ajax from './ajax'
|
||||
|
||||
export default (uuid: string) => {
|
||||
return {
|
||||
local: BaseUrl +'app/' + uuid + '/role/',
|
||||
get(id: number) {
|
||||
return new Interface(ajax.get, this.local + id)
|
||||
},
|
||||
list() {
|
||||
return new Interface(ajax.get, this.local)
|
||||
},
|
||||
create(uuid: string, name: string) {
|
||||
return new Interface(ajax.post, this.local, {
|
||||
uuid: uuid,
|
||||
name: name,
|
||||
})
|
||||
},
|
||||
bind(id: number, aid: number) {
|
||||
},
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import {Base64} from "js-base64";
|
||||
import {Interface} from './interface'
|
||||
import ajax from './ajax'
|
||||
import {BaseUrl} from './setting'
|
||||
|
||||
export default {
|
||||
local: BaseUrl + 'user/',
|
||||
register(username: string, password: string, prop?: any) {
|
||||
const data = Object.assign({
|
||||
username: username,
|
||||
password: Base64.encode(password)
|
||||
}, prop)
|
||||
return new Interface(ajax.post, this.local, data)
|
||||
},
|
||||
login(username: string, password: string, uuid: string) {
|
||||
return new Interface(ajax.head, this.local + username, {
|
||||
uid_type: 'username',
|
||||
uuid: uuid,
|
||||
password: Base64.encode(password)
|
||||
})
|
||||
},
|
||||
get(id: number) {
|
||||
return new Interface(ajax.get, this.local + id)
|
||||
},
|
||||
list() {
|
||||
return new Interface(ajax.get, this.local)
|
||||
},
|
||||
update(id: number, props: any) {
|
||||
return new Interface(ajax.patch, this.local + id, props)
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
export const R = {
|
||||
// 应用管理配置权限
|
||||
App: 'app',
|
||||
// 用户管理和绑定应用权限
|
||||
User: 'user',
|
||||
// 权限资源定义权限
|
||||
Resource: 'resource',
|
||||
// 角色管理和绑定用户权限
|
||||
Role: 'role',
|
||||
// 权限管理和绑定角色权限
|
||||
Auth: 'auth',
|
||||
}
|
||||
|
||||
const level = {
|
||||
None: 0,
|
||||
Do: 1,
|
||||
Part: 1,
|
||||
Read: 2,
|
||||
Create: 3,
|
||||
Update: 4,
|
||||
Delete: 5,
|
||||
All: 6
|
||||
}
|
||||
|
||||
export interface Auth {
|
||||
rid: string
|
||||
ruid: string
|
||||
level: number
|
||||
}
|
||||
|
||||
class authLevel {
|
||||
level = level.None
|
||||
constructor(level: number) {
|
||||
this.level = level
|
||||
}
|
||||
CanDo(): boolean {
|
||||
return this.level >= level.Do
|
||||
}
|
||||
CanRead(): boolean {
|
||||
return this.level >= level.Read
|
||||
}
|
||||
CanCreate(): boolean {
|
||||
return this.level >= level.Create
|
||||
}
|
||||
CanUpdate(): boolean {
|
||||
return this.level >= level.Update
|
||||
}
|
||||
CanDelete(): boolean {
|
||||
return this.level >= level.Delete
|
||||
}
|
||||
CanDoAny(): boolean {
|
||||
return this.level >= level.All
|
||||
}
|
||||
}
|
||||
|
||||
export class Auths {
|
||||
private readonly list: [Auth]
|
||||
|
||||
constructor(auths: [Auth]) {
|
||||
this.list = auths
|
||||
}
|
||||
|
||||
Get(rid: string, ruid: string): authLevel {
|
||||
let l = level.None
|
||||
for (let i of this.list) {
|
||||
if (i.rid == rid && (i.ruid === '' || i.ruid === ruid) && i.level > l) {
|
||||
l = i.level
|
||||
}
|
||||
}
|
||||
return new authLevel(l)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function NewAuths(a: any) {
|
||||
return new Auths(a)
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<n-layout has-sider>
|
||||
<n-layout-sider
|
||||
collapse-mode="transform"
|
||||
:collapsed-width="0"
|
||||
:width="150"
|
||||
show-trigger="bar"
|
||||
content-style="padding: 4px;"
|
||||
bordered
|
||||
default-collapsed
|
||||
:native-scrollbar="false"
|
||||
style="height: calc(100vh - 108px)"
|
||||
>
|
||||
<slot name="sider"></slot>
|
||||
</n-layout-sider>
|
||||
<n-layout style="height: calc(100vh - 108px);padding-left: 20px" :native-scrollbar="false">
|
||||
<slot></slot>
|
||||
<n-back-top>
|
||||
</n-back-top>
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="page-h1">首页</h1>
|
||||
{{ uuid }}
|
||||
{{ app }}
|
||||
<div :ref="el => nav[6]=el" class="my-80">123123</div>
|
||||
<div :ref="el => nav[k-1]=el" class="mb-64 text-center" v-for="(k) in [1,2,3,4,5,6]" :key="k">
|
||||
{{ k }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {inject, ref} from "vue";
|
||||
|
||||
let uuid = inject('uuid')
|
||||
let app = inject('app')
|
||||
let nav = ref([])
|
||||
|
||||
defineExpose({
|
||||
nav
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="page-h1">角色管理</h1>
|
||||
<n-data-table
|
||||
:bordered="false"
|
||||
:columns="columns"
|
||||
:data="roles"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {h, inject, onMounted, Ref, ref} from 'vue'
|
||||
import api from '@/api'
|
||||
import {NButton} from 'naive-ui'
|
||||
|
||||
let roles = ref([])
|
||||
let uuid = inject<Ref>('uuid')
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
key: 'id',
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
title: '角色名',
|
||||
key: 'name',
|
||||
width: 100,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'created_at',
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
title: '绑定用户数',
|
||||
key: 'user_count',
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: '',
|
||||
render(row) {
|
||||
return [
|
||||
h(NButton, {
|
||||
class: 'mr-1',
|
||||
size: 'small',
|
||||
onClick: () => console.log(row),
|
||||
},
|
||||
{default: () => '查看权限'}),
|
||||
h(NButton, {
|
||||
class: 'mr-1',
|
||||
size: 'small',
|
||||
onClick: () => console.log(row),
|
||||
},
|
||||
{default: () => '查看用户'},
|
||||
),
|
||||
]
|
||||
},
|
||||
},
|
||||
]
|
||||
onMounted(() => {
|
||||
api.role(uuid.value).list().Start(e => {
|
||||
roles.value = e
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="flex justify-center">
|
||||
<div style="line-height: 48px" class="inline-block mt-16 grid grid-cols-5 w-1/3 text-center gap-4">
|
||||
<div>应用名</div>
|
||||
<div class="col-span-4">
|
||||
<n-input v-model:value="data.name" @blur="update('name')"></n-input>
|
||||
</div>
|
||||
<div>logo</div>
|
||||
<div class="col-span-4">
|
||||
<n-upload
|
||||
action="/api/upload"
|
||||
@finish="handleFinish"
|
||||
:show-file-list="false"
|
||||
>
|
||||
<n-avatar size="large" round :src="data.icon">
|
||||
</n-avatar>
|
||||
</n-upload>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {inject, watch, ref, onMounted} from "vue";
|
||||
import api from "@/api";
|
||||
import {useMessage} from "naive-ui";
|
||||
|
||||
let msg = useMessage()
|
||||
let app: any = inject('app')
|
||||
let data = ref({
|
||||
name: '',
|
||||
icon: ''
|
||||
})
|
||||
|
||||
function handleFinish(e: any) {
|
||||
if (e.event.target.response) {
|
||||
let d = JSON.parse(e.event.target.response)
|
||||
if (d.status === 1) {
|
||||
data.value.icon = d.content
|
||||
update('icon')
|
||||
return
|
||||
}
|
||||
}
|
||||
msg.error('上传失败')
|
||||
data.value.icon = app.value.icon
|
||||
}
|
||||
|
||||
function update(key: string) {
|
||||
// @ts-ignore
|
||||
let v = data.value[key]
|
||||
if (v === app.value[key]) {
|
||||
return
|
||||
}
|
||||
api.app.update(app.value.uuid, {[key]: v}).Start(e => {
|
||||
msg.success('更新成功')
|
||||
app.value[key] = v
|
||||
}, e => {
|
||||
data.value[key] = app.value[key]
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function sync() {
|
||||
data.value.name = app.value.name
|
||||
data.value.icon = app.value.icon
|
||||
}
|
||||
watch(app, sync)
|
||||
onMounted(sync)
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="page-h1">用户名单</h1>
|
||||
<n-data-table
|
||||
:bordered="false"
|
||||
:columns="columns"
|
||||
:data="users"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {inject, onMounted, ref, h} from "vue";
|
||||
import api from "@/api";
|
||||
import {NTag} from 'naive-ui'
|
||||
|
||||
let uuid: any = inject('uuid')
|
||||
let users = ref([])
|
||||
|
||||
onMounted(() => {
|
||||
api.app.user(uuid.value as string).list(0).Start(e => {
|
||||
users.value = e
|
||||
console.log(e)
|
||||
})
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
key: 'user_id',
|
||||
width: 50
|
||||
},
|
||||
{
|
||||
title: '用户',
|
||||
key: 'user.username',
|
||||
width: 200,
|
||||
fixed: 'left'
|
||||
},
|
||||
{
|
||||
title: '加入时间',
|
||||
key: 'user.created_at',
|
||||
fixed: 'left'
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
key: 'status',
|
||||
render(row) {
|
||||
let t = statusTag(row.status)
|
||||
return h(NTag,{
|
||||
'type': t[1]
|
||||
},
|
||||
{
|
||||
default: () => t[0]
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
function statusTag(s: string) {
|
||||
switch (s) {
|
||||
case 'ok':
|
||||
return ['正常', 'success']
|
||||
case "apply":
|
||||
return ['申请中', 'info']
|
||||
case 'deny':
|
||||
return ['拒绝', '']
|
||||
case 'disabled':
|
||||
return ['禁用', 'warning']
|
||||
}
|
||||
return ['未知', '']
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -1,10 +1,95 @@
|
||||
<template>
|
||||
<div></div>
|
||||
<div class="flex items-center justify-center">
|
||||
<div
|
||||
:style="{background:Theme.me.lightBox, 'box-shadow': Theme.me.lightBoxShadow}"
|
||||
class="px-10 pb-9 pt-28 rounded-xl w-96">
|
||||
<n-form label-width="70px" label-align="left" :model="data" ref="form_ref" label-placement="left" :rules="rules">
|
||||
<n-form-item required label="用户名" path="username">
|
||||
<n-input @keydown.enter="divs[1].focus()" :ref="el => {if (el)divs[0]=el}"
|
||||
v-model:value="data.username"></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item required label="密码" path="password">
|
||||
<n-input @keydown.enter="divs[2].focus()" :ref="el => {if (el) divs[1]=el}" v-model:value="data.password"
|
||||
type="password"></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item required label="重复密码" path="pass">
|
||||
<n-input @keydown.enter="register" :ref="el => {if (el) divs[2]=el}" v-model:value="data.pass"
|
||||
type="password"></n-input>
|
||||
</n-form-item>
|
||||
<div class="flex justify-around mt-4">
|
||||
<n-button @click="register">注册</n-button>
|
||||
</div>
|
||||
</n-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {onMounted, ref} from "vue";
|
||||
import {Theme} from "@/theme";
|
||||
import {useMessage} from 'naive-ui'
|
||||
import api from "@/api"
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
let msg = useMessage()
|
||||
const router = useRouter()
|
||||
|
||||
const divs = ref([])
|
||||
let form_ref = ref(null)
|
||||
let data = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
pass: ''
|
||||
})
|
||||
let rules = {
|
||||
username: [
|
||||
{
|
||||
required: true,
|
||||
validator(r: any, v: any) {
|
||||
return (v && v.length >= 3 && v.length <= 16) || new Error('长度要求3~16')
|
||||
},
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
],
|
||||
password: [{
|
||||
required: true,
|
||||
validator(r: any, v: any) {
|
||||
return (v && v.length >= 6 && v.length <= 16) || new Error('长度要求6~16')
|
||||
},
|
||||
trigger: ['input', 'blur']
|
||||
}],
|
||||
pass: [
|
||||
{
|
||||
required: true,
|
||||
validator(r: any, v: any) {
|
||||
return (v && v === data.value.password) || new Error('密码不正确')
|
||||
},
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function register() {
|
||||
// @ts-ignore
|
||||
form_ref.value.validate((e: any) => {
|
||||
if (!e) {
|
||||
api.user.register(data.value.username, data.value.password).Start((url: string) => {
|
||||
msg.success('注册成功')
|
||||
router.push({name: 'login'})
|
||||
}, e => {
|
||||
console.log(e)
|
||||
msg.warning('注册失败:' + e)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (divs.value[0]) {
|
||||
// @ts-ignore
|
||||
divs.value[0].focus()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
@ -0,0 +1,5 @@
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
@ -0,0 +1 @@
|
||||
src/libs/wwLogin.js
|
@ -0,0 +1,23 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/essential',
|
||||
'@vue/standard',
|
||||
'@vue/typescript/recommended'
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020
|
||||
},
|
||||
rules: {
|
||||
'object-curly-spacing': 0,
|
||||
'space-before-function-paren': 0,
|
||||
'@typescript-eslint/camelcase': 0,
|
||||
'@typescript-eslint/no-empty-function': 0,
|
||||
'@typescript-eslint/no-explicit-any': 0,
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
@ -0,0 +1,24 @@
|
||||
# oaf
|
||||
|
||||
## Project setup
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
yarn serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
yarn build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
yarn lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "oaf",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@veypi/one-icon": "^1.0.1",
|
||||
"axios": "^0.21.1",
|
||||
"core-js": "^3.6.5",
|
||||
"js-base64": "^3.6.0",
|
||||
"vue": "^2.6.11",
|
||||
"vue-class-component": "^7.2.3",
|
||||
"vue-m-message": "^3.1.0",
|
||||
"vue-property-decorator": "^9.1.2",
|
||||
"vue-router": "^3.2.0",
|
||||
"vuetify": "^2.4.0",
|
||||
"vuex": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^2.33.0",
|
||||
"@typescript-eslint/parser": "^2.33.0",
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-plugin-router": "~4.5.0",
|
||||
"@vue/cli-plugin-typescript": "~4.5.0",
|
||||
"@vue/cli-plugin-vuex": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/eslint-config-standard": "^5.1.2",
|
||||
"@vue/eslint-config-typescript": "^5.0.2",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"less": "^3.0.4",
|
||||
"less-loader": "^5.0.0",
|
||||
"sass": "^1.32.0",
|
||||
"sass-loader": "^10.0.0",
|
||||
"typescript": "~3.9.3",
|
||||
"vue-cli-plugin-vuetify": "^2.2.2",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vuetify-loader": "^1.7.0"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 3.6 KiB |
File diff suppressed because one or more lines are too long
@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<!-- <link rel="icon" href="<%= BASE_URL %>favicon.ico">-->
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
|
||||
Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<v-app @mousewheel.native.prevent="">
|
||||
<v-app-bar
|
||||
app
|
||||
color="primary"
|
||||
dark
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<one-icon style="color: aqua;font-size: 56px">glassdoor</one-icon>
|
||||
<span class="font-italic font-weight-bold" style="font-size: 20px">统一认证</span>
|
||||
</div>
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
|
||||
<v-main>
|
||||
<router-view></router-view>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import util from '@/libs/util'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'App',
|
||||
|
||||
components: {},
|
||||
|
||||
data: () => ({
|
||||
//
|
||||
}),
|
||||
|
||||
beforeCreate() {
|
||||
util.title('统一认证')
|
||||
this.$store.dispatch('fetchSelf')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@import './assets/common';
|
||||
|
||||
html,
|
||||
body {
|
||||
.full_size;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#app {
|
||||
.full_size;
|
||||
//.none_select;
|
||||
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none; /* Chrome Safari */
|
||||
}
|
||||
</style>
|
@ -0,0 +1,65 @@
|
||||
import axios from 'axios'
|
||||
import store from '@/store'
|
||||
|
||||
function baseRequests(url: string, method: any = 'GET', query: any, data: any, success: any, fail?: Function) {
|
||||
return axios({
|
||||
url: url,
|
||||
params: query,
|
||||
data: data,
|
||||
method: method,
|
||||
headers: {
|
||||
auth_token: localStorage.auth_token
|
||||
}
|
||||
})
|
||||
.then((res: any) => {
|
||||
if ('auth_token' in res.headers) {
|
||||
localStorage.auth_token = res.headers.auth_token
|
||||
}
|
||||
if (method === 'HEAD') {
|
||||
success(res.headers)
|
||||
} else {
|
||||
success(res.data)
|
||||
}
|
||||
})
|
||||
.catch((e: any) => {
|
||||
if (e.response && e.response.status === 401) {
|
||||
console.log(e)
|
||||
store.dispatch('handleLogout')
|
||||
return
|
||||
}
|
||||
console.log(e)
|
||||
if (e.response && e.response.status === 500) {
|
||||
return
|
||||
}
|
||||
if (typeof fail === 'function') {
|
||||
fail(e.response)
|
||||
} else if (e.response && e.response.status === 400) {
|
||||
console.log(400)
|
||||
} else {
|
||||
console.log(e.request)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const ajax = {
|
||||
get(url: '', data = {}, success = {}, fail?: Function) {
|
||||
return baseRequests(url, 'GET', data, {}, success, fail)
|
||||
},
|
||||
head(url: '', data = {}, success = {}, fail?: Function) {
|
||||
return baseRequests(url, 'HEAD', data, {}, success, fail)
|
||||
},
|
||||
delete(url: '', data = {}, success = {}, fail?: Function) {
|
||||
return baseRequests(url, 'DELETE', data, {}, success, fail)
|
||||
},
|
||||
post(url: '', data = {}, success = {}, fail?: Function) {
|
||||
return baseRequests(url, 'POST', {}, data, success, fail)
|
||||
},
|
||||
put(url: '', data = {}, success = {}, fail?: Function) {
|
||||
return baseRequests(url, 'PUT', {}, data, success, fail)
|
||||
},
|
||||
patch(url: '', data = {}, success = {}, fail?: Function) {
|
||||
return baseRequests(url, 'PATCH', {}, data, success, fail)
|
||||
}
|
||||
}
|
||||
|
||||
export default ajax
|
@ -0,0 +1,216 @@
|
||||
/*
|
||||
* Copyright (C) 2019 light <light@light-laptop>
|
||||
*
|
||||
* Distributed under terms of the MIT license.
|
||||
*/
|
||||
|
||||
import Vue from 'vue'
|
||||
import {Base64} from 'js-base64'
|
||||
import ajax from './ajax'
|
||||
import store from '@/store'
|
||||
|
||||
export type SuccessFunction<T> = (e: any) => void;
|
||||
export type FailedFunction<T> = (e: any) => void;
|
||||
|
||||
const Code = {
|
||||
42011: '无操作权限',
|
||||
22031: '资源不存在 或 您无权操作该资源'
|
||||
}
|
||||
|
||||
class Interface {
|
||||
private readonly method: Function
|
||||
private readonly api: string
|
||||
private readonly data: any
|
||||
|
||||
constructor(method: Function, api: string, data?: any) {
|
||||
this.method = method
|
||||
this.api = api
|
||||
this.data = data
|
||||
}
|
||||
|
||||
Start(success: SuccessFunction<any>, fail?: FailedFunction<any>) {
|
||||
const newFail = function (data: any) {
|
||||
if (data && data.code === 40001) {
|
||||
// no login
|
||||
store.dispatch('handleLogout')
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// @ts-ignore
|
||||
if (data && data.code > 0 && Code[data.code]) {
|
||||
}
|
||||
if (fail) {
|
||||
fail(data)
|
||||
}
|
||||
}
|
||||
|
||||
const newSuccess = function (data: any) {
|
||||
if (Number(data.status) === 1) {
|
||||
if (success) {
|
||||
success(data.content)
|
||||
}
|
||||
} else {
|
||||
newFail(data)
|
||||
if (data.code === 41001) {
|
||||
store.dispatch('handleLogout')
|
||||
// bus.$emit('log_out')
|
||||
}
|
||||
}
|
||||
}
|
||||
this.method(this.api, this.data, newSuccess, newFail)
|
||||
}
|
||||
}
|
||||
|
||||
const message = {
|
||||
count() {
|
||||
return new Interface(ajax.get, '/api/message/', {
|
||||
count: true,
|
||||
status: 'UnRead'
|
||||
})
|
||||
},
|
||||
get_content(id: number) {
|
||||
return new Interface(ajax.get, '/api/message/' + Number(id))
|
||||
},
|
||||
list(status: string) {
|
||||
return new Interface(ajax.get, '/api/message/', {status})
|
||||
},
|
||||
update(id: number, status: string) {
|
||||
return new Interface(ajax.patch, '/api/message/' + Number(id), {status})
|
||||
}
|
||||
}
|
||||
|
||||
const role = {
|
||||
local: '/api/role/',
|
||||
get(id: number) {
|
||||
return new Interface(ajax.get, this.local + id)
|
||||
},
|
||||
list() {
|
||||
return new Interface(ajax.get, this.local)
|
||||
},
|
||||
update(id: number, props: any) {
|
||||
return new Interface(ajax.patch, this.local + id, props)
|
||||
},
|
||||
create(props: any) {
|
||||
return new Interface(ajax.post, this.local, props)
|
||||
},
|
||||
del(id: number) {
|
||||
return new Interface(ajax.delete, this.local + id)
|
||||
},
|
||||
bind(id: number, aid: number) {
|
||||
return new Interface(ajax.get, this.local + id + '/bind/' + aid)
|
||||
},
|
||||
unbind(id: number, aid: number) {
|
||||
return new Interface(ajax.get, this.local + id + '/unbind/' + aid)
|
||||
}
|
||||
}
|
||||
|
||||
const app = {
|
||||
local: '/api/app/',
|
||||
self() {
|
||||
return new Interface(ajax.get, this.local, {is_self: true})
|
||||
},
|
||||
get(id: string) {
|
||||
return new Interface(ajax.get, this.local + id)
|
||||
},
|
||||
list() {
|
||||
return new Interface(ajax.get, this.local)
|
||||
}
|
||||
}
|
||||
|
||||
const user = {
|
||||
local: '/api/user/',
|
||||
register(username: string, password: string, uuid: string, prop?: any) {
|
||||
const data = Object.assign({
|
||||
username: username,
|
||||
uuid: uuid,
|
||||
password: Base64.encode(password)
|
||||
}, prop)
|
||||
return new Interface(ajax.post, this.local, data)
|
||||
},
|
||||
login(username: string, password: string, uuid: string) {
|
||||
return new Interface(ajax.head, this.local + username, {
|
||||
uid_type: 'username',
|
||||
uuid: uuid,
|
||||
password: Base64.encode(password)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const api = {
|
||||
role: role,
|
||||
app: app,
|
||||
user: user,
|
||||
admin: {
|
||||
auths() {
|
||||
return new Interface(ajax.get, '/api/auth/')
|
||||
},
|
||||
user: {
|
||||
create(props: any) {
|
||||
const p = Object.assign({}, props)
|
||||
p.password = Base64.encode(props.password)
|
||||
return new Interface(ajax.post, '/api/user/', p)
|
||||
},
|
||||
update(user_id: number, props: any) {
|
||||
return new Interface(ajax.patch, '/api/user/' + user_id, props)
|
||||
},
|
||||
enable(user_id: number) {
|
||||
return new Interface(ajax.patch, '/api/user/' + user_id, {
|
||||
status: 'ok'
|
||||
})
|
||||
},
|
||||
disable(user_id: number) {
|
||||
return new Interface(ajax.patch, '/api/user/' + user_id, {
|
||||
status: 'disabled'
|
||||
})
|
||||
},
|
||||
attach_role(user_id: number, props: any) {
|
||||
return new Interface(ajax.post, '/api/user/' + user_id + '/role/', props)
|
||||
},
|
||||
detach_role(user_id: number, id: any) {
|
||||
return new Interface(ajax.delete, '/api/user/' + user_id + '/role/' + id)
|
||||
},
|
||||
reset_pass(user_id: number, password: string) {
|
||||
return new Interface(ajax.patch, '/api/user/' + user_id, {password})
|
||||
}
|
||||
}
|
||||
},
|
||||
auth: {
|
||||
event() {
|
||||
return {
|
||||
local: '/api/user/event/',
|
||||
list() {
|
||||
return new Interface(ajax.get, this.local)
|
||||
},
|
||||
create(title: string, tag: string, start_date: any, end_date: any) {
|
||||
return new Interface(ajax.post, this.local, {title, tag, start_date, end_date})
|
||||
},
|
||||
del(id: number) {
|
||||
return new Interface(ajax.delete, this.local + id)
|
||||
}
|
||||
}
|
||||
},
|
||||
favorite(name: string, tag: string, ok: boolean) {
|
||||
if (ok) {
|
||||
return new Interface(ajax.post, '/api/user/favorite', {name, tag})
|
||||
}
|
||||
return new Interface(ajax.delete, '/api/user/favorite', {name, tag})
|
||||
},
|
||||
get(id: number) {
|
||||
return new Interface(ajax.get, '/api/user/' + id)
|
||||
},
|
||||
search(username: string) {
|
||||
return new Interface(ajax.get, '/api/user/', {
|
||||
username
|
||||
})
|
||||
}
|
||||
},
|
||||
message: message
|
||||
}
|
||||
|
||||
const Api = {
|
||||
install(vue: typeof Vue): void {
|
||||
vue.prototype.$api = api
|
||||
}
|
||||
}
|
||||
export {Api}
|
||||
export default api
|
@ -0,0 +1,25 @@
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.full_size {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.none_select {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.none_border{
|
||||
border: none;
|
||||
}
|
||||
.none_border_input {
|
||||
.ivu-input {
|
||||
.none_border;
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.5 100"><defs><style>.cls-1{fill:#1697f6;}.cls-2{fill:#7bc6ff;}.cls-3{fill:#1867c0;}.cls-4{fill:#aeddff;}</style></defs><title>Artboard 46</title><polyline class="cls-1" points="43.75 0 23.31 0 43.75 48.32"/><polygon class="cls-2" points="43.75 62.5 43.75 100 0 14.58 22.92 14.58 43.75 62.5"/><polyline class="cls-3" points="43.75 0 64.19 0 43.75 48.32"/><polygon class="cls-4" points="64.58 14.58 87.5 14.58 43.75 100 43.75 62.5 64.58 14.58"/></svg>
|
After Width: | Height: | Size: 539 B |
@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div id="wx_reg"></div>
|
||||
</template>
|
||||
<script lang='ts'>
|
||||
import {Component, Vue, Prop} from 'vue-property-decorator'
|
||||
import '@/libs/wwLogin.js'
|
||||
|
||||
@Component({
|
||||
components: {}
|
||||
})
|
||||
export default class WxLogin extends Vue {
|
||||
goto(id: string, app: string, url: string, state?: number, href?: string) {
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
window.WwLogin({
|
||||
id: 'wx_reg',
|
||||
appid: id,
|
||||
agentid: app,
|
||||
redirect_uri: encodeURIComponent(url),
|
||||
state: state,
|
||||
href: href
|
||||
})
|
||||
}
|
||||
|
||||
@Prop({default: ''})
|
||||
aid = ''
|
||||
|
||||
@Prop({default: ''})
|
||||
app = ''
|
||||
|
||||
@Prop({default: ''})
|
||||
url = ''
|
||||
|
||||
mounted() {
|
||||
this.goto(this.aid, this.app, this.url, new Date().getTime())
|
||||
}
|
||||
|
||||
created() {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<v-card class="core" elevation="4">
|
||||
<v-system-bar color="info">
|
||||
<one-icon>mail</one-icon>
|
||||
<v-spacer></v-spacer>
|
||||
<one-icon>user-group</one-icon>
|
||||
<span>*{{ core.user_count }}</span>
|
||||
</v-system-bar>
|
||||
<div></div>
|
||||
</v-card>
|
||||
</template>
|
||||
<script lang='ts'>
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator'
|
||||
|
||||
@Component({
|
||||
components: {}
|
||||
})
|
||||
export default class AppCard extends Vue {
|
||||
@Prop({default: {}})
|
||||
core: any
|
||||
|
||||
mounted() {
|
||||
console.log(this.core)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.core {
|
||||
width: 256px;
|
||||
background: #2c3e50;
|
||||
height: 128px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
<script lang='ts'>
|
||||
import {Component, Vue} from 'vue-property-decorator'
|
||||
|
||||
@Component({
|
||||
components: {}
|
||||
})
|
||||
export default class Demo extends Vue {
|
||||
mounted() {
|
||||
}
|
||||
|
||||
created() {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -0,0 +1,64 @@
|
||||
function padLeftZero(str: string): string {
|
||||
return ('00' + str).substr(str.length)
|
||||
}
|
||||
|
||||
const util = {
|
||||
title: function (title: string) {
|
||||
window.document.title = title ? title + ' - oa' : 'veypi project'
|
||||
},
|
||||
getCookie(name: string) {
|
||||
const reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)')
|
||||
const arr = document.cookie.match(reg)
|
||||
if (arr) {
|
||||
return unescape(arr[2])
|
||||
} else return null
|
||||
},
|
||||
delCookie(name: string) {
|
||||
const exp = new Date()
|
||||
exp.setTime(exp.getTime() - 1)
|
||||
const cval = this.getCookie(name)
|
||||
if (cval !== null) {
|
||||
document.cookie = name + '=' + cval + ';expires=' + exp.toLocaleString()
|
||||
}
|
||||
},
|
||||
setCookie(name: string, value: string, time: number) {
|
||||
const exp = new Date()
|
||||
exp.setTime(exp.getTime() + time)
|
||||
document.cookie =
|
||||
name + '=' + escape(value) + ';expires=' + exp.toLocaleString()
|
||||
},
|
||||
checkLogin() {
|
||||
// return parseInt(this.getCookie('stat')) === 1
|
||||
return Boolean(localStorage.auth_token)
|
||||
},
|
||||
|
||||
formatDate(date: Date, fmt: string) {
|
||||
if (/(y+)/.test(fmt)) {
|
||||
fmt = fmt.replace(
|
||||
RegExp.$1,
|
||||
(date.getFullYear() + '').substr(4 - RegExp.$1.length)
|
||||
)
|
||||
}
|
||||
const o = {
|
||||
'M+': date.getMonth() + 1,
|
||||
'd+': date.getDate(),
|
||||
'h+': date.getHours(),
|
||||
'm+': date.getMinutes(),
|
||||
's+': date.getSeconds()
|
||||
}
|
||||
for (const k in o) {
|
||||
if (new RegExp(`(${k})`).test(fmt)) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// @ts-ignore
|
||||
const str = o[k] + ''
|
||||
fmt = fmt.replace(
|
||||
RegExp.$1,
|
||||
RegExp.$1.length === 1 ? str : padLeftZero(str)
|
||||
)
|
||||
}
|
||||
}
|
||||
return fmt
|
||||
}
|
||||
}
|
||||
|
||||
export default util
|
@ -0,0 +1,2 @@
|
||||
!function(a,b,c){function d(c){var d=b.createElement("iframe"),e="https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid="+c.appid+"&agentid="+c.agentid+"&redirect_uri="+c.redirect_uri+"&state="+c.state+"&login_type=jssdk";e+=c.style?"&style="+c.style:"",e+=c.href?"&href="+c.href:"",d.src=e,d.frameBorder="0",d.allowTransparency="true",d.scrolling="no",d.width="300px",d.height="400px";var f=b.getElementById(c.id);f.innerHTML="",f.appendChild(d),d.onload=function(){d.contentWindow.postMessage&&a.addEventListener&&(a.addEventListener("message",function(b){
|
||||
b.data&&b.origin.indexOf("work.weixin.qq.com")>-1&&(a.location.href=b.data)}),d.contentWindow.postMessage("ask_usePostMessage","*"))}}a.WwLogin=d}(window,document);
|
@ -0,0 +1,24 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import vuetify from './plugins/vuetify'
|
||||
import {Api} from '@/api'
|
||||
import OneIcon from '@veypi/one-icon'
|
||||
import Message from 'vue-m-message'
|
||||
import 'vue-m-message/dist/index.css'
|
||||
|
||||
Vue.use(Message) // will mount `Vue.prototype.$message`
|
||||
|
||||
// Vue.use(OneIcon, {href: 'https://at.alicdn.com/t/font_2872366_7aws02sx9bl.js'})
|
||||
Vue.use(OneIcon, {href: './icon.js'})
|
||||
Vue.use(Api)
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
vuetify,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
@ -0,0 +1,24 @@
|
||||
import Vue from 'vue'
|
||||
import Vuetify from 'vuetify/lib/framework'
|
||||
|
||||
Vue.use(Vuetify)
|
||||
|
||||
const light = {
|
||||
primary: '#2196f3',
|
||||
secondary: '#00bcd4',
|
||||
accent: '#3f51b5',
|
||||
error: '#f44336',
|
||||
warning: '#ff5722',
|
||||
info: '#ff9800',
|
||||
success: '#4caf50',
|
||||
reset: '#684bff'
|
||||
}
|
||||
|
||||
export default new Vuetify({
|
||||
theme: {
|
||||
dark: false,
|
||||
themes: {
|
||||
light: light
|
||||
}
|
||||
}
|
||||
})
|
@ -0,0 +1,57 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter, {RouteConfig} from 'vue-router'
|
||||
import Home from '../views/Home.vue'
|
||||
import Demo from '@/views/demo.vue'
|
||||
import Login from '@/views/login.vue'
|
||||
import Register from '@/views/register.vue'
|
||||
import NotFound from '@/views/404.vue'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
// 避免push到相同路径报错
|
||||
// 获取原型对象上的push函数
|
||||
const originalPush = VueRouter.prototype.push
|
||||
// 修改原型对象中的push方法
|
||||
VueRouter.prototype.push = function push(location: any) {
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
return originalPush.call(this, location).catch(err => err)
|
||||
}
|
||||
|
||||
const routes: Array<RouteConfig> = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: '/app',
|
||||
name: 'app',
|
||||
component: Demo
|
||||
},
|
||||
{
|
||||
path: '/login/:uuid?',
|
||||
name: 'login',
|
||||
component: Login
|
||||
},
|
||||
{
|
||||
path: '/register/:uuid?',
|
||||
name: 'register',
|
||||
component: Register
|
||||
},
|
||||
{
|
||||
path: '/wx',
|
||||
name: 'wx',
|
||||
component: () => import('../views/wx.vue')
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
name: '404',
|
||||
component: NotFound
|
||||
}
|
||||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
@ -0,0 +1,13 @@
|
||||
import Vue, { VNode } from 'vue'
|
||||
|
||||
declare global {
|
||||
namespace JSX {
|
||||
// tslint:disable no-empty-interface
|
||||
interface Element extends VNode {}
|
||||
// tslint:disable no-empty-interface
|
||||
interface ElementClass extends Vue {}
|
||||
interface IntrinsicElements {
|
||||
[elem: string]: any;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
declare module '*.js'
|
||||
declare module '*.vue' {
|
||||
import Vue from 'vue'
|
||||
export default Vue
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
declare module 'vuetify/lib/framework' {
|
||||
import Vuetify from 'vuetify'
|
||||
export default Vuetify
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import api from '@/api'
|
||||
import router from '@/router'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
oauuid: '',
|
||||
user: null
|
||||
},
|
||||
mutations: {
|
||||
setOA(state: any, data: any) {
|
||||
state.oauuid = data.uuid
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
fetchSelf({commit}) {
|
||||
api.app.self().Start(d => {
|
||||
commit('setOA', d)
|
||||
})
|
||||
},
|
||||
handleLogout() {
|
||||
localStorage.removeItem('auth_token')
|
||||
router.push({name: 'login'})
|
||||
}
|
||||
},
|
||||
modules: {}
|
||||
})
|
@ -0,0 +1,14 @@
|
||||
// 1. 确保在声明补充的类型之前导入 'vue'
|
||||
import Vue from 'vue'
|
||||
import api from '@/api'
|
||||
|
||||
export type PluginFunction<T> = (Vue: typeof Vue, options?: T) => void;
|
||||
|
||||
// 2. 定制一个文件,设置你想要补充的类型
|
||||
// 在 types/vue.d.ts 里 Vue 有构造函数类型
|
||||
declare module 'vue/types/vue' {
|
||||
// 3. 声明为 Vue 补充的东西
|
||||
interface Vue {
|
||||
$api: typeof api;
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
<style>
|
||||
</style>
|
||||
<template>
|
||||
<div class='home d-flex justify-center align-center'>
|
||||
<one-icon style="font-size: 100px">404</one-icon>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang='ts'>
|
||||
import {Component, Vue} from 'vue-property-decorator'
|
||||
import util from '@/libs/util'
|
||||
|
||||
@Component({
|
||||
components: {}
|
||||
})
|
||||
export default class NotFound extends Vue {
|
||||
mounted() {
|
||||
}
|
||||
|
||||
created() {
|
||||
util.title('404')
|
||||
}
|
||||
}
|
||||
</script>
|
@ -0,0 +1,50 @@
|
||||
<style>
|
||||
.home {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class='full_size'>
|
||||
<v-row no-gutters class="pa-8">
|
||||
<v-col v-for="(item, key) in apps" :key="key" class="mx-4 my-2">
|
||||
<AppCard :core="item"></AppCard>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang='ts'>
|
||||
import {Component, Vue} from 'vue-property-decorator'
|
||||
import util from '@/libs/util'
|
||||
import AppCard from '@/components/app.vue'
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
AppCard
|
||||
}
|
||||
})
|
||||
export default class Home extends Vue {
|
||||
apps = []
|
||||
|
||||
getApps() {
|
||||
this.$api.app.list().Start(d => {
|
||||
console.log(d)
|
||||
this.apps = d
|
||||
})
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.getApps()
|
||||
}
|
||||
|
||||
created() {
|
||||
}
|
||||
|
||||
beforeCreate() {
|
||||
if (!util.checkLogin()) {
|
||||
this.$router.push({name: 'login', query: this.$route.query, params: this.$route.params})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -0,0 +1,21 @@
|
||||
<style>
|
||||
</style>
|
||||
<template>
|
||||
<div class='home d-flex justify-center align-center'>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang='ts'>
|
||||
import {Component, Vue} from 'vue-property-decorator'
|
||||
|
||||
@Component({
|
||||
components: {}
|
||||
})
|
||||
export default class Demo extends Vue {
|
||||
mounted() {
|
||||
}
|
||||
|
||||
created() {
|
||||
}
|
||||
}
|
||||
</script>
|
@ -0,0 +1,124 @@
|
||||
<style>
|
||||
</style>
|
||||
<template>
|
||||
<v-row class="fill-height" align="center" justify="center" style="background: #ebebeb">
|
||||
<v-col cols="12" sm="8" md="6" lg="4" xl="3">
|
||||
<v-card class="elevation-12 mx-5" style="opacity: 0.8">
|
||||
<v-row justify="center">
|
||||
<v-card class="elevation-1 mt-n7 primary" style="width: 80%">
|
||||
<v-card-actions>
|
||||
<v-row>
|
||||
<v-icon
|
||||
style="position: absolute;left: 10px;top:19px;z-index: 1"
|
||||
@click="$router.back()"
|
||||
size="36"
|
||||
>mdi-arrow-left-circle
|
||||
</v-icon>
|
||||
<v-col cols="12" class="text-center">
|
||||
<h1 class="display-2 ">注册</h1>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-row>
|
||||
<v-card-text class="text-center">
|
||||
<v-form ref="form">
|
||||
<v-text-field
|
||||
type="text"
|
||||
prepend-inner-icon="mdi-account-circle"
|
||||
v-model="form.username"
|
||||
label="账号"
|
||||
:rules="ruleInline.user"
|
||||
:counter="16"
|
||||
>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
type="password"
|
||||
v-model="form.passwd"
|
||||
label="密码"
|
||||
prepend-inner-icon="mdi-lock"
|
||||
:rules="ruleInline.password"
|
||||
:counter="16"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
type="password"
|
||||
v-model="form.passwdCheck"
|
||||
label="密码"
|
||||
prepend-inner-icon="mdi-lock"
|
||||
:rules="ruleInline.passwordCheck"
|
||||
:counter="16"
|
||||
@keyup.enter="handleSubmit"
|
||||
></v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn type="primary" @click="handleSubmit">提交</v-btn>
|
||||
<v-btn @click="handleReset()">重置</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script lang='ts'>
|
||||
import {Component, Vue} from 'vue-property-decorator'
|
||||
|
||||
@Component({
|
||||
components: {}
|
||||
})
|
||||
export default class Register extends Vue {
|
||||
form = {
|
||||
passwd: '',
|
||||
passwdCheck: '',
|
||||
email: '',
|
||||
username: ''
|
||||
}
|
||||
|
||||
ruleInline = {
|
||||
user: [
|
||||
(v: string) => !!v || 'required',
|
||||
(v: string) => (v && v.length >= 3 && v.length <= 16) || '长度要求3~16'
|
||||
],
|
||||
password: [
|
||||
(v: string) => !!v || 'required',
|
||||
(v: string) => (v && v.length >= 6 && v.length <= 16) || '长度要求6~16'
|
||||
],
|
||||
passwordCheck: [
|
||||
(v: string) => !!v || 'required',
|
||||
(v: string) => (v && v === this.form.passwd) || '密码不一致'
|
||||
]
|
||||
}
|
||||
|
||||
get app_uuid() {
|
||||
return this.$route.params.uuid || this.$store.state.oauuid
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
if (!this.$refs.form.validate()) {
|
||||
return
|
||||
}
|
||||
this.$api.user.register(this.form.username, this.form.passwd, this.app_uuid).Start(
|
||||
() => {
|
||||
this.$message.success('注册成功!')
|
||||
this.$router.push({name: 'login', params: this.$route.params, query: this.$route.query})
|
||||
},
|
||||
(data) => {
|
||||
if (data && data.code === '31011') {
|
||||
this.$message.error('用户名重复')
|
||||
} else {
|
||||
this.$message.error('注册失败')
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
handleReset() {
|
||||
this.form.username = ''
|
||||
this.form.passwd = ''
|
||||
this.form.passwdCheck = ''
|
||||
}
|
||||
}
|
||||
</script>
|
@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class='home d-flex justify-center align-center'>
|
||||
<wx-login v-if="enable" :aid="aid" :app="agentID" :url="url"></wx-login>
|
||||
<v-overlay :value="!enable">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
size="64"
|
||||
></v-progress-circular>
|
||||
</v-overlay>
|
||||
</div>
|
||||
</template>
|
||||
<script lang='ts'>
|
||||
import {Component, Vue} from 'vue-property-decorator'
|
||||
import WxLogin from '@/components/WxLogin.vue'
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
WxLogin
|
||||
}
|
||||
})
|
||||
export default class Wx extends Vue {
|
||||
aid = ''
|
||||
agentID = ''
|
||||
url = ''
|
||||
|
||||
get enable() {
|
||||
return this.uuid && this.aid && this.agentID && this.url
|
||||
}
|
||||
|
||||
get uuid() {
|
||||
return this.$route.query.uuid
|
||||
}
|
||||
|
||||
get code() {
|
||||
return this.$route.query.code
|
||||
}
|
||||
|
||||
get state() {
|
||||
return this.$route.query.state
|
||||
}
|
||||
|
||||
get msg() {
|
||||
return this.$route.query.msg
|
||||
}
|
||||
|
||||
mounted() {
|
||||
if (this.msg) {
|
||||
console.log(this.msg)
|
||||
alert(this.msg)
|
||||
}
|
||||
}
|
||||
|
||||
created() {
|
||||
if (this.uuid) {
|
||||
this.$api.app.get(this.uuid as string).Start(e => {
|
||||
this.url = e.wx.url + '/api/wx/login/' + this.uuid
|
||||
this.aid = e.wx.corp_id
|
||||
this.agentID = e.wx.agent_id
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -0,0 +1,40 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"types": [
|
||||
"webpack-env"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"tests/**/*.ts",
|
||||
"tests/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
module.exports = {
|
||||
transpileDependencies: [
|
||||
'vuetify'
|
||||
],
|
||||
configureWebpack: {
|
||||
output: {
|
||||
filename: '[name].[hash].js'
|
||||
}
|
||||
},
|
||||
outputDir: '../sub/static',
|
||||
devServer: {
|
||||
host: '0.0.0.0',
|
||||
port: 19520,
|
||||
disableHostCheck: true,
|
||||
proxy: {
|
||||
'^/api': {
|
||||
target: 'http://127.0.0.1:4001',
|
||||
ws: true,
|
||||
changeOrigin: true
|
||||
},
|
||||
'^/media': {
|
||||
target: 'http://127.0.0.1:4001',
|
||||
ws: true,
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,17 +1,96 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite App</title>
|
||||
<script type="module" crossorigin src="/static/index.dddecd43.js"></script>
|
||||
<link rel="modulepreload" href="/static/vendor.ba3bd51d.js">
|
||||
<link rel="stylesheet" href="/static/vendor.3a295b6b.css">
|
||||
<link rel="stylesheet" href="/static/index.c49db26f.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<link rel="icon" href="/favicon.ico"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Vite App</title>
|
||||
<script type="module" crossorigin src="/static/index.8f7b8116.js"></script>
|
||||
<link rel="modulepreload" href="/static/vendor.29407274.js">
|
||||
<link rel="stylesheet" href="/static/vendor.7d59d594.css">
|
||||
<link rel="stylesheet" href="/static/index.22e5e80b.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="loader-wrapper">
|
||||
<div class="lds-ripple">
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="load_title">Loading...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
<style>
|
||||
#loader-wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 999999;
|
||||
background: #999;
|
||||
}
|
||||
|
||||
#loader-wrapper .load_title {
|
||||
font-family: 'Open Sans';
|
||||
color: #FFF;
|
||||
font-size: 19px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
z-index: 9999999999999;
|
||||
position: absolute;
|
||||
top: 70%;
|
||||
opacity: 1;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
#loader-wrapper .load_title span {
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
font-size: 13px;
|
||||
color: #FFF;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.lds-ripple {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
left: calc(50% - 40px);
|
||||
top: calc(70% - 100px);
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.lds-ripple div {
|
||||
position: absolute;
|
||||
border: 4px solid #fff;
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
}
|
||||
|
||||
.lds-ripple div:nth-child(2) {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
|
||||
@keyframes lds-ripple {
|
||||
0% {
|
||||
top: 36px;
|
||||
left: 36px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</html>
|
||||
|
Loading…
Reference in New Issue