Commit 7bdfd5aa authored by 朱国瑞's avatar 朱国瑞

登录校验和裁剪视频功能

parent dd2526f1
VITE_APP_BASE_URL=http://se-test.wmdigit.com
\ No newline at end of file
...@@ -5,5 +5,5 @@ ...@@ -5,5 +5,5 @@
// Generated by unplugin-auto-import // Generated by unplugin-auto-import
export {} export {}
declare global { declare global {
const ElMessage: typeof import('element-plus/es')['ElMessage']
} }
...@@ -33,9 +33,11 @@ declare module 'vue' { ...@@ -33,9 +33,11 @@ declare module 'vue' {
IconEcosystem: typeof import('./src/components/icons/IconEcosystem.vue')['default'] IconEcosystem: typeof import('./src/components/icons/IconEcosystem.vue')['default']
IconSupport: typeof import('./src/components/icons/IconSupport.vue')['default'] IconSupport: typeof import('./src/components/icons/IconSupport.vue')['default']
IconTooling: typeof import('./src/components/icons/IconTooling.vue')['default'] IconTooling: typeof import('./src/components/icons/IconTooling.vue')['default']
LoginDialog: typeof import('./src/components/LoginDialog.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
TheWelcome: typeof import('./src/components/TheWelcome.vue')['default'] TheWelcome: typeof import('./src/components/TheWelcome.vue')['default']
VideoCrop: typeof import('./src/components/VideoCrop.vue')['default']
WelcomeItem: typeof import('./src/components/WelcomeItem.vue')['default'] WelcomeItem: typeof import('./src/components/WelcomeItem.vue')['default']
} }
export interface ComponentCustomProperties { export interface ComponentCustomProperties {
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>text2video</title> <title>text2video</title>
</head> </head>
<body> <body>
......
...@@ -11,12 +11,14 @@ ...@@ -11,12 +11,14 @@
"@element-plus/icons-vue": "^2.1.0", "@element-plus/icons-vue": "^2.1.0",
"axios": "^1.6.1", "axios": "^1.6.1",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"cropperjs": "^1.6.2",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"element-plus": "^2.4.2", "element-plus": "^2.4.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"sass": "^1.69.5", "sass": "^1.69.5",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-drag-resize": "^2.0.3",
"vue-router": "^4.2.5" "vue-router": "^4.2.5"
}, },
"devDependencies": { "devDependencies": {
...@@ -1267,6 +1269,11 @@ ...@@ -1267,6 +1269,11 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/cropperjs": {
"version": "1.6.2",
"resolved": "https://registry.npmmirror.com/cropperjs/-/cropperjs-1.6.2.tgz",
"integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA=="
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
...@@ -4168,6 +4175,11 @@ ...@@ -4168,6 +4175,11 @@
} }
} }
}, },
"node_modules/vue-drag-resize": {
"version": "2.0.3",
"resolved": "https://registry.npmmirror.com/vue-drag-resize/-/vue-drag-resize-2.0.3.tgz",
"integrity": "sha512-5q03tZ/LyvQsg1iHRcqs+wI2OKNbNIWl9+7V8rVL6MxJhZLCIYSSgbAUaDE38LhD6dFd5aJhdgNmES61AxjXuw=="
},
"node_modules/vue-eslint-parser": { "node_modules/vue-eslint-parser": {
"version": "9.3.2", "version": "9.3.2",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.2.tgz", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.2.tgz",
......
...@@ -15,12 +15,14 @@ ...@@ -15,12 +15,14 @@
"@element-plus/icons-vue": "^2.1.0", "@element-plus/icons-vue": "^2.1.0",
"axios": "^1.6.1", "axios": "^1.6.1",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"cropperjs": "^1.6.2",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"element-plus": "^2.4.2", "element-plus": "^2.4.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"sass": "^1.69.5", "sass": "^1.69.5",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-drag-resize": "^2.0.3",
"vue-router": "^4.2.5" "vue-router": "^4.2.5"
}, },
"devDependencies": { "devDependencies": {
......
<script setup lang="ts"> <script setup lang="ts">
import { RouterLink, RouterView } from "vue-router"; import { RouterLink, RouterView } from 'vue-router'
import { ElConfigProvider } from "element-plus"; import { ElConfigProvider } from 'element-plus'
import zhCn from "element-plus/dist/locale/zh-cn.mjs"; import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
const locale = zhCn; const locale = zhCn
</script> </script>
<template> <template>
......
import { getToken } from '@/utils/token'
import axios from 'axios' import axios from 'axios'
import { ElLoading } from 'element-plus' import { ElLoading, ElMessage } from 'element-plus'
const VERSION = '20210531' const VERSION = '20210531'
...@@ -40,7 +41,18 @@ function isHideLoading() { ...@@ -40,7 +41,18 @@ function isHideLoading() {
request.interceptors.request.use( request.interceptors.request.use(
(config: any) => { (config: any) => {
config.headers['tenant'] = '0' config.headers['tenant'] = '0'
// permission 处理
if (config.url === 'user/refresh') {
const refreshToken = getToken('refresh_token')
if (refreshToken) {
config.headers['Authorization'] = refreshToken
}
} else {
const accessToken = getToken('access_token')
if (accessToken) {
config.headers['Authorization'] = accessToken
}
}
if (!isHideLoading()) { if (!isHideLoading()) {
loading = ElLoading.service({ fullscreen: true }) loading = ElLoading.service({ fullscreen: true })
} }
......
This diff is collapsed.
/**
* 用户相关接口
*/
import request from '@/api/request'
import { useAuthStore } from '@/stores/state'
import { saveTokens } from '@/utils/token'
export default {
// 登录
login(postData: Wm.LoginParams) {
return new Promise((resolve, reject) => {
request
.post('/user/login', postData)
.then((res: any) => {
const authStore = useAuthStore()
authStore.login()
saveTokens(res.access_token, res.refresh_token)
resolve(res)
})
.catch((err: any) => {
reject(err)
})
})
},
// 注册
register(postData: Wm.RegisterParams) {
return request.post('/user/login', postData)
}
}
<script lang="ts" setup>
import userService from '@/api/service/userService'
import utils from '@/utils/utils'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { onMounted, reactive, ref, watch, watchEffect } from 'vue'
const props = defineProps(['visible'])
const emit = defineEmits(['update:visible'])
const isVisible = ref(props.visible)
const loginFormRef = ref<FormInstance>()
const title = ref('登录')
const isRegister = ref(false)
const isMobile = ref(utils.checkIsMobile())
const loginForm = reactive({
username: '',
password: '',
captcha: ''
})
const rules = reactive<FormRules<typeof loginForm>>({
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
captcha: [{ required: true, message: '请输入验证码', trigger: 'blur' }]
})
onMounted(() => {
console.log('mounted')
})
watch(
() => props.visible,
(val) => {
console.log('visible', val)
isVisible.value = val
}
)
const onCancel = () => {
console.log('cancel')
emit('update:visible', false)
}
const handleSwitch = () => {
console.log('handleSwitchRegister')
isRegister.value = !isRegister.value
if (isRegister.value) {
title.value = '注册'
} else {
title.value = '登录'
}
}
const onConfirm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
const postData = {
username: loginForm.username,
password: loginForm.password
}
userService
.login(postData)
.then((res: any) => {
console.log('res', res)
ElMessage.success({
message: '登录成功'
})
handleClose()
})
.catch((err: any) => {
console.log('err', err)
ElMessage.error({
message: err.message
})
})
} else {
console.log('error submit!', fields)
}
})
}
const handleClose = () => {
console.log('handleClose')
emit('update:visible', false)
}
</script>
<template>
<el-dialog
v-model="isVisible"
lock-scroll
:close-on-click-modal="false"
:show-close="false"
:title="title"
:width="isMobile ? '80%' : '400px'"
align-center
center
class="wm-login-dialog"
:class="{ 'is-mobile': isMobile }"
>
<el-form
ref="loginFormRef"
style="max-width: 400px"
:model="loginForm"
status-icon
:rules="rules"
label-width="auto"
class="wm-login-form"
>
<el-form-item label="用户名" prop="username">
<el-input class="wm-input" v-model="loginForm.username" type="text" autocomplete="off" />
</el-form-item>
<el-form-item label="密码 " prop="password">
<el-input
class="wm-input"
v-model="loginForm.password"
type="password"
autocomplete="off"
/>
</el-form-item>
<!-- <el-form-item label="验证码" prop="captcha">
<el-input v-model.number="loginForm.captcha" />
</el-form-item> -->
<!-- <div class="register-tips">
<span class="link" @click="handleSwitch">{{ isRegister ? '去登录' : '去注册' }}</span>
</div> -->
</el-form>
<template #footer>
<div class="dialog-footer">
<!-- <el-button @click="onCancel">Cancel</el-button> -->
<el-button type="primary" class="login-btn" @click="onConfirm(loginFormRef)">
登录
</el-button>
</div>
</template>
</el-dialog>
</template>
<style scoped lang="scss">
.wm-login-dialog {
.wm-login-form {
.register-tips {
text-align: right;
.link {
color: #409eff;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
}
.login-btn {
width: 100%;
}
.wm-input {
width: 300px;
}
&.is-mobile {
.wm-input {
width: 100%;
}
}
}
</style>
<style lang="scss">
.wm-login-dialog {
.el-dialog__body {
padding: 10px calc(var(--el-dialog-padding-primary) + 5px) 10px;
}
}
</style>
<script lang="ts" setup>
import userService from '@/api/service/userService'
import utils from '@/utils/utils'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { nextTick, onMounted, reactive, ref, toRaw, watch, watchEffect } from 'vue'
import VueDragResize from 'vue-drag-resize'
const props = defineProps({
visible: {
type: Boolean,
default: false
},
url: {
type: String,
default: ''
},
cropArea: {
type: Array,
default: () => []
}
})
watch(
() => [props.cropArea, props.visible],
(newVal, oldVal) => {
console.log('cropArea', newVal[0])
if (!props.visible) {
showCropBox.value = false
// handleInitCropArea()
}
}
)
const emit = defineEmits(['update:visible', 'crop'])
const isVisible = ref(props.visible)
const loginFormRef = ref<FormInstance>()
const title = ref('裁剪视频')
const isMobile = ref(utils.checkIsMobile())
const cropBoxRef = ref<any>()
const backgroundVideoRef = ref<HTMLVideoElement>()
const croppedCanvasRef = ref<HTMLCanvasElement>()
const rectData = ref({
left: 0,
top: 0,
height: 100,
width: 100
})
const showCropBox = ref(false)
const cropPreviewBox = ref({
width: 0,
height: 0
})
onMounted(() => {
console.log('mounted')
})
watch(
() => props.visible,
(val) => {
console.log('visible', val)
isVisible.value = val
}
)
const onCancel = () => {
console.log('cancel')
handleClose()
}
const onConfirm = async () => {
const video = backgroundVideoRef.value
// 计算裁剪框在视频中的位置
const scaleX = video!.videoWidth / video!.clientWidth
const scaleY = video!.videoHeight / video!.clientHeight
const cropData = {
x: Math.round(rectData.value.left * scaleX),
y: Math.round(rectData.value.top * scaleY),
width: Math.round(rectData.value.width * scaleX),
height: Math.round(rectData.value.height * scaleY)
}
if (cropData.width <= 0 || cropData.height <= 0) {
ElMessage.error('裁剪框尺寸不正确')
return
}
if (cropData.x < 0) {
cropData.x = 0
}
if (cropData.y < 0) {
cropData.y = 0
}
if (cropData.width > video!.videoWidth) {
cropData.width = video!.videoWidth
}
if (cropData.x + cropData.width > video!.videoWidth) {
cropData.width = video!.videoWidth - cropData.x
}
if (cropData.height > video!.videoHeight) {
cropData.height = video!.videoHeight
}
if (cropData.y + cropData.height > video!.videoHeight) {
cropData.height = video!.videoHeight - cropData.y
}
console.log(rectData.value, scaleX, scaleY)
console.log('cropData', cropData)
// 转换为 左上角坐标 右下角坐标 例如:[[100,200], [300,400]]
const cropAreaData = [
[cropData.x, cropData.y],
[cropData.x + cropData.width, cropData.y + cropData.height]
]
emit('crop', cropAreaData)
handleClose()
}
const handleClose = () => {
console.log('handleClose')
emit('update:visible', false)
}
const cropVideoFrame = () => {
try {
const cropBox = cropBoxRef.value.$el.getBoundingClientRect()
const video = backgroundVideoRef.value
const canvas = croppedCanvasRef.value
if (!video || !canvas) {
return
}
const ctx = canvas!.getContext('2d')
const scaleX = video!.videoWidth / video!.clientWidth
const scaleY = video!.videoHeight / video!.clientHeight
canvas!.width = cropBox.width
canvas!.height = cropBox.height
ctx!.drawImage(
video!,
(cropBox.left - video!.getBoundingClientRect().left) * scaleX,
(cropBox.top - video!.getBoundingClientRect().top) * scaleY,
cropBox.width * scaleX,
cropBox.height * scaleY,
0,
0,
cropBox.width,
cropBox.height
)
// 递归调用,实现每一帧都更新
requestAnimationFrame(cropVideoFrame)
} catch (error) {
console.log(error)
}
}
const resize = (newRect: any) => {
// console.log(newRect)
rectData.value = newRect
}
const onVideoLoaded = (val: any) => {
console.log('onVideoLoaded', val)
const video = backgroundVideoRef.value
cropPreviewBox.value = {
width: video?.clientWidth || 0,
height: video?.clientHeight || 0
}
showCropBox.value = true
const cropArea: any = toRaw(props.cropArea)
if (cropArea.length > 0) {
const scaleX = cropPreviewBox.value.width / video!.videoWidth
const scaleY = cropPreviewBox.value.height / video!.videoHeight
console.log('cropArea', cropArea, scaleX, scaleY)
const left = Math.round(cropArea[0][0] * scaleX)
const top = Math.round(cropArea[0][1] * scaleY)
const width = Math.round((cropArea[1][0] - cropArea[0][0]) * scaleX)
const height = Math.round((cropArea[1][1] - cropArea[0][1]) * scaleY)
console.log('init cropArea', left, top, width, height)
rectData.value = {
left,
top,
width,
height
}
} else {
// 全部显示
rectData.value = {
left: 0,
top: 0,
width: video!.clientWidth,
height: video!.clientHeight
}
}
// nextTick(() => {
// cropVideoFrame()
// })
}
const handleInitCropArea = () => {
const video = backgroundVideoRef.value
const cropArea: any = toRaw(props.cropArea)
if (!video) {
return
}
if (cropArea.length > 0) {
const scaleX = cropPreviewBox.value.width / video!.videoWidth
const scaleY = cropPreviewBox.value.height / video!.videoHeight
console.log('cropArea', cropArea, scaleX, scaleY)
const left = Math.round(cropArea[0][0] * scaleX)
const top = Math.round(cropArea[0][1] * scaleY)
const width = Math.round((cropArea[1][0] - cropArea[0][0]) * scaleX)
const height = Math.round((cropArea[1][1] - cropArea[0][1]) * scaleY)
console.log('init cropArea', left, top, width, height)
rectData.value = {
left,
top,
width,
height
}
}
}
</script>
<template>
<el-dialog
v-model="isVisible"
:close-on-click-modal="false"
:show-close="false"
:title="title"
:width="isMobile ? '80%' : '400px'"
align-center
center
lock-scroll
class="wm-video-crop-dialog"
:class="{ 'is-mobile': isMobile }"
>
<!-- 底部视频和裁剪框 -->
<div class="video-crop-container">
<video
ref="backgroundVideoRef"
:src="url"
autoplay
loop
muted
webkit-playsinline
playsinline
class="background-video"
@loadeddata="onVideoLoaded"
v-if="isVisible"
></video>
<VueDragResize
ref="cropBoxRef"
:w="rectData.width"
:h="rectData.height"
:x="rectData.left"
:y="rectData.top"
:parent-limitation="true"
:is-resizable="true"
:is-draggable="true"
class="crop-box"
v-on:resizing="resize"
v-on:dragging="resize"
:isActive="true"
v-if="showCropBox"
></VueDragResize>
</div>
<!-- 显示裁剪后的内容 -->
<div class="cropped-image-container" v-if="false">
<div class="title">裁剪后的视频显示效果</div>
<div
:style="{
width: cropPreviewBox.width + 'px',
height: cropPreviewBox.height + 'px'
}"
>
<canvas ref="croppedCanvasRef"></canvas>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button class="btn" @click="onCancel">取消</el-button>
<el-button type="primary" class="btn confirm" @click="onConfirm"> 裁剪 </el-button>
</div>
</template>
</el-dialog>
</template>
<style scoped lang="scss">
.wm-video-crop-dialog {
.video-crop-container {
position: relative;
width: 100%;
font-size: 0;
.background-video {
width: 100%;
height: 100%;
object-fit: cover;
}
.crop-box {
border-color: var(--el-color-primary-dark-2);
position: absolute;
cursor: move;
.vdr-stick {
background: var(--el-color-primary-dark-2);
border: 1px solid var(--el-color-primary-dark-2);
-webkit-box-shadow: 0 0 2px var(--el-color-primary-dark-2);
box-shadow: 0 0 2px var(--el-color-primary-dark-2);
}
}
}
.cropped-image-container {
margin-top: 10px;
// text-align: center;
.title {
font-size: 16px;
margin-bottom: 10px;
}
}
.brn {
&.confirm {
margin-left: 10px;
}
}
&.is-mobile {
.wm-input {
width: 100%;
}
}
}
</style>
<style lang="scss">
.wm-video-crop-dialog {
.el-dialog__body {
padding: 10px calc(var(--el-dialog-padding-primary) + 5px) 10px;
}
.video-crop-container {
.crop-box {
border-color: var(--el-color-primary);
&::before {
outline: 1px dashed var(--el-color-primary);
}
.vdr-stick {
background: var(--el-color-primary);
border: 1px solid var(--el-color-primary);
-webkit-box-shadow: 0 0 2px var(--el-color-primary);
box-shadow: 0 0 2px var(--el-color-primary);
}
}
}
}
</style>
...@@ -7,6 +7,7 @@ import 'element-plus/dist/index.css' ...@@ -7,6 +7,7 @@ import 'element-plus/dist/index.css'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import * as ElementPlusIconsVue from '@element-plus/icons-vue' import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import VueDragResize from 'vue-drag-resize'
const app = createApp(App) const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) { for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
...@@ -15,5 +16,6 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) { ...@@ -15,5 +16,6 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.use(createPinia()) app.use(createPinia())
app.use(router) app.use(router)
app.use(ElementPlus) app.use(ElementPlus)
app.use(VueDragResize)
app.mount('#app') app.mount('#app')
...@@ -5,7 +5,6 @@ import RecordSteps from '../views/home/record_steps.vue' ...@@ -5,7 +5,6 @@ import RecordSteps from '../views/home/record_steps.vue'
import CalcRetire from '../views/home/calc_retire.vue' import CalcRetire from '../views/home/calc_retire.vue'
import GenDigitHumanVideo from '../views/home/gen_digit_human_video.vue' import GenDigitHumanVideo from '../views/home/gen_digit_human_video.vue'
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL), history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [ routes: [
......
import { getToken } from '@/utils/token'
import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', {
state: () => ({
isLoggedIn: getToken('access_token') ? true : false
}),
actions: {
login() {
this.isLoggedIn = true
},
logout() {
this.isLoggedIn = false
}
}
})
/**
* 存储tokens
* @param {string} accessToken
* @param {string} refreshToken
*/
export function saveTokens(accessToken: string, refreshToken: string) {
localStorage.setItem('access_token', `Bearer ${accessToken}`)
localStorage.setItem('refresh_token', `Bearer ${refreshToken}`)
}
/**
* 存储access_token
* @param {string} accessToken
*/
export function saveAccessToken(accessToken: string) {
localStorage.setItem('access_token', `Bearer ${accessToken}`)
}
/**
* 获得某个token
* @param {string} tokenKey
*/
export function getToken(tokenKey: string) {
return localStorage.getItem(tokenKey)
}
/**
* 移除token
*/
export function removeToken() {
localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token')
}
import _cryptoJs from 'crypto-js'; import _cryptoJs from 'crypto-js'
export default class utils { export default class utils {
// 格式化为json字符串 // 格式化为json字符串
static formatJson(jsonString: string) { static formatJson(jsonString: string) {
try { try {
return JSON.stringify(JSON.parse(jsonString), null, 2); // 对JSON字符串进行格式化处理 return JSON.stringify(JSON.parse(jsonString), null, 2) // 对JSON字符串进行格式化处理
} catch (error) { } catch (error) {
return 'Invalid JSON' return 'Invalid JSON'
} }
...@@ -22,22 +22,22 @@ export default class utils { ...@@ -22,22 +22,22 @@ export default class utils {
// 从文本中提取 JSON // 从文本中提取 JSON
static extractJSON(text: string) { static extractJSON(text: string) {
// 正则表达式匹配 JSON 格式的字符串 // 正则表达式匹配 JSON 格式的字符串
const jsonRegex = /\[\s*\{\s*"序号"\s*:\s*\d+.*?\}\s*\]/s; const jsonRegex = /\[\s*\{\s*"序号"\s*:\s*\d+.*?\}\s*\]/s
const matches = text.match(jsonRegex); const matches = text.match(jsonRegex)
return matches ? matches[0] : null; return matches ? matches[0] : null
} }
// 生成年月日时分秒毫秒字符串 // 生成年月日时分秒毫秒字符串
static genDateTimeStr() { static genDateTimeStr() {
const now = new Date(); const now = new Date()
const year = now.getFullYear(); const year = now.getFullYear()
const month = (now.getMonth() + 1).toString().padStart(2, '0'); const month = (now.getMonth() + 1).toString().padStart(2, '0')
const day = now.getDate().toString().padStart(2, '0'); const day = now.getDate().toString().padStart(2, '0')
const hours = now.getHours().toString().padStart(2, '0'); const hours = now.getHours().toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0'); const minutes = now.getMinutes().toString().padStart(2, '0')
const seconds = now.getSeconds().toString().padStart(2, '0'); const seconds = now.getSeconds().toString().padStart(2, '0')
const milliseconds = now.getMilliseconds().toString().padStart(3, '0'); const milliseconds = now.getMilliseconds().toString().padStart(3, '0')
const formattedDateTime = `${year}${month}${day}${hours}${minutes}${seconds}${milliseconds}`; const formattedDateTime = `${year}${month}${day}${hours}${minutes}${seconds}${milliseconds}`
// console.log(formattedDateTime); // 输出类似:20221231120530123 // console.log(formattedDateTime); // 输出类似:20221231120530123
return formattedDateTime return formattedDateTime
} }
...@@ -45,39 +45,41 @@ export default class utils { ...@@ -45,39 +45,41 @@ export default class utils {
// 对一句话再次拆分文本 // 对一句话再次拆分文本
static splitDetailText(sentences: string[]) { static splitDetailText(sentences: string[]) {
// console.log(sentences) // console.log(sentences)
let result_sentences: string[] = []; let result_sentences: string[] = []
let currentSentence = ''; let currentSentence = ''
for (let i = 0; i < sentences.length; i++) { for (let i = 0; i < sentences.length; i++) {
const str = sentences[i]; const str = sentences[i]
currentSentence += str + ','; currentSentence += str + ','
if (i < sentences.length - 1 && (currentSentence + sentences[i + 1]).length <= 20) { if (i < sentences.length - 1 && (currentSentence + sentences[i + 1]).length <= 20) {
continue; continue
} }
if (i === sentences.length - 2 && sentences[i + 1].length <= 5) { if (i === sentences.length - 2 && sentences[i + 1].length <= 5) {
continue; continue
} }
if (currentSentence.length < 10) { if (currentSentence.length < 10) {
continue; continue
} }
result_sentences.push(currentSentence.endsWith(",") ? currentSentence.slice(0, -1) : currentSentence); result_sentences.push(
currentSentence = ''; currentSentence.endsWith(',') ? currentSentence.slice(0, -1) : currentSentence
)
currentSentence = ''
} }
// console.log('result_sentences=', result_sentences); // console.log('result_sentences=', result_sentences);
return result_sentences; return result_sentences
} }
static splitMoreText (sentences: string[]) { static splitMoreText(sentences: string[]) {
// console.log(sentences) // console.log(sentences)
let result_sentences: string[] = []; let result_sentences: string[] = []
for (let i = 0; i < sentences.length; i++) { for (let i = 0; i < sentences.length; i++) {
const str = sentences[i]; const str = sentences[i]
let tempSentences = str.split(/[!|?|。|!|?|,|,]/); let tempSentences = str.split(/[!|?|。|!|?|,|,]/)
let newList = utils.splitDetailText(tempSentences); let newList = utils.splitDetailText(tempSentences)
result_sentences = result_sentences.concat(newList); result_sentences = result_sentences.concat(newList)
} }
// console.log('result_sentences=', result_sentences); // console.log('result_sentences=', result_sentences);
return result_sentences; return result_sentences
}; }
// 拆分文本 // 拆分文本
static splitText(str: string, type: string = 'default') { static splitText(str: string, type: string = 'default') {
...@@ -98,37 +100,47 @@ export default class utils { ...@@ -98,37 +100,47 @@ export default class utils {
// 拆分英文文本 // 拆分英文文本
static splitTextEn(str: string) { static splitTextEn(str: string) {
str = str.replaceAll('"','').replaceAll('"','') str = str.replaceAll('"', '').replaceAll('"', '')
// 使用正则表达式拆分文本 // 使用正则表达式拆分文本
let sentences = str.split(/[!|?|.]/); let sentences = str.split(/[!|?|.]/)
// 过滤掉长度为 0 的句子 // 过滤掉长度为 0 的句子
sentences = sentences.filter(s => s.length > 0); sentences = sentences.filter((s) => s.length > 0)
// console.log(sentences) // console.log(sentences)
return sentences return sentences
} }
// 过滤掉中文字符 // 过滤掉中文字符
static filterChineseAndPunctuation(inputString: string) { static filterChineseAndPunctuation(inputString: string) {
return inputString.replace(/[\u4E00-\u9FA5\u3000-\u303F\uff00-\uffef]/g, '') // 过滤中文字符 return inputString
.replace(/[^\w\s]|_/g, '') // 过滤标点符号 .replace(/[\u4E00-\u9FA5\u3000-\u303F\uff00-\uffef]/g, '') // 过滤中文字符
.replace(/\s+/g, ' '); // 连续多个空格替换为一个空格 .replace(/[^\w\s]|_/g, '') // 过滤标点符号
.replace(/\s+/g, ' ') // 连续多个空格替换为一个空格
} }
// 检查该字符串是否只包含中文标点符号和英文标点符号 // 检查该字符串是否只包含中文标点符号和英文标点符号
static containsOnlyPunctuation(str: string) { static containsOnlyPunctuation(str: string) {
// 使用正则表达式匹配是否只包含标点符号(包括中文标点) // 使用正则表达式匹配是否只包含标点符号(包括中文标点)
return /^[!-\/:-@\[-`{-~\p{P}\p{S}\s]*$/u.test(str); return /^[!-\/:-@\[-`{-~\p{P}\p{S}\s]*$/u.test(str)
} }
// 加密 // 加密
static aesEncrypt(word: string) { static aesEncrypt(word: string) {
var key = _cryptoJs.enc.Utf8.parse('e6ef616dc57343248f6b3e98a07e1dde'); var key = _cryptoJs.enc.Utf8.parse('e6ef616dc57343248f6b3e98a07e1dde')
var srcs = _cryptoJs.enc.Utf8.parse(word); var srcs = _cryptoJs.enc.Utf8.parse(word)
var encrypted = _cryptoJs.AES.encrypt(srcs, key, { var encrypted = _cryptoJs.AES.encrypt(srcs, key, {
mode: _cryptoJs.mode.ECB, mode: _cryptoJs.mode.ECB,
padding: _cryptoJs.pad.Pkcs7 padding: _cryptoJs.pad.Pkcs7
}); })
return encrypted.toString(); return encrypted.toString()
} }
static checkIsMobile = () => {
const userAgent = navigator.userAgent || navigator.vendor
if (/Mobile|Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent)) {
return true
} else {
return false
}
}
} }
<script setup lang="ts"> <script setup lang="ts">
import text2videoService from "@/api/service/text2videoService"; import text2videoService from '@/api/service/text2videoService'
import utils from "@/utils/utils"; import utils from '@/utils/utils'
import { import {
ElMessage, genFileId, ElMessage,
genFileId,
type UploadInstance, type UploadInstance,
type UploadProps, type UploadProps,
type UploadRawFile type UploadRawFile
} from "element-plus"; } from 'element-plus'
import { nextTick, onMounted, reactive, ref } from "vue"; import { nextTick, onMounted, reactive, ref } from 'vue'
const debug = ref(import.meta.env.MODE === 'production' ? false : true); const debug = ref(import.meta.env.MODE === 'production' ? false : true)
// const debug = ref(false); // const debug = ref(false);
const loading = ref(false); const loading = ref(false)
const form = reactive({ const form = reactive({
task_id: "", task_id: '',
birth: "1976-01", birth: '1976-01',
sex_value: "男", sex_value: '男',
sex_option: [ sex_option: [
{ {
value: '男', value: '男',
label: '男', label: '男'
}, },
{ {
value: '女50岁退休', value: '女50岁退休',
label: '女(普通)50岁退', label: '女(普通)50岁退'
}, },
{ {
value: '女55岁退休', value: '女55岁退休',
label: '女(管理、灵活就业)55岁退', label: '女(管理、灵活就业)55岁退'
}, }
], ],
retire_age: "", retire_age: '',
retire_date: "", retire_date: '',
how_long_to_retire: "", how_long_to_retire: ''
}); })
onMounted(() => { onMounted(() => {
// 初始化task_id // 初始化task_id
form.task_id = utils.genDateTimeStr(); form.task_id = utils.genDateTimeStr()
console.log('页面加载,task_id=', form.task_id); console.log('页面加载,task_id=', form.task_id)
document.title = "延迟退休年龄计算器"; document.title = '延迟退休年龄计算器'
}); })
const calculateRetirement = (birthStr: string, gender: string) => { const calculateRetirement = (birthStr: string, gender: string) => {
console.log(birthStr) console.log(birthStr)
let parts = birthStr.split('-'); // 使用'-'作为分隔符来拆分字符串 let parts = birthStr.split('-') // 使用'-'作为分隔符来拆分字符串
let birthYear = parseInt(parts[0], 10); // 年份 let birthYear = parseInt(parts[0], 10) // 年份
let birthMonth = parseInt(parts[1], 10); // 月份 let birthMonth = parseInt(parts[1], 10) // 月份
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear()
const birthDate = new Date(`${birthYear}-${birthMonth < 10 ? '0' + birthMonth : birthMonth}-01`); const birthDate = new Date(`${birthYear}-${birthMonth < 10 ? '0' + birthMonth : birthMonth}-01`)
let retirementAge, delayMonths, retirementYear, retirementMonth; let retirementAge, delayMonths, retirementYear, retirementMonth
if (gender === '男') { if (gender === '男') {
retirementAge = 60; retirementAge = 60
delayMonths = Math.ceil(((birthYear - 1965)*12 + birthMonth) / 4); delayMonths = Math.ceil(((birthYear - 1965) * 12 + birthMonth) / 4)
} else if (gender === '女50岁退休') { } else if (gender === '女50岁退休') {
retirementAge = 50; retirementAge = 50
delayMonths = Math.ceil(((birthYear - 1975)*12 + birthMonth) / 2); delayMonths = Math.ceil(((birthYear - 1975) * 12 + birthMonth) / 2)
} else if (gender === '女55岁退休') { } else if (gender === '女55岁退休') {
retirementAge = 55; retirementAge = 55
delayMonths = Math.ceil(((birthYear - 1970)*12 + birthMonth) / 4); delayMonths = Math.ceil(((birthYear - 1970) * 12 + birthMonth) / 4)
} else { } else {
console.log("Invalid gender input. Please choose '男', '女50岁退休', or '女55岁退休'."); console.log("Invalid gender input. Please choose '男', '女50岁退休', or '女55岁退休'.")
return; return
} }
if (birthYear >= 1977 && gender == '男') { if (birthYear >= 1977 && gender == '男') {
delayMonths = 36; delayMonths = 36
} else if (birthYear >= 1985 && gender == '女50岁退休') { } else if (birthYear >= 1985 && gender == '女50岁退休') {
delayMonths = 60; delayMonths = 60
} else if (birthYear >= 1982 && gender == '女55岁退休') { } else if (birthYear >= 1982 && gender == '女55岁退休') {
delayMonths = 36; delayMonths = 36
} }
retirementAge += Math.floor(delayMonths / 12); retirementAge += Math.floor(delayMonths / 12)
console.log(retirementAge, delayMonths) console.log(retirementAge, delayMonths)
let retirementDate = new Date(birthDate)
let retirementDate = new Date(birthDate); console.log(retirementDate)
console.log(retirementDate)
retirementDate.setFullYear(retirementDate.getFullYear() + retirementAge)
retirementDate.setFullYear(retirementDate.getFullYear() + retirementAge); retirementDate.setMonth(retirementDate.getMonth() + (delayMonths % 12))
retirementDate.setMonth(retirementDate.getMonth() + delayMonths % 12); console.log(retirementDate)
console.log(retirementDate)
retirementYear = retirementDate.getFullYear()
retirementYear = retirementDate.getFullYear(); retirementMonth = retirementDate.getMonth() + 1 // getMonth() returns 0-11
retirementMonth = retirementDate.getMonth() + 1; // getMonth() returns 0-11 console.log(retirementYear, retirementMonth)
console.log(retirementYear, retirementMonth)
let yearsUntilRetirement = retirementDate.getFullYear() - currentYear - 1
let yearsUntilRetirement = retirementDate.getFullYear() - currentYear -1; let monthsUntilRetirement = ((retirementDate.getMonth() - new Date().getMonth() + 12) % 12) - 1
let monthsUntilRetirement = (retirementDate.getMonth() - new Date().getMonth() + 12) % 12 -1; if (monthsUntilRetirement < 0) {
if (monthsUntilRetirement < 0) { yearsUntilRetirement--
yearsUntilRetirement--; monthsUntilRetirement += 12
monthsUntilRetirement += 12; }
}
if (
if ((birthYear < 1965 && gender == '男') || (birthYear < 1965 && gender == '男') ||
(birthYear < 1975 && gender == '女50岁退休') || (birthYear < 1975 && gender == '女50岁退休') ||
(birthYear < 1970 && gender == '女55岁退休')) { (birthYear < 1970 && gender == '女55岁退休')
form.retire_age = `您已退休`; ) {
form.retire_date = `您已退休`; form.retire_age = `您已退休`
form.how_long_to_retire = `您已退休`; form.retire_date = `您已退休`
form.how_long_to_retire = `您已退休`
} else { } else {
form.retire_age = `${retirementAge}岁+${delayMonths % 12}个月`; form.retire_age = `${retirementAge}岁+${delayMonths % 12}个月`
form.retire_date = `${retirementYear}-${retirementMonth < 10 ? '0' + retirementMonth : retirementMonth}-01`; form.retire_date = `${retirementYear}-${
form.how_long_to_retire = `${yearsUntilRetirement}${monthsUntilRetirement}个月`; retirementMonth < 10 ? '0' + retirementMonth : retirementMonth
} }-01`
form.how_long_to_retire = `${yearsUntilRetirement}${monthsUntilRetirement}个月`
}; }
}
</script> </script>
<!-- ============================================================================================================ --> <!-- ============================================================================================================ -->
...@@ -133,12 +135,7 @@ const calculateRetirement = (birthStr: string, gender: string) => { ...@@ -133,12 +135,7 @@ const calculateRetirement = (birthStr: string, gender: string) => {
</el-form-item> </el-form-item>
<!-- 性别 --> <!-- 性别 -->
<el-form-item label="性别"> <el-form-item label="性别">
<el-select <el-select v-model="form.sex_value" placeholder="Select" size="large" style="width: 240px">
v-model="form.sex_value"
placeholder="Select"
size="large"
style="width: 240px"
>
<el-option <el-option
v-for="item in form.sex_option" v-for="item in form.sex_option"
:key="item.value" :key="item.value"
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
<script setup lang="ts"> <script setup lang="ts">
import text2videoService from "@/api/service/text2videoService"; import text2videoService from '@/api/service/text2videoService'
import utils from "@/utils/utils"; import utils from '@/utils/utils'
import { import {
ElMessage, genFileId, ElMessage,
genFileId,
type UploadInstance, type UploadInstance,
type UploadProps, type UploadProps,
type UploadRawFile type UploadRawFile
} from "element-plus"; } from 'element-plus'
import { nextTick, onMounted, reactive, ref } from "vue"; import { nextTick, onMounted, reactive, ref } from 'vue'
const debug = ref(import.meta.env.MODE === 'production' ? false : true); const debug = ref(import.meta.env.MODE === 'production' ? false : true)
// const debug = ref(false); // const debug = ref(false);
const loading = ref(false); const loading = ref(false)
const form = reactive({ const form = reactive({
task_id: "", task_id: '',
task_content: "开发一款坦克大战的小游戏", task_content: '开发一款坦克大战的小游戏',
prompt: `你是一名高级的程序员。现在需要根据产品需求,设计一份完整的程序编写的思路,比如第一步写什么模块,第二步写什么模块,整个思路应逻辑清晰、流畅。以json格式输出:\n[{"序号": 序号, "描述": 描述, "详细说明": 详细说明}, ...]\n`, prompt: `你是一名高级的程序员。现在需要根据产品需求,设计一份完整的程序编写的思路,比如第一步写什么模块,第二步写什么模块,整个思路应逻辑清晰、流畅。以json格式输出:\n[{"序号": 序号, "描述": 描述, "详细说明": 详细说明}, ...]\n`,
answer: "", answer: '',
answer_json: [], answer_json: []
}); })
const llms = { const llms = {
tyqw_online: {'api': 'tyqw', 'name':'线上通义千问'}, tyqw_online: { api: 'tyqw', name: '线上通义千问' },
baichuan: {'api': 'langchain', 'name':'本地baichuan2-7b'}, baichuan: { api: 'langchain', name: '本地baichuan2-7b' },
qwen_local: {'api': 'langchain', 'name':'本地Qwen-7B-Chat'}, qwen_local: { api: 'langchain', name: '本地Qwen-7B-Chat' },
chatgpt: {'api': 'gpt', 'name':'chatgpt'}, chatgpt: { api: 'gpt', name: 'chatgpt' },
kimi: {'api': 'kimi', 'name':'kimi'}, kimi: { api: 'kimi', name: 'kimi' }
}; }
onMounted(() => { onMounted(() => {
// 初始化task_id // 初始化task_id
form.task_id = utils.genDateTimeStr(); form.task_id = utils.genDateTimeStr()
console.log('页面加载,task_id=', form.task_id) console.log('页面加载,task_id=', form.task_id)
}); })
const onSubmitGpt = () => { const onSubmitGpt = () => {
const my_prompt = ref(`${form.prompt}。产品需求如下:${form.task_content}`); const my_prompt = ref(`${form.prompt}。产品需求如下:${form.task_content}`)
text2videoService text2videoService
.submitLLM(utils.aesEncrypt(my_prompt.value), utils.aesEncrypt(llms.tyqw_online.api), [], form.task_id, "true") .submitLLM(
utils.aesEncrypt(my_prompt.value),
utils.aesEncrypt(llms.tyqw_online.api),
[],
form.task_id,
'true'
)
.then((result: string) => { .then((result: string) => {
console.log(result); console.log(result)
form.answer = result; form.answer = result
const jsonText = utils.extractJSON(result); const jsonText = utils.extractJSON(result)
console.log(jsonText); console.log(jsonText)
form.answer_json = utils.formatJsonObj(jsonText!); form.answer_json = utils.formatJsonObj(jsonText!)
}) })
.catch((error: any) => { .catch((error: any) => {
console.error(error); console.error(error)
ElMessage({ ElMessage({
message: error, message: error,
type: "error", type: 'error'
}); })
}); })
}; }
</script> </script>
<!-- ============================================================================================================ --> <!-- ============================================================================================================ -->
...@@ -99,9 +106,7 @@ const onSubmitGpt = () => { ...@@ -99,9 +106,7 @@ const onSubmitGpt = () => {
<el-table-column width="200" label="操作" align="center"> <el-table-column width="200" label="操作" align="center">
<template v-slot="scope"> <template v-slot="scope">
<div style="margin: 5px 0"> <div style="margin: 5px 0">
<el-button type="primary" size="small" @click="" <el-button type="primary" size="small" @click="">与大模型继续沟通</el-button>
>与大模型继续沟通</el-button
>
</div> </div>
<!-- <div style="margin: 10px 0"> <!-- <div style="margin: 10px 0">
......
/// <reference path="./types/index.d.ts" /> /// <reference path="./types/index.d.ts" />
declare module 'element-plus/dist/locale/zh-cn.mjs' declare module 'element-plus/dist/locale/zh-cn.mjs'
declare module 'vue-drag-resize';
\ No newline at end of file
...@@ -68,4 +68,19 @@ declare namespace Wm { ...@@ -68,4 +68,19 @@ declare namespace Wm {
"x": int, "x": int,
"y": int, "y": int,
} }
interface LoginParams {
username: string
password: string
captcha?: string
}
interface RegisterParams {
confirm_password: string
email?: string
group_ids: number[]
nickname: string
password: string
username: string
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment