crud cfgpage

master
veypi 1 year ago
parent 8a0b44fb96
commit 2563324bb5

@ -41,7 +41,8 @@ CREATE TABLE IF NOT EXISTS `app`
`join_method` int NOT NULL DEFAULT 0, `join_method` int NOT NULL DEFAULT 0,
`role_id` varchar(32), `role_id` varchar(32),
`redirect` varchar(255), `host` varchar(255) NOT NULL DEFAULT '',
`redirect` varchar(255) NOT NULL DEFAULT '',
`status` int NOT NULL COMMENT '状态0ok1disabled' DEFAULT 0, `status` int NOT NULL COMMENT '状态0ok1disabled' DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE

@ -14,12 +14,10 @@ use crate::{
}; };
use actix_web::{delete, get, head, http, post, web, HttpResponse, Responder}; use actix_web::{delete, get, head, http, post, web, HttpResponse, Responder};
use base64; use base64;
use chrono::{DateTime, Local, NaiveDateTime};
use proc::access_read; use proc::access_read;
use rand::Rng; use rand::Rng;
use sea_orm::{ use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, TransactionTrait};
ActiveModelTrait, ColumnTrait,
EntityTrait, QueryFilter, TransactionTrait,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::info; use tracing::info;
@ -148,7 +146,6 @@ pub async fn register(
Ok(p) => p, Ok(p) => p,
Err(_) => return Err(Error::ArgInvalid("password".to_string())), Err(_) => return Err(Error::ArgInvalid("password".to_string())),
}; };
info!("{}", p);
let mut u = models::user::Model::default(); let mut u = models::user::Model::default();
u.username = q.username.clone(); u.username = q.username.clone();
u.id = uuid::Uuid::new_v4().to_string().replace("-", ""); u.id = uuid::Uuid::new_v4().to_string().replace("-", "");
@ -158,6 +155,10 @@ pub async fn register(
u.icon = Some(format!("/media/icon/usr/{:04}.jpg", idx)); u.icon = Some(format!("/media/icon/usr/{:04}.jpg", idx));
u.space = 300; u.space = 300;
u.used = 0; u.used = 0;
info!("{}", u.created.to_string());
u.created = Local::now().naive_utc();
u.updated = Local::now().naive_utc();
info!("{}", u.created.to_string());
u.into() u.into()
} }
}; };

@ -15,6 +15,7 @@
"dependencies": { "dependencies": {
"@quasar/extras": "^1.16.4", "@quasar/extras": "^1.16.4",
"@toast-ui/editor": "^3.2.2", "@toast-ui/editor": "^3.2.2",
"@types/validator": "^13.11.2",
"@veypi/msg": "^0.1.1", "@veypi/msg": "^0.1.1",
"@veypi/oaer": "^0.0.1", "@veypi/oaer": "^0.0.1",
"@veypi/one-icon": "2", "@veypi/one-icon": "2",
@ -26,6 +27,7 @@
"mitt": "^3.0.1", "mitt": "^3.0.1",
"pinia": "^2.0.11", "pinia": "^2.0.11",
"quasar": "^2.6.0", "quasar": "^2.6.0",
"validator": "^13.11.0",
"vite-plugin-rewrite-all": "^1.0.1", "vite-plugin-rewrite-all": "^1.0.1",
"vue": "^3.0.0", "vue": "^3.0.0",
"vue-i18n": "^9.2.2", "vue-i18n": "^9.2.2",

@ -41,12 +41,4 @@ body,
:root { :root {
--z-index: 1; --z-index: 1;
} }
.page-h1 {
font-size: 2.5rem;
line-height: 2.5rem;
margin-left: 2.5rem;
margin-top: 1.5rem;
margin-bottom: 2rem;
}
</style> </style>

File diff suppressed because one or more lines are too long

@ -6,21 +6,35 @@
--> -->
<template> <template>
<div> <div>
<div class="v-crud" :vertical='modeV'>
<q-page-sticky position="top-right" style="z-index: 20" :offset="[27, 27]"> <div class="v-crud-keys">
<q-btn @click="modeV = !modeV" round icon="save_as" class="" /> <template v-for="k of keys" :key="k.name">
</q-page-sticky> <div class="v-crud-cell" :style="Object.assign({}, cstyle[k.name], cstyle.k)">
<div class="grid" :class="[modeV ? '' : 'grid-cols-2']"> {{ k.label || k.name }}
<div class="grid" :style="modeV ? grid_len : ''">
<div :class="styles.k" v-for="k of keys" :key="k.name">
{{ k.label || k.name }}
</div>
</div>
<div class="grid" :style="modeV ? '' : grid_len">
<div class="grid hover:bg-gray-200" :style="modeV ? grid_len : ''" v-for="( item, idx ) in data " :key="idx">
<div :class="styles.v" v-for="k of keys" :key="k.name">
{{ item[k.name] }}
</div> </div>
</template>
</div>
<div class="v-crud-values">
<div class="v-crud-line rounded-3xl" :class="cstyle.line" v-for=" (item, idx) in items" :key="idx">
<template v-for="k of keys" :key="k.name">
<div class="v-crud-cell" :changed="item.__changed[k.name]" :selected="selected
=== `${item.__idx}.${k.name}`" @click="ifselect ?
selected = `${item.__idx}.${k.name}` : ''" :style="Object.assign({},
cstyle[k.name], cstyle.v)">
<slot :name="`k_${k.name}`" :row="item" :value="item[k.name]" :set="setv(item, k.name)">
<template v-if="k.editable === undefined ? editable :
k.editable">
<vinput :model-value="item[k.name] === undefined ?
k.default : item[k.name]" :type="k.typ" :options="k.options" @update:model-value="setv(item,
k.name)($event)"></vinput>
</template>
<template v-else>
{{ item[k.name] === undefined ? k.default :
item[k.name] }}
</template>
</slot>
</div>
</template>
</div> </div>
</div> </div>
</div> </div>
@ -28,63 +42,193 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, watch } from 'vue'; import { computed, getCurrentInstance, onMounted, reactive, ref, watch } from 'vue';
import vinput from 'src/components/vinput'
import { ArgType, Dict } from 'src/models';
interface itemProp {
name: '',
label?: '',
value: any,
type?: '',
}
interface keyProp { interface keyProp {
name: string, name: string,
label?: string, label?: string,
default?: any, default?: any,
typ?: string, width?: number,
typ?: ArgType,
editable?: boolean,
options?: any,
} }
let emits = defineEmits<{
const grid_len = computed(() => { (e: 'update', v: any): void
return { }>()
'grid-template-columns': 'repeat(' +
(modeV.value ? props.keys?.length : props.data?.length)
+ ', minmax(0, 1fr))'
}
})
let props = withDefaults(defineProps<{ let props = withDefaults(defineProps<{
vertical?: boolean vertical?: boolean
keys?: keyProp[], keys?: keyProp[],
data?: any[] data?: any[]
kclass?: Array<string>, hover?: boolean,
vclass?: Array<string>, kstyle?: { [k: string]: string },
cclass?: Array<string>, vstyle?: { [k: string]: string },
cstyle?: { [k: string]: string },
kalign?: 'center' | 'left' | 'right',
valign?: 'center' | 'left' | 'right',
editable?: boolean
ifselect?: boolean
}>(), }>(),
{ {
vertical: false, vertical: false,
data: [] as any, data: [] as any,
kclass: [] as any, hover: false,
vclass: [] as any, kstyle: {} as any,
cclass: ['w-40', 'h-40'] as any, vstyle: {} as any,
cstyle: {} as any,
kalign: 'center',
valign: 'center',
} }
) )
const modeV = ref(props.vertical)
watch(computed(() => props.vertical), (v) => modeV.value = v)
let items = ref<any[]>([])
// watch(computed(() => props.data), (v) => {
// syncItems()
// })
const syncItems = () => {
let res = props.data?.map((v: any, i: any) => {
return Object.assign({ __idx: i, __changed: {} }, v)
}) as any
items.value.splice(0, items.value.length)
items.value.push(...res)
// Object.assign(items, res)
}
const styles = computed(() => { const selected = ref()
let k = [];
let v = []; let alignDic = { 'center': 'center', 'left': 'start', 'right': 'end' }
k.push(...props.kclass, ...props.cclass) const cstyle = computed(() => {
v.push(...props.vclass, ...props.cclass) let res = { line: [] } as any
return { let l = props.keys?.length || 0
k, v let w = 100
let style = modeV.value ? 'flex-basis' : 'height'
props.keys?.forEach((k, i) => {
if (k.width && k.width > 0 && k.width < 100) {
res[k.name] = { [style]: (k.width || 1) + '%' }
w = w - k.width
l = l - 1
}
})
props.keys?.forEach((k, i) => {
if (k.width && k.width > 0 && k.width < 100) {
} else {
res[k.name] = { [style]: w / l + '%' }
}
})
res.k = Object.assign({ 'justify-content': alignDic[props.kalign] }, props.cstyle, props.kstyle)
res.v = Object.assign({ 'justify-content': alignDic[props.valign] }, props.cstyle, props.vstyle)
if (props.hover) {
res.line.push('hover:bg-gray-200');
} }
return res
}) })
const modeV = ref(props.vertical)
watch(computed(() => props.vertical), (v) => modeV.value = v) const updatedItems = ref<Dict[]>([])
const setv = (item: any, k: string) => {
return (v: any) => {
item[k] = v
item.__changed[k] = true
if (!updatedItems.value[item.__idx]) {
updatedItems.value[item.__idx] = {}
}
updatedItems.value[item.__idx][k] = v
emits('update', updatedItems.value)
console.log(`update ${k} to ${v}`)
}
}
const save = () => {
}
const reload = () => {
console.log('reload')
syncItems()
}
onMounted(() => {
syncItems()
})
defineExpose({
reload
})
</script> </script>
<style scoped></style> <style lang="scss" scoped>
.v-crud {
display: flex;
}
.v-crud-keys {
display: flex;
}
.v-crud-values {
display: flex;
}
.v-crud-line {
display: flex;
}
.v-crud-cell {
min-width: 8rem;
min-height: 4rem;
display: flex;
justify-content: center;
align-items: center;
overflow: auto;
overflow-y: auto;
&[selected=true] {
background: rgba($color: #888, $alpha: .1);
}
&[changed=true] {
background: rgba($color: #888, $alpha: .1);
}
}
.v-crud[vertical=true] {
flex-direction: column;
.v-crud-keys {
flex-wrap: nowrap;
}
.v-crud-values {
flex-direction: column;
.v-crud-line {
flex-wrap: nowrap;
}
}
}
.v-crud[vertical=false] {
flex-wrap: nowrap;
.v-crud-keys {
flex-direction: column;
}
.v-crud-values {
flex-wrap: nowrap;
.v-crud-line {
flex-direction: column;
}
}
}
</style>

@ -0,0 +1,9 @@
/*
* @name: index
* @author: veypi <i@veypi.com>
* @date: 2022-04-03 14:56
* @descriptionindex
*/
import index from './index.vue'
export default index

@ -0,0 +1,422 @@
<template>
<div :vtype="type" :class="[hideBorder ? 'hide-hr' : '', flexy ? 'flex-col' : '', disabled ? 'cursor-not-allowed' : '']"
ref="all" class="vinput center flex justify-center items-center relative" :style="dy_style">
<div v-if="props.label" class="flex-shrink" :style="{ 'width': labelWidth }">
<slot name="label">{{ props.label }}</slot>
</div>
<div class="flex-grow vinput-body">
<template v-if="type === ArgType.Number">
<input :type="type" :disabled="disabled" @input="check()" :value="value" @focusout="update"
@focusin="change('input')" class="noborder w-full" :style="dy_style" style="font-weight: inherit" ref="inputRef"
@blur="update" @keyup.enter="unblur(); update();">
<hr>
</template>
<template v-else-if="type === ArgType.Text">
<input :type="type" :disabled="disabled" @input="check()" :value="value" @focusout="update"
@focusin="change('input');" class="noborder w-full" :style="dy_style" style="font-weight: inherit"
ref="inputRef" @blur="update" @keyup.enter="update">
<hr>
</template>
<template v-else-if="type === ArgType.Password">
<input :type="type" :disabled="disabled" @input="check()" :value="value" @focusout="update"
@focusin="change('input');" class="noborder w-full" :style="dy_style" style="font-weight: inherit"
ref="inputRef" @blur="update" @keyup.enter="update">
<hr>
</template>
<template v-else-if="type === ArgType.File">
<!-- <FormKit v-model="value" @change="update" class="noborder" type="file" outer-class="w-full" />-->
<!-- <FormKit class="noborder" type="file" outer-class="w-full" />-->
<div class="div-center rounded-md relative" style="">
<slot name='file'>
{{ value == "" ? "no file chosen" : value }}
</slot>
<input class="absolute w-full h-full" type="file" style="color:white;font-size: large;opacity: 0%;top:0;left:0"
@change="choose_file($event)" />
</div>
<!-- @click="setParameter(v.key,vv.key, c.value)"-->
</template>
<template v-else-if="type === ArgType.Radio">
<div class="flex justify-between gap-4">
<template :key="ov.key" v-for="ov of options">
<div :class="[value === ov.key ? 'radio-btn-active' :
'div-btn']" @click="setSelect(ov.key)" style="color:white;"
class="div-center font-bold grow truncate radio-btn rounded-md transition duration-500">
{{ ov.name || ov.key }}
</div>
</template>
</div>
</template>
<template v-else-if="type === ArgType.Select">
<div class="noborder cursor-pointer w-full overflow-x-auto whitespace-nowrap" @click="showSelect" :title="title">
<span v-if="!value"></span>
<span v-else-if="!Array.isArray(value)">{{ transDic[value] || value }}</span>
<template v-else>
<span class="mx-2" v-for=" iv in value " :key="iv">{{ transDic[iv] || iv }}</span>
</template>
</div>
<div @mouseleave="showSelectOpt = false"
:style="{ left: selectPos[0] + 'px', top: selectPos[1] + 'px', height: showSelectOpt ? '20rem' : '0rem' }"
class="select-opt text-base text-white rounded-md overflow-y-auto" style="min-width: 10rem;" :title="title">
<div class="m-2 p-2" v-if="!options"></div>
<div :class="[ok === value ? 'bg-gray-500' : 'bg-gray-800']"
class="cursor-pointer m-2 p-2 rounded-md hover:bg-gray-500" @click="setSelect(ok)"
v-for="( ov, ok ) in transDic " :key="ok">
{{ ov }}
</div>
<div class="w-full h-32"></div>
</div>
</template>
<template v-else-if="type === ArgType.Region">
<div class="flex items-center justify-center">
<template v-if="value[0] !== '∞'">
<OneIcon class="div-btn" @click="updateIndex(0, '∞')">kuohao</OneIcon>
<input type="number" :disabled="disabled" @input="check()" v-model="value[0]" @focusout="update"
@focusin="change('input')" class="noborder w-1/3 text-center" @blur="update" @keyup.enter="update">
</template>
<template v-else>
<OneIcon class="div-btn" @click="updateIndex(0, 0)">zuokuohao</OneIcon>
<div class="w-1/3 flex justify-center items-center">
<OneIcon>minus</OneIcon>
<OneIcon>infinite</OneIcon>
</div>
</template>
<div>,</div>
<template v-if="value[1] !== '∞'">
<input type="number" :disabled="disabled" v-model="value[1]" @focusout="update" @focusin="change('input')"
class="noborder w-1/3 text-center" @blur="update" @keyup.enter="update">
<OneIcon class="div-btn" @click="updateIndex(1, '∞')">kuohao-r</OneIcon>
</template>
<template v-else>
<div class="w-1/3 flex justify-center items-center">
<OneIcon>plus</OneIcon>
<OneIcon>infinite</OneIcon>
</div>
<OneIcon class="div-btn" @click="updateIndex(1, 1)">youkuohao</OneIcon>
</template>
</div>
</template>
<template v-else-if="type === ArgType.Bool">
<div class="rounded-full relative overflow-x-hidden transition duration-300 cursor-pointer text-white leading-8"
@click="value = !value; update()" style='height: 2rem;width: 6rem;'
:style="{ 'background': value ? '#1467ff' : '#555' }">
<template v-if="value">
<slot name="ok"></slot>
</template>
<template v-else>
<slot name="no"></slot>
</template>
<div class="bool-bg rounded-full m-1" style="background: #fff;height: 1.5rem;width: 1.5rem;"
:style="{ 'transform': 'translateX(' + (value ? '4' : '0') + 'rem)' }">
</div>
</div>
</template>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, watch, computed } from 'vue'
import { ArgType, Dict } from 'src/models'
import validator from 'validator';
import { OneIcon } from '@veypi/one-icon'
const props = withDefaults(defineProps<{
modelValue?: any
type?: ArgType,
options?: any,
disabled?: boolean
hideBorder?: boolean
tidy?: boolean
label?: string
labelWidth?: string
align?: string
flexy?: boolean
require?: boolean
validate?: any
//
title?: string
}>(), {
modelValue: '',
type: ArgType.Text,
disabled: false,
hideBorder: false,
tidy: false,
align: '',
flexy: false,
require: false,
labelWidth: '4rem'
})
const emit = defineEmits<{
(e: 'update:modelValue', data: any): void
(e: 'change', data: any): void
(e: 'upload', data: any): void
}>()
const dy_style = computed(() => `text-align:${props.align}`)
let inputRef = ref<HTMLInputElement>()
let all = ref<HTMLElement>()
const transDic = ref({} as Dict)
const change = (s: string) => {
if (props.disabled) {
return
}
if (s === 'idle') {
all.value?.classList.remove('vinput-active')
all.value?.classList.remove('vinput-error')
return
} else if (s === 'input') {
all.value?.classList.add('vinput-active')
} else if (s === 'error') {
all.value?.classList.add('vinput-error')
}
}
const value = ref(props.modelValue)
const sync = () => {
if (typeof props.modelValue === 'object') {
value.value = JSON.parse(JSON.stringify(props.modelValue))
} else {
value.value = props.modelValue
}
if (props.type === ArgType.Number) {
let v = parseFloat(props.modelValue) || 0
}
if (props.type === ArgType.Radio || props.type === ArgType.Select) {
transDic.value = {}
if (Array.isArray(props.options)) {
for (let i of props.options) {
if (typeof i === 'string') {
transDic.value[i] = i
} else {
transDic.value[i.key] = i.name
}
}
} else {
for (let i in props.options) {
transDic.value[i] = props.options[i]
console.log([typeof i])
console.log(transDic.value)
}
}
}
}
watch(props, sync)
const check = (e?: InputEvent) => {
if (props.type === ArgType.Number) {
let v = inputRef.value?.valueAsNumber
if (v !== 0 && !v) {
return false
}
if (typeof props.options?.max === 'number' && v > props.options.max) {
return false
}
if (typeof props.options?.min === 'number' && v < props.options.min) {
return false
}
value.value = v
} else if (props.type === ArgType.Region) {
if (value.value[0] !== '∞' && value.value[1] !== '∞' && value.value[0] >= value.value[1]) {
return false
}
} else if (props.type === ArgType.Text || props.type === ArgType.Password) {
value.value = inputRef.value?.value
if (!validator.isLength(value.value, props.options)) {
return false
}
}
if (typeof props.validate === 'function') {
if (!props.validate(value.value)) {
return false
}
}
return true
}
const update = () => {
if (value.value === props.modelValue) {
return
}
if (check()) {
change('idle')
emit('update:modelValue', value.value)
emit('change', value.value)
} else {
change('error')
}
}
const updateIndex = (index: number, v: any) => {
if (props.disabled) {
return
}
value.value[index] = v
update()
}
onMounted(() => {
sync()
})
const showSelectOpt = ref(false)
const selectPos = ref([0, 0])
const showSelect = (e: MouseEvent) => {
if (props.disabled) {
return
}
selectPos.value[0] = e.clientX - 20
selectPos.value[1] = e.clientY - 20
showSelectOpt.value = true
}
const setSelect = (e: any) => {
showSelectOpt.value = false
if (Array.isArray(value.value)) {
for (let i in value.value) {
if (value.value[i] === e) {
value.value.splice(i, 1)
update()
return
}
}
value.value.push(e)
} else {
value.value = e
}
update()
}
function choose_file(e: any) {
let filename = String(e.target.files[0].name)
const h = filename.substring(filename.lastIndexOf('.') + 1)
if (filename.length > 25) {
value.value = filename.slice(0, 15) + "...\xa0\xa0\xa0." + h
}
else {
value.value = filename
}
emit('upload', e.target.files[0])
// if (resultFile) {
// var reader = new FileReader();
// reader.readAsText(resultFile);
// reader.onload = function (e) {
// let d = this.result
// };
//
// }
}
function unblur() {
inputRef.value?.blur()
}
</script>
<style lang="scss" scoped>
.no-tidy {
padding: 0.5rem 2rem;
}
.vinput {
position: relative;
width: 100%;
hr {
margin: auto;
position: absolute;
bottom: -1px;
width: calc(100% - 0rem);
left: 0rem;
border: var(--input-line-default) solid 1px;
//visibility: hidden;
transition: all 0.2s linear;
}
&:hover hr {
border: var(--input-line-shine) solid 1px;
width: 100%;
left: 0rem;
}
}
.vinput-body {}
.hide-hr {
hr {
border: none !important;
width: 0;
left: 50%;
}
}
.vinput-active {
hr {
border: var(--input-line-shine) solid 1px;
}
}
.vinput-error {
hr {
border: var(--input-line-error) solid 1px !important;
}
}
.noborder {
border: none;
outline: none;
background: none;
}
select {
-webkit-appearance: none;
-moz-appearance: none;
}
.select-opt {
z-index: 10;
position: fixed;
left: 0;
top: 0;
background: #333;
transform-origin: top;
transition: height 0.3s linear;
}
.radio-btn {
background: #A8A8A8;
min-height: 2.5rem;
}
.radio-btn-active {
background: #EF857D;
}
.bool-bg {
position: absolute;
height: 100%;
left: 0px;
bottom: 0px;
/* 渐变背景 ,自左到右 */
/* background: linear-gradient(135deg, #FF9D6C, #BB4E75); */
/* background: linear-gradient(to right, #f09819, #ff5858); */
/* 添加动画过渡.贝塞尔曲线 */
transition: 0.3s cubic-bezier(1, 0.05, 0.9, 0.9);
/* transition: left 3s linear; */
}
</style>

@ -1 +1,61 @@
// app global css in SCSS form // app global css in SCSS form
:root {
transition: all 0.2s linear;
--base-color: #000;
--base-bg: #f5f5f5;
--base-bg-1: #e0e0e0;
--base-bg-2: #d0d0d0;
--base-bg-3: #c0c0c0;
--header-bg: #d0d0d0;
--color-primary: #2196f3;
--color-secondary: #03a9f4;
--color-accent: #ff9800;
--color-error: #f44336;
--color-warning: #ff5722;
--color-info: #ffc107;
--color-success: #4caf50;
--input-line-default: #002f55;
--input-line-shine: #1467ff;
--input-line-error: var(--color-error);
}
.page-h1 {
font-size: 2.5rem;
line-height: 2.5rem;
margin-left: 2.5rem;
margin-top: 1.5rem;
margin-bottom: 2rem;
}
.div-center {
@apply flex justify-center items-center;
}
.div-btn {
@apply cursor-pointer transition duration-500 ease-in-out transform hover:scale-110 hover:opacity-50;
}
.div-btn hr {
right: 50%;
position: absolute;
bottom: 1px;
width: 0;
border: var(--color-primary) solid 1px;
visibility: hidden;
transition: all 0.2s linear;
}
.div-btn:hover {
/*border-bottom: #03a9f4 1px solid;*/
}
.div-btn:hover hr {
visibility: visible;
width: 80%;
right: 10%;
}

@ -15,14 +15,15 @@
// src/css/quasar.variables.sass // src/css/quasar.variables.sass
$primary : #f74d22;
$secondary : #fa9243;
$accent : #9C27B0;
$dark : #1d1d1d; $primary : #f74d22;
$dark-page : #121212; $secondary : #fa9243;
$accent : #9C27B0;
$positive : #21BA45; $dark : #1d1d1d;
$negative : #ff0000; $dark-page : #121212;
$info : #28d4ce;
$warning : #f2e638; $positive : #21BA45;
$negative : #ff0000;
$info : #28d4ce;
$warning : #f2e638;

@ -5,7 +5,7 @@
* Distributed under terms of the MIT license. * Distributed under terms of the MIT license.
--> -->
<template> <template>
<div class="p-4"> <div class="p-4 w-full h-full">
<div class="flex items-center mb-8"> <div class="flex items-center mb-8">
<q-avatar class="mx-2" round size="4rem"> <q-avatar class="mx-2" round size="4rem">
<img :src="app.icon"> <img :src="app.icon">

@ -8,6 +8,9 @@ function padLeftZero(str: string): string {
const util = { const util = {
datetostr(d: string) {
return new Date(d + 'z').toLocaleString()
},
randomNum(minNum: number, maxNum: number) { randomNum(minNum: number, maxNum: number) {
return Math.floor(Math.random() * maxNum) + minNum return Math.floor(Math.random() * maxNum) + minNum
}, },

@ -9,6 +9,40 @@ import { RouteLocationRaw } from 'vue-router';
export { type Auths, type modelsSimpleAuth, NewAuths, R } from './auth' export { type Auths, type modelsSimpleAuth, NewAuths, R } from './auth'
export type Dict = { [key: string]: any }
export enum ArgType {
Text = 'text',
Password = 'password',
Bool = 'bool',
Select = 'select',
Radio = 'radio',
Number = 'number',
Region = 'region',
NumList = 'numList',
StrList = 'strList',
Table = 'table',
Grid = 'grid',
File = 'file',
Img = 'img'
}
export const ArgTypesTrans = {
[ArgType.Text]: '文本',
[ArgType.Password]: '密码',
[ArgType.Select]: '选择器',
[ArgType.Radio]: '单选框',
[ArgType.Number]: '数字',
[ArgType.Region]: '区间',
[ArgType.NumList]: '数组',
[ArgType.StrList]: '文本集合',
[ArgType.Table]: '表格',
[ArgType.Grid]: '矩阵',
[ArgType.File]: '文件',
[ArgType.Img]: '图片',
[ArgType.Bool]: '开关',
}
export interface DocItem { export interface DocItem {
name: string name: string
url: string url: string

@ -6,31 +6,74 @@
--> -->
<template> <template>
<div> <div>
<div> <div class="flex justify-center pt-10">
<CRUD :keys="keys" :data="data"> <CRUD ref="table" v-if="app.id" :keys="keys" :data="[app]" kalign="left" valign="left" editable
<template #a="">_____</template> :vstyle="{ 'width': '50vw' }" @update="newApp = $event[0]" :kstyle="{ 'width': '10rem' }">
<template #k_icon="{ value, set }">
<div class="w-full flex justify-center">
<uploader class="" @success="set" dir="app_icon">
<q-avatar>
<img :src="value">
</q-avatar>
</uploader>
</div>
</template>
<template #k_key>
<div class="w-full div-center">
<q-btn color='primary'>获取秘钥</q-btn>
</div>
</template>
</CRUD> </CRUD>
</div> </div>
<div v-if="newApp" class="flex justify-center gap-8 mt-6">
<q-btn color="brown-5" label="回退" @click="table.reload" />
<q-btn color="deep-orange" glossy label="保存" @click="save" />
</div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import CRUD from 'src/components/crud.vue' import CRUD from 'src/components/crud.vue'
import { ref } from 'vue'; import { ArgType, modelsApp } from 'src/models';
import { inject, onMounted, Ref, ref } from 'vue';
import uploader from 'src/components/uploader';
import api from 'src/boot/api';
import msg from '@veypi/msg';
const keys = ref<any>([ const keys = ref<any>([
{ {
name: 'name', name: 'name',
label: '阿萨德', label: '应用名',
}, { name: 'key' }, { name: 'status' } },
{ name: 'id', label: 'uuid', editable: false },
{ name: 'key', label: '秘钥Key' },
{ name: 'icon', label: 'logo' },
{
name: 'join_method', label: '用户注册', typ: ArgType.Radio,
options: [{ key: 0, name: '允许' }, { key: 1, name: '禁止' },
{ key: 2, name: '申请' }]
},
{ name: 'host', label: '项目首页' },
{ name: 'redirect', label: '跳转地址' },
]) ])
let app = inject('app') as Ref<modelsApp>
const newApp = ref(null)
const table = ref()
const save = () => {
api.app.update(app.value.id, newApp.value).then(e => {
msg.Info('更新成功')
Object.assign(app.value, newApp.value)
newApp.value = null
}).catch(e => {
msg.Warn('更新失败 ' + e)
})
}
onMounted(() => {
})
const data = ref([
{ name: 'asd', key: 'akk' },
{ name: '1sd', key: '1kk' },
{ name: '2sd', key: '2kk' },
{ name: '3sd', key: '3kk' },
{ name: '4sd', key: '4kk' },
])
</script> </script>
<style scoped></style> <style scoped></style>

@ -7,6 +7,11 @@
<template> <template>
<div> <div>
<q-table title="Treats" :rows="rows" :columns="columns" row-key="name"> <q-table title="Treats" :rows="rows" :columns="columns" row-key="name">
<template #body-cell-created="props">
<q-td :props="props">
{{ util.datetostr(props.value) }}
</q-td>
</template>
<template v-slot:body-cell-status="props"> <template v-slot:body-cell-status="props">
<q-td :props="props"> <q-td :props="props">
<div> <div>
@ -35,6 +40,7 @@ import { computed, inject, onMounted, Ref, ref, watch } from 'vue';
import { AUStatus, modelsAppUser, modelsUser, modelsApp } from 'src/models'; import { AUStatus, modelsAppUser, modelsUser, modelsApp } from 'src/models';
import api from 'src/boot/api'; import api from 'src/boot/api';
import msg from '@veypi/msg'; import msg from '@veypi/msg';
import { util } from 'src/libs';
const auOpts: { [index: number]: any } = { const auOpts: { [index: number]: any } = {
[AUStatus.OK]: ['正常', 'positive'], [AUStatus.OK]: ['正常', 'positive'],
@ -59,7 +65,7 @@ const columns = [
(row.nickname ? '(' + row.nickname + ')' : ''), (row.nickname ? '(' + row.nickname + ')' : ''),
sortable: true sortable: true
}, },
{ name: 'created', field: 'created', align: 'center', label: '加入时间', sortable: true }, { name: 'created', field: 'updated', align: 'center', label: '加入时间', sortable: true },
{ name: 'status', field: 'status', align: 'center', label: '账号状态', sortable: true }, { name: 'status', field: 'status', align: 'center', label: '账号状态', sortable: true },
{ name: 'action', field: 'action', align: 'center', label: '操作' }, { name: 'action', field: 'action', align: 'center', label: '操作' },
] as any ] as any

@ -431,6 +431,11 @@
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.4.tgz#2b38784cd16957d3782e8e2b31c03bc1d13b4d65" resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.4.tgz#2b38784cd16957d3782e8e2b31c03bc1d13b4d65"
integrity sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ== integrity sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==
"@types/validator@^13.11.2":
version "13.11.2"
resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.11.2.tgz#a2502325a3c0bd29f36dbac3b763223edd801e17"
integrity sha512-nIKVVQKT6kGKysnNt+xLobr+pFJNssJRi2s034wgWeFBUx01fI8BeHTW2TcRp7VcFu9QCYG8IlChTuovcm0oKQ==
"@typescript-eslint/eslint-plugin@^5.10.0": "@typescript-eslint/eslint-plugin@^5.10.0":
version "5.62.0" version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db"
@ -4226,6 +4231,11 @@ uuid@^9.0.0:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
validator@^13.11.0:
version "13.11.0"
resolved "https://registry.yarnpkg.com/validator/-/validator-13.11.0.tgz#23ab3fd59290c61248364eabf4067f04955fbb1b"
integrity sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==
vary@~1.1.2: vary@~1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"

Loading…
Cancel
Save