feat: first commit

This commit is contained in:
2025-03-27 19:33:39 +08:00
commit af5072e5ef
64 changed files with 7356 additions and 0 deletions
+109
View File
@@ -0,0 +1,109 @@
<template>
<div :class="'angle ' + identity + ' ' + direction">
</div>
</template>
<script lang="ts">
// 箭头方向
export enum EDirection {
LeftTop = "left-top",
LeftBottom = "left-bottom",
RightTop = "right-top",
RightBottom = "right-bottom"
}
// 身份类型
export enum EIdentity {
// 重点人员
Suspect = "suspect",
// 陌生人
Stranger = "stranger",
// 居民
Resident = "resident",
// 未归档
Unknow = "unknow",
// 其它
Other = "other"
}
</script>
<script setup lang="ts">
withDefaults(defineProps<{ identity: EIdentity, direction: EDirection }>(), {
identity: EIdentity.Stranger,
direction: EDirection.LeftTop
});
</script>
<style lang="scss">
$suspect-color:#ff5757;
$stranger-color:#f9db12;
$residents-color:#12DB68;
$unknow-color:#989898;
$primary1_color:#00C9DE;
@mixin borderStyle($color) {
height: 8px;
width: 8px;
border-top: 3px solid $color;
border-left: 3px solid $color;
}
@mixin angle-block($color) {
&.left-top {
@include borderStyle($color);
position: absolute;
left: 0;
top: 0;
}
&.left-bottom {
@include borderStyle($color);
transform: rotate(270deg);
position: absolute;
bottom: 0;
left: 0;
}
&.right-top {
@include borderStyle($color);
transform: rotate(90deg);
position: absolute;
right: 0;
top: 0;
}
&.right-bottom {
@include borderStyle($color);
transform: rotate(180deg);
position: absolute;
bottom: 0;
right: 0;
}
}
.angle {
// 重点人员
&.suspect {
@include angle-block($suspect-color);
}
// 陌生人
&.stranger {
@include angle-block($stranger-color);
}
// 居民
&.resident {
@include angle-block($residents-color);
}
// 未归档
&.unknow {
@include angle-block($unknow-color);
}
// 其它
&.other {
@include angle-block($primary1_color);
}
}
</style>
+13
View File
@@ -0,0 +1,13 @@
<template>
<object-rect :top="top" :left="left" :width="-(left - right)" :height="-(top - bottom)"></object-rect>
</template>
<script setup lang="ts">
// import { EIdentity } from './Angle.vue';
import { computed } from 'vue';
const props = defineProps<{
top: number,
right: number,
bottom: number,
left: number
}>()
</script>
+47
View File
@@ -0,0 +1,47 @@
<template>
<codemirror v-model="value" placeholder="Code goes here..." :style="{ height: '400px' }" :autofocus="true"
:indent-with-tab="true" :tab-size="2" :extensions="extensions" @ready="handleReady" @change="log('change', $event)"
@focus="log('focus', $event)" @blur="log('blur', $event)" />
</template>
<script>
import { defineComponent } from 'vue'
import { Codemirror } from 'vue-codemirror'
import { javascript } from '@codemirror/lang-javascript'
// import { oneDark } from '@codemirror/theme-one-dark'
export default defineComponent({
components: {
Codemirror
},
setup() {
const code = ref(`console.log('Hello, world!')`)
const extensions = [javascript()]
// Codemirror EditorView instance ref
const view = shallowRef()
const handleReady = (payload) => {
view.value = payload.view
}
// Status is available at all times via Codemirror EditorView
const getCodemirrorStates = () => {
const state = view.value.state
const ranges = state.selection.ranges
const selected = ranges.reduce((r, range) => r + range.to - range.from, 0)
const cursor = ranges[0].anchor
const length = state.doc.length
const lines = state.doc.lines
// more state info ...
// return ...
}
return {
value,
extensions,
handleReady,
log: console.log
}
}
})
</script>
+36
View File
@@ -0,0 +1,36 @@
<template>
<p class="el-message__content">{{ getMsgTempalte ? getMsgTempalte(countDownTime) : countDownTime }}</p>
</template>
<script setup lang="ts">
import { onBeforeMount, ref, onBeforeUnmount } from "vue";
const props = withDefaults(defineProps<{
timespan?: number,
getMsgTempalte: (value: number) => string
}>(), {
timespan: 3
})
const emit = defineEmits<{
(n: 'next'): void
}>()
const countDownTime = ref(props.timespan);
const timeout = ref();
onBeforeMount(() => {
timeout.value = setInterval(() => {
if (countDownTime.value >= 1) {
countDownTime.value -= 1;
}
else {
clearInterval(timeout.value);
emit('next');
}
}, 1000);
})
onBeforeUnmount(() => {
clearInterval(timeout.value);
})
</script>
+57
View File
@@ -0,0 +1,57 @@
<template>
<el-tag v-for="tag in dynamicTags" :key="tag" class="mx-1" closable :disable-transitions="false"
@close="handleClose(tag)">
{{ tag }}
</el-tag>
<el-input v-if="inputVisible" ref="InputRef" v-model="inputValue" class="ml-1 w-20" size="small"
@keyup.enter="handleInputConfirm" @blur="handleInputConfirm" />
<el-button v-else class="button-new-tag ml-1" size="small" @click="showInput">
+ New Tag
</el-button>
</template>
<style scoped>
.mx-1 {
margin-left: 0.25rem;
margin-right: 0.25rem;
}
</style>
<script lang="ts" setup>
import { nextTick, ref } from 'vue'
import { ElInput } from 'element-plus'
const props = defineProps<{ modelValue: [] }>();
const emits = defineEmits<{
(
e: "update:modelValue",
val: string | number | (string | number)[] | undefined
): void;
}>();
const inputValue = ref('')
const dynamicTags = ref(props.modelValue)
const inputVisible = ref(false)
const InputRef = ref<InstanceType<typeof ElInput>>()
const handleClose = (tag: string) => {
dynamicTags.value.splice(dynamicTags.value.indexOf(tag), 1)
emits('update:modelValue', dynamicTags.value)
}
const showInput = () => {
inputVisible.value = true
nextTick(() => {
InputRef.value!.input!.focus()
})
}
const handleInputConfirm = () => {
if (inputValue.value) {
dynamicTags.value.push(inputValue.value)
emits('update:modelValue', dynamicTags.value)
}
inputVisible.value = false
inputValue.value = ''
}
</script>
+230
View File
@@ -0,0 +1,230 @@
<template>
<div :class="$style['img-preview']">
<vue-cropper :key="id" ref="cropper" :src="imgUrl" alt="" :autoCrop="false" dragMode="move" :background="false"
:viewMode="2" :toggleDragModeOnDblclick="false" @ready="onReady" @crop="onCrop" @zoom="onZoom" />
<div :class="$style['mask']">
<calc-rect-dock :top="rectPosition.top" :right="rectPosition.right" :bottom="rectPosition.bottom"
:left="rectPosition.left"></calc-rect-dock>
</div>
<div :class="$style['prev']" @click="$emit('imgIdxChange', -1)">
<el-icon>
<d-arrow-left />
</el-icon>
</div>
<div :class="$style['next']" @click="$emit('imgIdxChange', 1)">
<el-icon>
<d-arrow-right />
</el-icon>
</div>
</div>
</template>
<script setup lang="ts">
import VueCropper from 'vue-cropperjs';
import 'cropperjs/dist/cropper.css';
import { reactive, ref, onBeforeMount, watch } from "vue";
import { computed } from '@vue/reactivity';
import { merge, cloneDeep } from "lodash-es";
import {
DArrowLeft,
DArrowRight,
} from "@element-plus/icons-vue";
const props = defineProps<{
id: "",
imgUrl: "",
position: "",
}>();
defineEmits<{
(n: 'imgIdxChange', value: TImgPreviewIdx): void
}>();
const cropper = ref();
const cropOption = reactive({
ratio: 1,
delta: { deltaX: 0, deltaY: 0 }
});
const rectPosition = reactive({
top: 0,
right: 0,
bottom: 0,
left: 0
});
const defRatio = ref(0);
onBeforeMount(() => {
initRectPosition();
})
watch([cropOption, () => props.id], () => {
initRectPosition();
})
const initRectPosition = () => {
const rectOption: any = formatRectPosition(cloneDeep(props.position)) || {};
merge(rectPosition, getStyle(rectOption));
}
const initCropOption = (e) => {
const canvasData: any = e.target?.cropper?.canvasData;
if (canvasData) {
if (e?.detail?.ratio && e.detail.ratio >= defRatio.value) {
merge(cropOption, {
ratio: e.detail.ratio,
delta: {
deltaX: canvasData.left,
deltaY: canvasData.top
}
});
}
else {
merge(cropOption, {
delta: {
deltaX: canvasData.left,
deltaY: canvasData.top
}
});
}
}
}
/**
* 图片渲染完成事件
* @param e
*/
const onReady = (e: any) => {
const canvasData: any = e.target?.cropper?.canvasData;
if (canvasData) {
defRatio.value = canvasData.width / e.target.width;
merge(cropOption, {
ratio: defRatio.value,
delta: { deltaX: 0, deltaY: 0 }
})
}
};
const onCrop = (e) => {
initCropOption(e);
}
const onZoom = (e) => {
initCropOption(e);
}
/**
* 格式化矩形框位置信息
* @param position json字符串
*/
const formatRectPosition = (position: any) => {
const fpm: any = {};
let rectInfo: any = {};
if (typeof (position) === "string") {
try {
rectInfo = JSON.parse(position);
const { face_rect, object_rect } = rectInfo;
if (face_rect) {
fpm.top = face_rect.top;
fpm.right = face_rect.right;
fpm.bottom = face_rect.bottom;
fpm.left = face_rect.left;
}
else if (object_rect) {
fpm.top = object_rect.top;
fpm.right = object_rect.right;
fpm.bottom = object_rect.bottom;
fpm.left = object_rect.left;
fpm.type = 2;//ERectType.Body;
}
return fpm;
} catch (error) {
console.warn("小图位置信息有误", position);
return null;
}
}
else {
if (position.face_rect) {
return position.face_rect;
}
else if (position.object_rect) {
return position.object_rect;
}
}
console.warn("小图位置信息有误", position);
return null;
}
/**
* 定位矩形框
*/
const getStyle = (position: any) => {
const faceRect = position;
const { ratio, delta } = cropOption;
if (ratio != -1) {
for (const item in faceRect) {
if (["top", "right", "bottom", "left"].includes(item))
faceRect[item] = parseFloat(faceRect[item]) * ratio
}
}
return {
top: faceRect.top + delta.deltaY,
left: faceRect.left + delta.deltaX,
bottom: faceRect.bottom + delta.deltaY,
right: faceRect.right + delta.deltaX,
};
}
</script>
<style module lang="scss">
:global {
:local(.img-preview) {
position: relative;
display: flex;
flex-direction: column;
:local(.mask) {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
pointer-events: none;
}
&:hover {
:local(.prev),
:local(.next) {
display: flex;
align-items: center;
justify-content: center;
}
}
:local(.prev),
:local(.next) {
position: absolute;
height: 100px;
width: 100px;
top: 50%;
transform: translate(0, -50%);
background-color: rgb(10 10 10 / 20%);
line-height: 100px;
text-align: center;
cursor: pointer;
display: none;
pointer-events: painted;
font-size: 30px;
}
:local(.prev) {
left: 0;
}
:local(.next) {
right: 0;
transform: translate(0, -50%);
}
}
}
</style>
+10
View File
@@ -0,0 +1,10 @@
<template>
<system-layout>
<template #sidebar>
<system-menu menuIdx="1-1" :parentIdxList="[1]" />
</template>
<template #content>
<router-view></router-view>
</template>
</system-layout>
</template>
+54
View File
@@ -0,0 +1,54 @@
<template>
<div :class="['face-identity-rect', getRootClassName(), identity].join(' ')"
:style="{ width: width + 'px', height: height + 'px' }">
<angle :identity="identity" :direction="EDirection.LeftTop"></angle>
<angle :identity="identity" :direction="EDirection.LeftBottom"></angle>
<angle :identity="identity" :direction="EDirection.RightTop"></angle>
<angle :identity="identity" :direction="EDirection.RightBottom"></angle>
</div>
</template>
<script setup lang="ts">
import { EDirection, EIdentity } from "./Angle.vue";
import { computed } from "vue";
const props: any = withDefaults(defineProps<{
identity: EIdentity, type: number,
width: number, height: number,
top: number,
left: number
}>(), {
identity: EIdentity.Resident,
type: 1
})
const topVar = computed(() => props.top + 'px')
const leftVar = computed(() => props.left + 'px')
const getRootClassName = () => {
// if (props.type == 1)
// return "face-rect";
// else
return "body-rect";
}
</script>
<style lang="scss">
.face-identity-rect {
position: absolute;
overflow: hidden;
top: v-bind(topVar);
left: v-bind(leftVar);
&.body-rect {
//.face-rect {
// background-color: rgba(255, 140, 0, .19);
border: solid 2px #ff8c00;
// }
.angle {
display: none;
}
}
}
</style>
+47
View File
@@ -0,0 +1,47 @@
<template>
<div :class="$style['upload']" @change="onFileChange">
<input ref="fileRef" :accept="accept" v-show="false" type="file" />
<el-input :model-value="fileName" :disabled="true" :style="{ width: '200px' }" />
<el-button :class="$style['scan']" @click="onPreUpload">浏览</el-button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
withDefaults(defineProps<{
accept?: any
}>(), {
accept: '*'
});
const emit = defineEmits<{
(n: "fileChange", file: File): void
}>();
const fileRef = ref();
const fileName = ref();
const onFileChange = (e: any) => {
const files = e.target.files;
fileName.value = files?.[0]?.name;
emit('fileChange', files?.[0]);
}
const onPreUpload = () => {
fileRef.value.value = "";
fileRef.value?.click();
}
</script>
<style module lang="scss">
:global {
:local(.upload) {
display: flex;
flex-direction: row;
align-items: center;
:local(.scan) {
margin-left: 10px;
}
}
}
</style>
+51
View File
@@ -0,0 +1,51 @@
<template>
<el-row>
<el-col :span="24">
<slot name="header" :style="{ height: '80px' }">
<home-header></home-header>
</slot>
</el-col>
</el-row>
<el-row :style="{
height: 'calc(100% - 80px)'
}">
<el-col :span="3">
<slot name="sidebar"></slot>
</el-col>
<el-col :span="21" :style="{
background: '#f0f2f5', padding: '20px', overflow: 'auto',
height: 'calc(100vh - 80px)'
}">
<slot name="content"></slot>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
</el-col>
</el-row>
</template>
<script setup lang="ts">
import HomeHeader from "@/containers/HomeHeader.vue"
</script>
<style module lang="scss">
/* .el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px;
min-height: 400px;
} */
:global {
html,
body,
#app {
height: 100%;
width: 100%;
padding: 0;
margin: 0;
}
:local(.layout) {
height: 100%;
}
}
</style>
+100
View File
@@ -0,0 +1,100 @@
<template>
<!-- <el-radio-group v-model="isCollapse" style="margin-bottom: 20px">
<el-radio-button :label="false">expand</el-radio-button>
<el-radio-button :label="true">collapse</el-radio-button>
</el-radio-group> -->
<div :class="$style['system-menu']">
<el-menu :default-active="activeIndex" :default-openeds="openeds" :router="true" class="el-menu-vertical-demo"
:collapse="isCollapse" active-text-color="#409eff" background-color="#001529" text-color="#fff"
:style="{ border: 0 }" @open="handleOpen" @close="handleClose">
<template v-if="menuJson.length == 0">
<el-menu-item index="/app/Employee">
用戶列表
</el-menu-item>
</template>
<template v-else>
<el-menu-item v-for="(item, index) in menuJson" :key="item.name" :index="item.route">
{{ item.name }}
</el-menu-item>
</template>
</el-menu>
</div>
</template>
<script lang="ts" setup>
import { ref, onBeforeMount } from 'vue'
import {
Menu as IconMenu,
Setting,
} from '@element-plus/icons-vue'
import { useRoute } from "vue-router";
import * as api from "@/common/api";
import { MENU_JSON } from "@/consts/dataDict";
const props = withDefaults(defineProps<{
menuIdx: string,
parentIdxList: string[]
}>(), {
menuIdx: "/app/system/info",
parentIdxList: () => ['system']
})
const activeIndex = ref();
const openeds = ref<string[]>([]);
const router = useRoute();
const menuJson = ref<any>([]);
onBeforeMount(() => {
activeIndex.value = router.path;
switch (true) {
case router.path.includes('/app/device'):
openeds.value = ['/app/device']
break;
case router.path.includes('/app/system'):
default:
openeds.value = ['/app/system/info'];
break;
}
getMenuJson();
})
const isCollapse = ref(false)
const handleOpen = (key: string, keyPath: string[]) => {
}
const handleClose = (key: string, keyPath: string[]) => {
}
const goTo = (url) => {
document.location.href = url;
}
const getMenuJson = async () => {
const { data: dataDict } = await api.getDynamic({ id: MENU_JSON }, "dataDict");
if (dataDict?.value) {
try {
menuJson.value = JSON.parse(dataDict.value);
} catch (error) {
console.log(error);
}
}
}
</script>
<style module lang="scss">
/* .el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px;
min-height: 400px;
} */
:global {
:local(.system-menu) {
height: 100%;
.el-menu-vertical-demo {
height: 100%;
}
}
}
</style>
+83
View File
@@ -0,0 +1,83 @@
'use strict';
import ExecutionEnvironment from 'exenv';
function transitionEnd() {
const transitionEndEventNames = {
WebkitTransition: 'webkitTransitionEnd',
MozTransition: 'transitionend',
OTransition: 'oTransitionEnd otransitionend',
transition: 'transitionend'
};
if (!ExecutionEnvironment.canUseDOM) {
return transitionEndEventNames;
}
const el = document.createElement('pin');
for (const name in transitionEndEventNames) {
if (el.style[name] !== undefined) {
return transitionEndEventNames[name];
}
}
return false;
}
const ifHasTransitionEnd = transitionEnd();
const prefixes = ['Webkit', 'Moz', 'ms', 'O', ''];
class AnimationManager {
constructor() {
this.animationHandle = `css${ifHasTransitionEnd ? 3 : 2}Animation`;
}
generate(options) {
Object.assign(this, options);
return this[this.animationHandle]();
}
css2Animation() {
const style = {};
style[this.horizontalDirection] = `${this.position[0]}px`;
style[this.verticalDirection] = `${this.position[1]}px`;
this.mixAnimation(style);
return style;
}
css3Animation() {
const style = {};
prefixes.map(prefix => {
let x, y;
if (this.horizontalDirection === 'right') {
x = this.containerWidth - this.size.width - this.position[0];
} else {
x = this.position[0];
}
if (this.verticalDirection === 'bottom') {
y = this.containerHeight - this.size.height - this.position[1];
} else {
y = this.position[1];
}
style[`${prefix}Transform`] = `translate3d(${x}px, ${y}px, 0)`;
});
this.mixAnimation(style);
return style;
}
mixAnimation(style) {
if (!this.closeAnimation) {
prefixes.map(prefix => {
style[`${prefix}TransitionDuration`] = `${this.transitionDuration}s`;
style[`${prefix}TransitionTimingFunction`] = this.transitionTimingFunction;
});
}
}
}
export default AnimationManager;
+175
View File
@@ -0,0 +1,175 @@
<template>
<div ref="container" :class="`${prefixClassName}-container`" :style="containerStyle">
<slot></slot>
</div>
</template>
<script setup>
import {
GridSort
} from 'autoresponsive-core3';
import pkg from '../../package';
import AnimationManager from './animation';
import {onBeforeMount, onMounted, onUpdated, ref} from "vue";
const props = defineProps({
containerWidth: {
type: Number,
default: null
},
containerHeight: {
type: Number,
default: null
},
gridWidth: {
type: Number,
default: 10
},
prefixClassName: {
type: String,
default: pkg.name
},
itemClassName: {
type: String,
default: 'item'
},
itemMargin: {
type: Number,
default: 0
},
horizontalDirection: {
type: String,
default: 'left'
},
transitionDuration: {
type: [String, Number],
default: 1
},
transitionTimingFunction: {
type: String,
default: 'linear'
},
verticalDirection: {
type: String,
default: 'top'
},
closeAnimation: {
type: Boolean,
default: false
},
onItemDidLayout: {
type: Function,
default: () => {}
},
onContainerDidLayout: {
type: Function,
default: () => {}
}
});
const containerStyle = {
position: 'relative'
};
let animationManager, fixedContainerHeight;
onBeforeMount(() => {
animationManager = new AnimationManager();
fixedContainerHeight = typeof props.containerHeight === 'number';
})
const mixItemInlineStyle = (s) => {
const itemMargin = props.itemMargin;
let style = {
display: 'block',
float: 'left',
margin: `0 ${itemMargin}px ${itemMargin}px 0`
};
if (props.containerWidth) {
style = {
position: 'absolute'
};
}
Object.assign(s, style);
}
const container = ref(null);
const updateChildren = () => {
const sortManager = new GridSort({
containerWidth: props.containerWidth,
gridWidth: props.gridWidth
});
sortManager.init();
let containerHeight = props.verticalDirection === 'bottom' || fixedContainerHeight ? props.containerHeight : 0;
const children = container.value.children;
for (let i = 0; i < children.length; i++) {
const node = children[i];
const canvas = node.__vnode.el;
let style = {};
switch (canvas.style.constructor.name) {
case 'CSS2Properties':
Object.values(canvas.style).forEach((prop) => {
style[prop] = canvas.style[prop];
});
break;
case 'CSSStyleDeclaration':
style = canvas.style;
break;
}
if (node.className &&
props.itemClassName &&
!~node.className.indexOf(props.itemClassName)) {
return;
}
const childWidth = parseInt(style.width, 10) + props.itemMargin;
const childHeight = parseInt(style.height, 10) + props.itemMargin;
const calculatedPosition = sortManager.getPosition(childWidth, childHeight);
if (fixedContainerHeight) {
container.value.style.height = `${containerHeight}px`;
} else {
if (calculatedPosition[1] + childHeight > containerHeight) {
containerHeight = calculatedPosition[1] + childHeight;
container.value.style.height = `${containerHeight}px`;
}
}
const options = Object.assign({}, props, {
position: calculatedPosition,
size: {
width: childWidth,
height: childHeight
},
containerHeight: containerHeight
});
const calculatedStyle = animationManager.generate(options);
mixItemInlineStyle(calculatedStyle);
Object.assign(node.style, calculatedStyle);
props.onItemDidLayout(node);
if (i + 1 === children.length) {
props.onContainerDidLayout();
}
}
}
onMounted(() => {
updateChildren();
})
onUpdated(() => {
updateChildren();
})
</script>