mirror of
https://github.com/yangjian102621/geekai.git
synced 2025-11-12 20:23:46 +08:00
feat(ui): web移动端初始化
This commit is contained in:
352
new-ui/projects/web/src/components/CaptchaPlus.vue
Normal file
352
new-ui/projects/web/src/components/CaptchaPlus.vue
Normal file
@@ -0,0 +1,352 @@
|
||||
<template>
|
||||
|
||||
<div class="wg-cap-wrap" :style="{width: width}">
|
||||
<div class="wg-cap-wrap__header">
|
||||
<span>请在下图<em>依次</em>点击:</span>
|
||||
<img class="wg-cap-wrap__thumb" v-if="thumbBase64Code" :src="thumbBase64Code" alt=" ">
|
||||
</div>
|
||||
<div class="wg-cap-wrap__body">
|
||||
<img class="wg-cap-wrap__picture" v-if="imageBase64Code" :src="imageBase64Code" alt=" "
|
||||
@click="handleClickPos($event)">
|
||||
<img class="wg-cap-wrap__loading"
|
||||
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBzdHlsZT0ibWFyZ2luOiBhdXRvOyBiYWNrZ3JvdW5kOiByZ2JhKDI0MSwgMjQyLCAyNDMsIDApOyBkaXNwbGF5OiBibG9jazsgc2hhcGUtcmVuZGVyaW5nOiBhdXRvOyIgd2lkdGg9IjY0cHgiIGhlaWdodD0iNjRweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIj4KICA8Y2lyY2xlIGN4PSI1MCIgY3k9IjM2LjgxMDEiIHI9IjEzIiBmaWxsPSIjM2U3Y2ZmIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN5IiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgY2FsY01vZGU9InNwbGluZSIga2V5U3BsaW5lcz0iMC40NSAwIDAuOSAwLjU1OzAgMC40NSAwLjU1IDAuOSIga2V5VGltZXM9IjA7MC41OzEiIHZhbHVlcz0iMjM7Nzc7MjMiPjwvYW5pbWF0ZT4KICA8L2NpcmNsZT4KPC9zdmc+"
|
||||
alt="正在加载中...">
|
||||
<div v-for="(dot, key) in dots" :key="key" class="wg-cap-wrap__dot" :style="`top: ${dot.y}px; left:${dot.x}px;`">
|
||||
<span>{{ dot.index }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wg-cap-wrap__footer">
|
||||
<div class="wg-cap-wrap__ico">
|
||||
<img @click="handleCloseEvent"
|
||||
src="data:image/svg+xml;base64,PHN2ZyB0PSIxNjI2NjE0NDM5NDIzIiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9Ijg2NzUiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNNTEyIDIzLjI3MjcyN2MyNjkuOTE3MDkxIDAgNDg4LjcyNzI3MyAyMTguODEwMTgyIDQ4OC43MjcyNzMgNDg4LjcyNzI3M2E0ODYuNjMyNzI3IDQ4Ni42MzI3MjcgMCAwIDEtODQuOTQ1NDU1IDI3NS40MDk0NTUgNDYuNTQ1NDU1IDQ2LjU0NTQ1NSAwIDAgMS03Ni44NDY1NDUtNTIuNTI2NTQ2QTM5My41NDE4MTggMzkzLjU0MTgxOCAwIDAgMCA5MDcuNjM2MzY0IDUxMmMwLTIxOC41MDc2MzYtMTc3LjEyODcyNy0zOTUuNjM2MzY0LTM5NS42MzYzNjQtMzk1LjYzNjM2NFMxMTYuMzYzNjM2IDI5My40OTIzNjQgMTE2LjM2MzYzNiA1MTJzMTc3LjEyODcyNyAzOTUuNjM2MzY0IDM5NS42MzYzNjQgMzk1LjYzNjM2NGEzOTUuMTcwOTA5IDM5NS4xNzA5MDkgMCAwIDAgMTI1LjQ0LTIwLjI5MzgxOSA0Ni41NDU0NTUgNDYuNTQ1NDU1IDAgMCAxIDI5LjQ4NjU0NSA4OC4yOTY3MjhBNDg4LjI2MTgxOCA0ODguMjYxODE4IDAgMCAxIDUxMiAxMDAwLjcyNzI3M0MyNDIuMDgyOTA5IDEwMDAuNzI3MjczIDIzLjI3MjcyNyA3ODEuOTE3MDkxIDIzLjI3MjcyNyA1MTJTMjQyLjA4MjkwOSAyMy4yNzI3MjcgNTEyIDIzLjI3MjcyN3ogbS0xMTUuMiAzMDcuNzEyTDUxMiA0NDYuMTM4MTgybDExNS4yLTExNS4yYTQ2LjU0NTQ1NSA0Ni41NDU0NTUgMCAxIDEgNjUuODE1MjczIDY1Ljg2MTgxOEw1NzcuODYxODE4IDUxMmwxMTUuMiAxMTUuMmE0Ni41NDU0NTUgNDYuNTQ1NDU1IDAgMSAxLTY1Ljg2MTgxOCA2NS44MTUyNzNMNTEyIDU3Ny44NjE4MThsLTExNS4yIDExNS4yYTQ2LjU0NTQ1NSA0Ni41NDU0NTUgMCAxIDEtNjUuODE1MjczLTY1Ljg2MTgxOEw0NDYuMTM4MTgyIDUxMmwtMTE1LjItMTE1LjJhNDYuNTQ1NDU1IDQ2LjU0NTQ1NSAwIDEgMSA2NS44NjE4MTgtNjUuODE1MjczeiIgcC1pZD0iODY3NiIgZmlsbD0iIzcwNzA3MCI+PC9wYXRoPjwvc3ZnPg=="
|
||||
alt="关闭">
|
||||
<img @click="handleRefreshEvent"
|
||||
src="data:image/svg+xml;base64,PHN2ZyB0PSIxNjI2NjE0NDk5NjM4IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjEzNjAiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMTg3LjQ1NiA0MjUuMDI0YTMzNiAzMzYgMCAwIDAgMzY4LjM4NCA0MjAuMjI0IDQ4IDQ4IDAgMCAxIDEyLjU0NCA5NS4xNjggNDMyIDQzMiAwIDAgMS00NzMuNjY0LTU0MC4xNmwtNTcuMjgtMTUuMzZhMTIuOCAxMi44IDAgMCAxLTYuMjcyLTIwLjkyOGwxNTkuMTY4LTE3OS40NTZhMTIuOCAxMi44IDAgMCAxIDIyLjE0NCA1Ljg4OGw0OC4wNjQgMjM1LjA3MmExMi44IDEyLjggMCAwIDEtMTUuODA4IDE0LjkxMmwtNTcuMjgtMTUuMzZ6TTgzNi40OCA1OTkuMDRhMzM2IDMzNiAwIDAgMC0zNjguMzg0LTQyMC4yMjQgNDggNDggMCAxIDEtMTIuNTQ0LTk1LjE2OCA0MzIgNDMyIDAgMCAxIDQ3My42NjQgNTQwLjE2bDU3LjI4IDE1LjM2YTEyLjggMTIuOCAwIDAgMSA2LjI3MiAyMC45MjhsLTE1OS4xNjggMTc5LjQ1NmExMi44IDEyLjggMCAwIDEtMjIuMTQ0LTUuODg4bC00OC4wNjQtMjM1LjA3MmExMi44IDEyLjggMCAwIDEgMTUuODA4LTE0LjkxMmw1Ny4yOCAxNS4zNnoiIGZpbGw9IiM3MDcwNzAiIHAtaWQ9IjEzNjEiPjwvcGF0aD48L3N2Zz4="
|
||||
alt="刷新">
|
||||
</div>
|
||||
<div class="wg-cap-wrap__btn">
|
||||
<el-button type="primary" @click="handleConfirmEvent">确认</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CaptchaPlus',
|
||||
mounted() {
|
||||
this.$emit('refresh')
|
||||
},
|
||||
props: {
|
||||
value: Boolean,
|
||||
width: {
|
||||
type: String,
|
||||
default: '300px'
|
||||
},
|
||||
calcPosType: {
|
||||
type: String,
|
||||
default: 'dom',
|
||||
validator: value => ['dom', 'screen'].includes(value)
|
||||
},
|
||||
maxDot: {
|
||||
type: Number,
|
||||
default: 5
|
||||
// validator: value => value > 10
|
||||
},
|
||||
imageBase64: String,
|
||||
thumbBase64: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dots: [],
|
||||
imageBase64Code: '',
|
||||
thumbBase64Code: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value() {
|
||||
this.dots = []
|
||||
this.imageBase64Code = ''
|
||||
this.thumbBase64Code = ''
|
||||
},
|
||||
imageBase64(val) {
|
||||
this.dots = []
|
||||
this.imageBase64Code = val
|
||||
},
|
||||
thumbBase64(val) {
|
||||
this.dots = []
|
||||
this.thumbBase64Code = val
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* @Description: 处理关闭事件
|
||||
*/
|
||||
handleCloseEvent() {
|
||||
this.$emit('close')
|
||||
// this.dots = []
|
||||
// this.imageBase64Code = ''
|
||||
// this.thumbBase64Code = ''
|
||||
},
|
||||
/**
|
||||
* @Description: 处理刷新事件
|
||||
*/
|
||||
handleRefreshEvent() {
|
||||
this.dots = []
|
||||
this.$emit('refresh')
|
||||
},
|
||||
/**
|
||||
* @Description: 处理确认事件
|
||||
*/
|
||||
handleConfirmEvent() {
|
||||
this.$emit('confirm', this.dots)
|
||||
},
|
||||
/**
|
||||
* @Description: 处理dot
|
||||
* @param ev
|
||||
*/
|
||||
handleClickPos(ev) {
|
||||
if (this.dots.length >= this.maxDot) {
|
||||
return
|
||||
}
|
||||
const e = ev || window.event
|
||||
e.preventDefault()
|
||||
const dom = e.currentTarget
|
||||
|
||||
const {domX, domY} = this.getDomXY(dom)
|
||||
// ===============================================
|
||||
// @notice 如 getDomXY 不准确可尝试使用 calcLocationLeft 或 calcLocationTop
|
||||
// const domX = this.calcLocationLeft(dom)
|
||||
// const domY = this.calcLocationTop(dom)
|
||||
// ===============================================
|
||||
let mouseX = (navigator.vendor === 'Netscape') ? e.pageX : e.x + document.body.offsetTop
|
||||
let mouseY = (navigator.vendor === 'Netscape') ? e.pageY : e.y + document.body.offsetTop
|
||||
// 兼容移动触摸事件
|
||||
if (e.touches && e.touches.length > 0) {
|
||||
mouseX = e.touches[0].clientX
|
||||
mouseY = e.touches[0].clientY
|
||||
} else {
|
||||
mouseX = e.clientX
|
||||
mouseY = e.clientY
|
||||
}
|
||||
|
||||
// 计算点击的相对位置
|
||||
const xPos = mouseX - domX
|
||||
const yPos = mouseY - domY
|
||||
|
||||
// 转整形
|
||||
const xp = parseInt(xPos.toString())
|
||||
const yp = parseInt(yPos.toString())
|
||||
|
||||
// 减去点的一半
|
||||
this.dots.push({
|
||||
x: xp - 11,
|
||||
y: yp - 11,
|
||||
index: this.dots.length + 1
|
||||
})
|
||||
return false
|
||||
},
|
||||
/**
|
||||
* @Description: 找到元素的屏幕位置
|
||||
* @param el
|
||||
*/
|
||||
calcLocationLeft(el) {
|
||||
let tmp = el.offsetLeft
|
||||
let val = el.offsetParent
|
||||
while (val != null) {
|
||||
tmp += val.offsetLeft
|
||||
val = val.offsetParent
|
||||
}
|
||||
return tmp
|
||||
},
|
||||
/**
|
||||
* @Description: 找到元素的屏幕位置
|
||||
* @param el
|
||||
*/
|
||||
calcLocationTop(el) {
|
||||
let tmp = el.offsetTop
|
||||
let val = el.offsetParent
|
||||
while (val != null) {
|
||||
tmp += val.offsetTop
|
||||
val = val.offsetParent
|
||||
}
|
||||
return tmp
|
||||
},
|
||||
/**
|
||||
* @Description: 找到元素的屏幕位置
|
||||
* @param dom
|
||||
*/
|
||||
getDomXY(dom) {
|
||||
let x = 0
|
||||
let y = 0
|
||||
if (dom.getBoundingClientRect) {
|
||||
let box = dom.getBoundingClientRect();
|
||||
let D = document.documentElement;
|
||||
x = box.left + Math.max(D.scrollLeft, document.body.scrollLeft) - D.clientLeft;
|
||||
y = box.top + Math.max(D.scrollTop, document.body.scrollTop) - D.clientTop
|
||||
} else {
|
||||
while (dom !== document.body) {
|
||||
x += dom.offsetLeft
|
||||
y += dom.offsetTop
|
||||
dom = dom.offsetParent
|
||||
}
|
||||
}
|
||||
return {
|
||||
domX: x,
|
||||
domY: y
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
.wg-cap-wrap {
|
||||
background: #ffffff;
|
||||
|
||||
-webkit-border-radius: 10px;
|
||||
-moz-border-radius: 10px;
|
||||
border-radius: 10px;
|
||||
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
.wg-cap-wrap__header {
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
font-size: 15px;
|
||||
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-webkit-align-items: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
padding-right: 5px;
|
||||
|
||||
em {
|
||||
padding: 0 3px;
|
||||
font-weight: bold;
|
||||
color: #3e7cff;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.wg-cap-wrap__image {
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.wg-cap-wrap__thumb {
|
||||
min-width: 150px;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.wg-cap-wrap__thumb.wg-cap-wrap__hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.wg-cap-wrap__body {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: -ms-flexbox;
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
background: #34383e;
|
||||
margin: auto;
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
|
||||
.wg-cap-wrap__picture {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wg-cap-wrap__picture.wg-cap-wrap__hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wg-cap-wrap__loading {
|
||||
position: absolute;
|
||||
z-index: 9;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
margin-left: -34px;
|
||||
margin-top: -34px;
|
||||
line-height: 68px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wg-cap-wrap__dot {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
color: #cedffe;
|
||||
background: #3e7cff;
|
||||
border: 2px solid #f7f9fb;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
-webkit-border-radius: 22px;
|
||||
-moz-border-radius: 22px;
|
||||
border-radius: 22px;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.wg-cap-wrap__footer {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
color: #34383e;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-webkit-align-items: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
padding-top: 15px;
|
||||
|
||||
.wg-cap-wrap__ico {
|
||||
flex: 1;
|
||||
|
||||
img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: #34383e;
|
||||
margin: 0 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.wg-cap-wrap__btn {
|
||||
display flex
|
||||
width: 120px;
|
||||
justify-content right
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
244
new-ui/projects/web/src/components/ChatMidJourney.vue
Normal file
244
new-ui/projects/web/src/components/ChatMidJourney.vue
Normal file
@@ -0,0 +1,244 @@
|
||||
<template>
|
||||
<div class="chat-line chat-line-mj" v-loading="loading">
|
||||
<div class="chat-line-inner">
|
||||
<div class="chat-icon">
|
||||
<img :src="icon" alt="User"/>
|
||||
</div>
|
||||
|
||||
<div class="chat-item">
|
||||
<div class="content">
|
||||
<div class="text" v-html="data.html"></div>
|
||||
<div class="images" v-if="data.image?.url !== ''">
|
||||
<el-image :src="data.image?.url"
|
||||
:zoom-rate="1.2"
|
||||
:preview-src-list="[data.image?.url]"
|
||||
fit="cover"
|
||||
:initial-index="0" loading="lazy">
|
||||
<template #placeholder>
|
||||
<div class="image-slot"
|
||||
:style="{height: height+'px', lineHeight:height+'px'}">
|
||||
正在加载图片<span class="dot">...</span></div>
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<Picture/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="opt" v-if="data.showOpt &&data.image?.hash !== ''">
|
||||
<div class="opt-line">
|
||||
<ul>
|
||||
<li><a @click="upscale(1)">U1</a></li>
|
||||
<li><a @click="upscale(2)">U2</a></li>
|
||||
<li><a @click="upscale(3)">U3</a></li>
|
||||
<li><a @click="upscale(4)">U4</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="opt-line">
|
||||
<ul>
|
||||
<li><a @click="variation(1)">V1</a></li>
|
||||
<li><a @click="variation(2)">V2</a></li>
|
||||
<li><a @click="variation(3)">V3</a></li>
|
||||
<li><a @click="variation(4)">V4</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bar" v-if="createdAt !== ''">
|
||||
<span class="bar-item"><el-icon><Clock/></el-icon> {{ createdAt }}</span>
|
||||
<span class="bar-item">tokens: {{ tokens }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, watch} from "vue";
|
||||
import {Clock, Picture} from "@element-plus/icons-vue";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {httpPost} from "@/utils/http";
|
||||
import {getSessionId} from "@/store/session";
|
||||
|
||||
const props = defineProps({
|
||||
content: Object,
|
||||
icon: String,
|
||||
chatId: String,
|
||||
roleId: Number,
|
||||
createdAt: String
|
||||
});
|
||||
|
||||
const data = ref(props.content)
|
||||
const tokens = ref(0)
|
||||
const cacheKey = "img_placeholder_height"
|
||||
const item = localStorage.getItem(cacheKey);
|
||||
const loading = ref(false)
|
||||
const height = ref(0)
|
||||
if (item) {
|
||||
height.value = parseInt(item)
|
||||
}
|
||||
if (data.value["image"]?.width > 0) {
|
||||
height.value = 350 * data.value["image"]?.height / data.value["image"]?.width
|
||||
localStorage.setItem(cacheKey, height.value)
|
||||
}
|
||||
data.value["showOpt"] = data.value["content"]?.indexOf("- Image #") === -1;
|
||||
// console.log(data.value)
|
||||
|
||||
watch(() => props.content, (newVal) => {
|
||||
data.value = newVal;
|
||||
});
|
||||
const emits = defineEmits(['disable-input', 'disable-input']);
|
||||
const upscale = (index) => {
|
||||
send('/api/mj/upscale', index)
|
||||
}
|
||||
|
||||
const variation = (index) => {
|
||||
send('/api/mj/variation', index)
|
||||
}
|
||||
|
||||
const send = (url, index) => {
|
||||
loading.value = true
|
||||
emits('disable-input')
|
||||
httpPost(url, {
|
||||
index: index,
|
||||
src: "chat",
|
||||
message_id: data.value?.["message_id"],
|
||||
message_hash: data.value?.["image"]?.hash,
|
||||
session_id: getSessionId(),
|
||||
prompt: data.value?.["prompt"],
|
||||
chat_id: props.chatId,
|
||||
role_id: props.roleId,
|
||||
icon: props.icon,
|
||||
}).then(() => {
|
||||
ElMessage.success("任务推送成功,请耐心等待任务执行...")
|
||||
loading.value = false
|
||||
}).catch(e => {
|
||||
ElMessage.error("任务推送失败:" + e.message)
|
||||
emits('disable-input')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.chat-line-mj {
|
||||
background-color #ffffff;
|
||||
justify-content: center;
|
||||
width 100%
|
||||
padding-bottom: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-bottom: 1px solid #d9d9e3;
|
||||
|
||||
.chat-line-inner {
|
||||
display flex;
|
||||
width 100%;
|
||||
max-width 900px;
|
||||
padding-left 10px;
|
||||
|
||||
.chat-icon {
|
||||
margin-right 20px;
|
||||
|
||||
img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
padding: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
position: relative;
|
||||
padding: 0 5px 0 0;
|
||||
overflow: hidden;
|
||||
|
||||
.content {
|
||||
word-break break-word;
|
||||
padding: 6px 10px;
|
||||
color #374151;
|
||||
font-size: var(--content-font-size);
|
||||
border-radius: 5px;
|
||||
overflow: auto;
|
||||
|
||||
.text {
|
||||
p:first-child {
|
||||
margin-top 0
|
||||
}
|
||||
}
|
||||
|
||||
.images {
|
||||
max-width 350px;
|
||||
|
||||
.el-image {
|
||||
border-radius 10px;
|
||||
|
||||
.image-slot {
|
||||
color #c1c1c1
|
||||
width 350px
|
||||
text-align center
|
||||
border-radius 10px;
|
||||
border 1px solid #e1e1e1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.opt {
|
||||
.opt-line {
|
||||
margin 6px 0
|
||||
|
||||
ul {
|
||||
display flex
|
||||
flex-flow row
|
||||
padding-left 10px
|
||||
|
||||
li {
|
||||
margin-right 10px
|
||||
|
||||
a {
|
||||
padding 6px 0
|
||||
width 64px
|
||||
text-align center
|
||||
border-radius 5px
|
||||
display block
|
||||
cursor pointer
|
||||
background-color #4E5058
|
||||
color #ffffff
|
||||
|
||||
&:hover {
|
||||
background-color #6D6F78
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bar {
|
||||
padding 10px;
|
||||
|
||||
.bar-item {
|
||||
background-color #f7f7f8;
|
||||
color #888
|
||||
padding 3px 5px;
|
||||
margin-right 10px;
|
||||
border-radius 5px;
|
||||
|
||||
.el-icon {
|
||||
position relative
|
||||
top 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
150
new-ui/projects/web/src/components/ChatPrompt.vue
Normal file
150
new-ui/projects/web/src/components/ChatPrompt.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<div class="chat-line chat-line-prompt">
|
||||
<div class="chat-line-inner">
|
||||
<div class="chat-icon">
|
||||
<img :src="icon" alt="User"/>
|
||||
</div>
|
||||
|
||||
<div class="chat-item">
|
||||
<div class="content" v-html="content"></div>
|
||||
<div class="bar" v-if="createdAt !== ''">
|
||||
<span class="bar-item"><el-icon><Clock/></el-icon> {{ createdAt }}</span>
|
||||
<span class="bar-item">Tokens: {{ finalTokens }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {defineComponent} from "vue"
|
||||
import {Clock} from "@element-plus/icons-vue";
|
||||
import {httpPost} from "@/utils/http";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ChatPrompt',
|
||||
components: {Clock},
|
||||
methods: {},
|
||||
props: {
|
||||
content: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'images/user-icon.png',
|
||||
},
|
||||
createdAt: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
tokens: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
model: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
finalTokens: this.tokens
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (!this.finalTokens) {
|
||||
httpPost("/api/chat/tokens", {text: this.content, model: this.model}).then(res => {
|
||||
this.finalTokens = res.data;
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.chat-line-prompt {
|
||||
background-color #ffffff;
|
||||
justify-content: center;
|
||||
width 100%
|
||||
padding-bottom: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-bottom: 1px solid #d9d9e3;
|
||||
|
||||
.chat-line-inner {
|
||||
display flex;
|
||||
width 100%;
|
||||
max-width 900px;
|
||||
padding-left 10px;
|
||||
|
||||
.chat-icon {
|
||||
margin-right 20px;
|
||||
|
||||
img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
padding: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
position: relative;
|
||||
padding: 0 5px 0 0;
|
||||
overflow: hidden;
|
||||
|
||||
.content {
|
||||
word-break break-word;
|
||||
padding: 6px 10px;
|
||||
color #374151;
|
||||
font-size: var(--content-font-size);
|
||||
border-radius: 5px;
|
||||
overflow: auto;
|
||||
|
||||
img {
|
||||
max-width: 600px;
|
||||
border-radius: 10px;
|
||||
margin 10px 0
|
||||
}
|
||||
|
||||
|
||||
a {
|
||||
color #20a0ff
|
||||
}
|
||||
|
||||
p {
|
||||
line-height 1.5
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
p:first-child {
|
||||
margin-top 0
|
||||
}
|
||||
}
|
||||
|
||||
.bar {
|
||||
padding 10px;
|
||||
|
||||
.bar-item {
|
||||
background-color #f7f7f8;
|
||||
color #888
|
||||
padding 3px 5px;
|
||||
margin-right 10px;
|
||||
border-radius 5px;
|
||||
|
||||
.el-icon {
|
||||
position relative
|
||||
top 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
248
new-ui/projects/web/src/components/ChatReply.vue
Normal file
248
new-ui/projects/web/src/components/ChatReply.vue
Normal file
@@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<div class="chat-line chat-line-reply">
|
||||
<div class="chat-line-inner">
|
||||
<div class="chat-icon">
|
||||
<img :src="icon" alt="ChatGPT">
|
||||
</div>
|
||||
|
||||
<div class="chat-item">
|
||||
<div class="content" v-html="content"></div>
|
||||
<div class="bar" v-if="createdAt !== ''">
|
||||
<span class="bar-item"><el-icon><Clock/></el-icon> {{ createdAt }}</span>
|
||||
<span class="bar-item">Tokens: {{ tokens }}</span>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
content="复制回答"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-button type="info" class="copy-reply" :data-clipboard-text="orgContent">
|
||||
<el-icon>
|
||||
<DocumentCopy/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {defineComponent} from "vue"
|
||||
import {Clock, DocumentCopy, Position} from "@element-plus/icons-vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ChatReply',
|
||||
components: {Position, Clock, DocumentCopy},
|
||||
props: {
|
||||
content: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
orgContent: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
createdAt: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
tokens: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'images/gpt-icon.png',
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
finalTokens: this.tokens
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.common-layout {
|
||||
.chat-line-reply {
|
||||
justify-content: center;
|
||||
background-color: rgba(247, 247, 248, 1);
|
||||
width 100%
|
||||
padding-bottom: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-bottom: 1px solid #d9d9e3;
|
||||
|
||||
.chat-line-inner {
|
||||
display flex;
|
||||
width 100%;
|
||||
max-width 900px;
|
||||
padding-left 10px;
|
||||
|
||||
.chat-icon {
|
||||
margin-right 20px;
|
||||
|
||||
img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
padding: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
position: relative;
|
||||
padding: 0 0 0 5px;
|
||||
overflow: hidden;
|
||||
|
||||
.content {
|
||||
min-height 20px;
|
||||
word-break break-word;
|
||||
padding: 6px 10px;
|
||||
color #374151;
|
||||
font-size: var(--content-font-size);
|
||||
border-radius: 5px;
|
||||
overflow auto;
|
||||
|
||||
a {
|
||||
color #20a0ff
|
||||
}
|
||||
|
||||
// control the image size in content
|
||||
|
||||
img {
|
||||
max-width: 600px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height 1.5
|
||||
|
||||
code {
|
||||
color #374151
|
||||
background-color #e7e7e8
|
||||
padding 0 3px;
|
||||
border-radius 5px;
|
||||
}
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
p:first-child {
|
||||
margin-top 0
|
||||
}
|
||||
|
||||
.code-container {
|
||||
position relative
|
||||
|
||||
.hljs {
|
||||
border-radius 10px
|
||||
line-height 1.5
|
||||
}
|
||||
|
||||
.copy-code-btn {
|
||||
position: absolute;
|
||||
right 10px
|
||||
top 10px
|
||||
cursor pointer
|
||||
font-size 12px
|
||||
color #c1c1c1
|
||||
|
||||
&:hover {
|
||||
color #20a0ff
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.lang-name {
|
||||
position absolute;
|
||||
right 10px
|
||||
bottom 50px
|
||||
padding 2px 6px 4px 6px
|
||||
background-color #444444
|
||||
border-radius 10px
|
||||
color #00e0e0
|
||||
}
|
||||
|
||||
|
||||
// 设置表格边框
|
||||
|
||||
table {
|
||||
width 100%
|
||||
margin-bottom 1rem
|
||||
color #212529
|
||||
border-collapse collapse;
|
||||
border 1px solid #dee2e6;
|
||||
background-color #ffffff
|
||||
|
||||
thead {
|
||||
th {
|
||||
border 1px solid #dee2e6
|
||||
vertical-align: bottom
|
||||
border-bottom: 2px solid #dee2e6
|
||||
padding 10px
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
border 1px solid #dee2e6
|
||||
padding 10px
|
||||
}
|
||||
}
|
||||
|
||||
// 代码快
|
||||
|
||||
blockquote {
|
||||
margin 0
|
||||
background-color: #ebfffe;
|
||||
padding: 0.8rem 1.5rem;
|
||||
border-left: 0.5rem solid;
|
||||
border-color: #026863;
|
||||
color: #2c3e50;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.bar {
|
||||
padding 10px;
|
||||
|
||||
.bar-item {
|
||||
background-color #e7e7e8;
|
||||
color #888
|
||||
padding 3px 5px;
|
||||
margin-right 10px;
|
||||
border-radius 5px;
|
||||
|
||||
.el-icon {
|
||||
position relative
|
||||
top 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-button {
|
||||
height 20px
|
||||
padding 5px 2px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.tool-box {
|
||||
font-size 16px;
|
||||
|
||||
.el-button {
|
||||
height 20px
|
||||
padding 5px 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
92
new-ui/projects/web/src/components/ConfigDialog.vue
Normal file
92
new-ui/projects/web/src/components/ConfigDialog.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
class="config-dialog"
|
||||
v-model="showDialog"
|
||||
:close-on-click-modal="true"
|
||||
:before-close="close"
|
||||
style="max-width: 600px"
|
||||
title="账户信息"
|
||||
>
|
||||
<div class="user-info" id="user-info">
|
||||
<el-form v-if="user.id" :model="user" label-width="150px">
|
||||
<el-form-item label="账户">
|
||||
<span>{{ user.username }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="剩余算力">
|
||||
<el-tag>{{ user['power'] }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="会员到期时间" v-if="user['expired_time'] > 0">
|
||||
<el-tag type="danger">{{ dateFormat(user['expired_time']) }}</el-tag>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, onMounted, ref} from "vue"
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {Plus} from "@element-plus/icons-vue";
|
||||
import Compressor from "compressorjs";
|
||||
import {dateFormat} from "@/utils/libs";
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
user: Object,
|
||||
models: Array,
|
||||
});
|
||||
|
||||
const showDialog = computed(() => {
|
||||
return props.show
|
||||
})
|
||||
const user = ref({
|
||||
username: '',
|
||||
nickname: '',
|
||||
avatar: '',
|
||||
calls: 0,
|
||||
tokens: 0,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 获取最新用户信息
|
||||
httpGet('/api/user/profile').then(res => {
|
||||
user.value = res.data
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取用户信息失败:" + e.message)
|
||||
});
|
||||
})
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const emits = defineEmits(['hide']);
|
||||
const close = function () {
|
||||
emits('hide', false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.config-dialog {
|
||||
.el-dialog {
|
||||
--el-dialog-width 90%;
|
||||
max-width 800px;
|
||||
|
||||
.el-dialog__body {
|
||||
overflow-y auto;
|
||||
|
||||
.user-info {
|
||||
position relative;
|
||||
|
||||
.el-message {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
.tip {
|
||||
color #c1c1c1
|
||||
font-size 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
100
new-ui/projects/web/src/components/CountDown.vue
Normal file
100
new-ui/projects/web/src/components/CountDown.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<!-- 倒计时组件 -->
|
||||
<div class="countdown">
|
||||
<el-tag size="large" :type="type">{{ timerStr }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
import {onMounted, ref, watch} from "vue";
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const props = defineProps({
|
||||
second: Number,
|
||||
type: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const emits = defineEmits(['timeout']);
|
||||
const counter = ref(props.second)
|
||||
const timerStr = ref("")
|
||||
const handler = ref(null)
|
||||
|
||||
watch(() => props.second, (newVal) => {
|
||||
counter.value = newVal
|
||||
resetTimer()
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
resetTimer()
|
||||
})
|
||||
|
||||
const resetTimer = () => {
|
||||
if (handler.value) {
|
||||
clearInterval(handler.value)
|
||||
}
|
||||
|
||||
counter.value = props.second
|
||||
formatTimer(counter.value)
|
||||
handler.value = setInterval(() => {
|
||||
formatTimer(counter.value)
|
||||
if (counter.value === 0) {
|
||||
clearInterval(handler.value)
|
||||
emits("timeout")
|
||||
}
|
||||
counter.value--
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const formatTimer = (secs) => {
|
||||
const timer = []
|
||||
let hour, min
|
||||
// 计算小时
|
||||
if (secs > 3600) {
|
||||
hour = Math.floor(secs / 3600)
|
||||
if (hour < 10) {
|
||||
hour = "0" + hour
|
||||
}
|
||||
secs = secs % 3600
|
||||
timer.push(hour + " 时 ")
|
||||
} else {
|
||||
timer.push("00 时 ")
|
||||
}
|
||||
// 计算分钟
|
||||
if (secs > 60) {
|
||||
min = Math.floor(secs / 60)
|
||||
if (min < 10) {
|
||||
min = "0" + min
|
||||
}
|
||||
secs = secs % 60
|
||||
timer.push(min + " 分 ")
|
||||
} else {
|
||||
timer.push("00 分 ")
|
||||
}
|
||||
// 计算秒数
|
||||
if (secs < 10) {
|
||||
secs = "0" + secs
|
||||
}
|
||||
timer.push(secs + " 秒")
|
||||
timerStr.value = timer.join("")
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
defineExpose({resetTimer})
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.countdown {
|
||||
display flex
|
||||
|
||||
.el-tag--large {
|
||||
.el-tag__content {
|
||||
font-size 14px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
199
new-ui/projects/web/src/components/FileSelect.vue
Normal file
199
new-ui/projects/web/src/components/FileSelect.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<el-container class="file-list-box">
|
||||
<el-tooltip class="box-item" effect="dark" content="打开文件管理中心">
|
||||
<el-button class="file-upload-img" @click="fetchFiles">
|
||||
<el-icon>
|
||||
<PictureFilled/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-dialog
|
||||
v-model="show"
|
||||
:close-on-click-modal="true"
|
||||
:show-close="true"
|
||||
:width="800"
|
||||
title="文件管理"
|
||||
>
|
||||
|
||||
<div class="file-list">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="3">
|
||||
<div class="grid-content">
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="afterRead"
|
||||
>
|
||||
<el-icon class="avatar-uploader-icon">
|
||||
<Plus/>
|
||||
</el-icon>
|
||||
</el-upload>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="3" v-for="file in fileList" :key="file.url">
|
||||
<div class="grid-content">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
:content="file.name"
|
||||
placement="top">
|
||||
<el-image :src="file.url" fit="cover" v-if="isImage(file.ext)" @click="insertURL(file.url)"/>
|
||||
<el-image :src="getFileIcon(file.ext)" fit="cover" v-else @click="insertURL(file.url)"/>
|
||||
</el-tooltip>
|
||||
|
||||
<div class="opt">
|
||||
<el-button type="danger" size="small" :icon="Delete" @click="removeFile(file)" circle/>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from "vue";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {Delete, PictureFilled, Plus} from "@element-plus/icons-vue";
|
||||
import {isImage, removeArrayItem} from "@/utils/libs";
|
||||
|
||||
const props = defineProps({
|
||||
userId: Number,
|
||||
});
|
||||
const emits = defineEmits(['selected']);
|
||||
const show = ref(false)
|
||||
const fileList = ref([])
|
||||
|
||||
const fetchFiles = () => {
|
||||
show.value = true
|
||||
httpGet("/api/upload/list").then(res => {
|
||||
fileList.value = res.data
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
|
||||
const getFileIcon = (ext) => {
|
||||
const files = {
|
||||
".docx": "doc.png",
|
||||
".doc": "doc.png",
|
||||
".xls": "xls.png",
|
||||
".xlsx": "xls.png",
|
||||
".ppt": "ppt.png",
|
||||
".pptx": "ppt.png",
|
||||
".md": "md.png",
|
||||
".pdf": "pdf.png",
|
||||
".sql": "sql.png"
|
||||
}
|
||||
if (files[ext]) {
|
||||
return '/images/ext/' + files[ext]
|
||||
}
|
||||
|
||||
return '/images/ext/file.png'
|
||||
}
|
||||
|
||||
const afterRead = (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file.file, file.name);
|
||||
// 执行上传操作
|
||||
httpPost('/api/upload', formData).then((res) => {
|
||||
fileList.value.unshift(res.data)
|
||||
ElMessage.success({message: "上传成功", duration: 500})
|
||||
}).catch((e) => {
|
||||
ElMessage.error('图片上传失败:' + e.message)
|
||||
})
|
||||
};
|
||||
|
||||
const removeFile = (file) => {
|
||||
httpGet('/api/upload/remove?id=' + file.id).then(() => {
|
||||
fileList.value = removeArrayItem(fileList.value, file, (v1, v2) => {
|
||||
return v1.id === v2.id
|
||||
})
|
||||
ElMessage.success("文件删除成功!")
|
||||
}).catch((e) => {
|
||||
ElMessage.error('文件删除失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const insertURL = (url) => {
|
||||
show.value = false
|
||||
emits('selected', url)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
|
||||
.file-list-box {
|
||||
.file-upload-img {
|
||||
padding: 8px 5px;
|
||||
border-radius: 6px;
|
||||
background: #19c37d;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.el-dialog {
|
||||
|
||||
.el-dialog__body {
|
||||
//padding 0
|
||||
|
||||
.file-list {
|
||||
|
||||
.grid-content {
|
||||
margin-bottom 10px
|
||||
position relative
|
||||
|
||||
.avatar-uploader {
|
||||
width 100%
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border 1px dashed #e1e1e1
|
||||
border-radius 6px
|
||||
|
||||
.el-upload {
|
||||
width 100%
|
||||
height 80px
|
||||
}
|
||||
}
|
||||
|
||||
.el-image {
|
||||
width 100%
|
||||
height 80px
|
||||
border 1px solid #ffffff
|
||||
border-radius 6px
|
||||
cursor pointer
|
||||
|
||||
&:hover {
|
||||
border 1px solid #20a0ff
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
color #20a0ff
|
||||
font-size 40px
|
||||
}
|
||||
|
||||
.opt {
|
||||
display none
|
||||
position absolute
|
||||
top 5px
|
||||
right 5px
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.opt {
|
||||
display block
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
36
new-ui/projects/web/src/components/FooterBar.vue
Normal file
36
new-ui/projects/web/src/components/FooterBar.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="footer">
|
||||
Powered by {{ author }} @
|
||||
<el-link type="primary" href="https://github.com/yangjian102621/chatgpt-plus" target="_blank">{{ title }}
|
||||
</el-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
import {ref} from "vue";
|
||||
|
||||
const title = ref(process.env.VUE_APP_TITLE)
|
||||
const author = ref('极客学长')
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
.container {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
display flex;
|
||||
justify-content center
|
||||
|
||||
.footer {
|
||||
max-width 400px;
|
||||
text-align center;
|
||||
font-size 14px;
|
||||
padding 20px;
|
||||
width 100%
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
94
new-ui/projects/web/src/components/InviteList.vue
Normal file
94
new-ui/projects/web/src/components/InviteList.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div class="invite-list" v-loading="loading">
|
||||
<el-row v-if="items.length > 0">
|
||||
<el-table :data="items" :row-key="row => row.id" table-layout="auto" border
|
||||
style="--el-table-border-color:#373C47;
|
||||
--el-table-tr-bg-color:#2D323B;
|
||||
--el-table-row-hover-bg-color:#373C47;
|
||||
--el-table-header-bg-color:#474E5C;
|
||||
--el-table-text-color:#d1d1d1">
|
||||
<el-table-column prop="username" label="用户"/>
|
||||
<el-table-column prop="invite_code" label="邀请码"/>
|
||||
<el-table-column prop="remark" label="邀请奖励"/>
|
||||
|
||||
<el-table-column label="注册时间">
|
||||
<template #default="scope">
|
||||
<span>{{ dateFormat(scope.row['created_at']) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-row>
|
||||
<el-empty :image-size="100" v-else/>
|
||||
<div class="pagination">
|
||||
<el-pagination v-if="total > 0" background
|
||||
layout="total,prev, pager, next"
|
||||
:hide-on-single-page="true"
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
@current-change="fetchData()"
|
||||
:total="total"/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref, watch} from "vue";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {dateFormat} from "@/utils/libs";
|
||||
import {DocumentCopy} from "@element-plus/icons-vue";
|
||||
import Clipboard from "clipboard";
|
||||
|
||||
const items = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const loading = ref(true)
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
const clipboard = new Clipboard('.copy-order-no');
|
||||
clipboard.on('success', () => {
|
||||
ElMessage.success("复制成功!");
|
||||
})
|
||||
|
||||
clipboard.on('error', () => {
|
||||
ElMessage.error('复制失败!');
|
||||
})
|
||||
})
|
||||
|
||||
// 获取数据
|
||||
const fetchData = () => {
|
||||
httpPost('/api/invite/list', {page: page.value, page_size: pageSize.value}).then((res) => {
|
||||
if (res.data) {
|
||||
items.value = res.data.items
|
||||
total.value = res.data.total
|
||||
page.value = res.data.page
|
||||
pageSize.value = res.data.page_size
|
||||
}
|
||||
loading.value = false
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取数据失败:" + e.message);
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
.invite-list {
|
||||
.pagination {
|
||||
margin: 20px 0 0 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.copy-order-no {
|
||||
cursor pointer
|
||||
position relative
|
||||
left 6px
|
||||
top 2px
|
||||
color #20a0ff
|
||||
}
|
||||
}
|
||||
</style>
|
||||
81
new-ui/projects/web/src/components/ItemList.vue
Normal file
81
new-ui/projects/web/src/components/ItemList.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="list-box" ref="container">
|
||||
<div class="list-inner">
|
||||
<div
|
||||
class="list-item"
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
:style="{width:itemWidth + 'px'}"
|
||||
>
|
||||
<div class="item-inner" :style="{padding: gap/2+'px'}">
|
||||
<div class="item-wrapper">
|
||||
<slot :item="item" :index="index" :width="itemWidth"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 列表组件
|
||||
import {onMounted, ref} from "vue";
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
gap: {
|
||||
type: Number,
|
||||
default: 12
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 240
|
||||
}
|
||||
});
|
||||
|
||||
const container = ref(null)
|
||||
const itemWidth = ref(props.width)
|
||||
|
||||
onMounted(() => {
|
||||
computeSize()
|
||||
})
|
||||
|
||||
const computeSize = () => {
|
||||
const w = container.value.offsetWidth - 10 // 减去滚动条的宽度
|
||||
let cols = Math.floor(w / props.width)
|
||||
itemWidth.value = Math.floor(w / cols)
|
||||
}
|
||||
|
||||
window.onresize = () => {
|
||||
computeSize()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
|
||||
.list-box {
|
||||
|
||||
.list-inner {
|
||||
display flex
|
||||
flex-wrap wrap
|
||||
|
||||
.list-item {
|
||||
.item-inner {
|
||||
display flex
|
||||
|
||||
.item-wrapper {
|
||||
height 100%
|
||||
width 100%
|
||||
display flex
|
||||
justify-content center
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
116
new-ui/projects/web/src/components/LoginDialog.vue
Normal file
116
new-ui/projects/web/src/components/LoginDialog.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
class="login-dialog"
|
||||
v-model="showDialog"
|
||||
:close-on-click-modal="true"
|
||||
:show-close="true"
|
||||
:before-close="close"
|
||||
:width="400"
|
||||
title="用户登录"
|
||||
>
|
||||
<div class="form">
|
||||
<el-form label-width="75px">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label">
|
||||
<el-icon>
|
||||
<User/>
|
||||
</el-icon>
|
||||
<span>账号</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<el-input v-model="username" size="large" placeholder="手机号码"/>
|
||||
</template>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label">
|
||||
<el-icon>
|
||||
<Lock/>
|
||||
</el-icon>
|
||||
<span>密码</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<el-input v-model="password" type="password" size="large" placeholder="密码"/>
|
||||
</template>
|
||||
</el-form-item>
|
||||
|
||||
<div class="login-btn">
|
||||
<el-button type="primary" @click="submit" size="large" round>登录</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, ref} from "vue"
|
||||
import {httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {setUserToken} from "@/store/session";
|
||||
import {validateMobile} from "@/utils/validate";
|
||||
import {Lock, User} from "@element-plus/icons-vue";
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
});
|
||||
const showDialog = computed(() => {
|
||||
return props.show
|
||||
})
|
||||
const username = ref("")
|
||||
const password = ref("")
|
||||
// eslint-disable-next-line no-undef
|
||||
const emits = defineEmits(['hide']);
|
||||
const submit = function () {
|
||||
if (!validateMobile(username.value)) {
|
||||
return ElMessage.error('请输入合法的手机号');
|
||||
}
|
||||
if (password.value.trim() === '') {
|
||||
return ElMessage.error('请输入密码');
|
||||
}
|
||||
|
||||
httpPost('/api/user/login', {username: username.value.trim(), password: password.value.trim()}).then((res) => {
|
||||
setUserToken(res.data)
|
||||
ElMessage.success("登录成功!")
|
||||
emits("hide")
|
||||
}).catch((e) => {
|
||||
ElMessage.error('登录失败,' + e.message)
|
||||
})
|
||||
}
|
||||
const close = function () {
|
||||
emits('hide', false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.login-dialog {
|
||||
border-radius 20px
|
||||
|
||||
.label {
|
||||
padding-top 3px
|
||||
|
||||
.el-icon {
|
||||
position relative
|
||||
font-size 20px
|
||||
margin-right 6px
|
||||
top 4px
|
||||
}
|
||||
|
||||
span {
|
||||
font-size 16px
|
||||
}
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
text-align center
|
||||
padding-top 10px
|
||||
|
||||
.el-button {
|
||||
width 50%
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
94
new-ui/projects/web/src/components/PasswordDialog.vue
Normal file
94
new-ui/projects/web/src/components/PasswordDialog.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
class="password-dialog"
|
||||
v-model="showDialog"
|
||||
:close-on-click-modal="true"
|
||||
:show-close="true"
|
||||
style="max-width: 600px"
|
||||
:before-close="close"
|
||||
title="修改密码"
|
||||
>
|
||||
<div class="form" id="password-form">
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="原始密码">
|
||||
<el-input v-model="form['old_pass']" type="password"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码">
|
||||
<el-input v-model="form['password']" type="password"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码">
|
||||
<el-input v-model="form['repass']" type="password"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="close">关闭</el-button>
|
||||
<el-button type="primary" @click="save">
|
||||
保存
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, ref} from "vue"
|
||||
import {httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
});
|
||||
const showDialog = computed(() => {
|
||||
return props.show
|
||||
})
|
||||
const form = ref({})
|
||||
const emits = defineEmits(['hide', 'logout']);
|
||||
const save = function () {
|
||||
if (!form.value['password'] || form.value['password'].length < 8) {
|
||||
return ElMessage.error({message: "密码的长度为8-16个字符", appendTo: "#password-form"});
|
||||
}
|
||||
if (form.value['repass'] !== form.value['password']) {
|
||||
return ElMessage.error({message: '两次输入密码不一致', appendTo: '#password-form'});
|
||||
}
|
||||
httpPost('/api/user/password', form.value).then(() => {
|
||||
ElMessage.success({
|
||||
message: '更新成功',
|
||||
appendTo: '#password-form',
|
||||
duration: 1000,
|
||||
onClose: () => emits('logout', false)
|
||||
})
|
||||
}).catch((e) => {
|
||||
ElMessage.error({
|
||||
message: '更新失败,' + e.message,
|
||||
appendTo: '#password-form'
|
||||
})
|
||||
})
|
||||
}
|
||||
const close = function () {
|
||||
emits('hide', false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.password-dialog {
|
||||
.el-dialog {
|
||||
--el-dialog-width 90%;
|
||||
max-width 650px;
|
||||
|
||||
.el-dialog__body {
|
||||
overflow-y auto;
|
||||
|
||||
.form {
|
||||
position relative;
|
||||
|
||||
.el-message {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
97
new-ui/projects/web/src/components/ResetAccount.vue
Normal file
97
new-ui/projects/web/src/components/ResetAccount.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="showDialog"
|
||||
:close-on-click-modal="true"
|
||||
style="max-width: 600px"
|
||||
:before-close="close"
|
||||
:title="title"
|
||||
>
|
||||
<div class="form" id="bind-mobile-form">
|
||||
<el-alert v-if="username !== ''" type="info" show-icon :closable="false" style="margin-bottom: 20px;">
|
||||
<p>当前绑定账号:{{ username }},只允许使绑定有效的手机号或者邮箱地址作为登录账号。</p>
|
||||
</el-alert>
|
||||
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="新账号">
|
||||
<el-input v-model="form.username"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="验证码">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="16">
|
||||
<el-input v-model="form.code" maxlength="6"/>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<send-msg size="" :receiver="form.username"/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button type="primary" @click="save">
|
||||
提交绑定
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, ref} from "vue";
|
||||
import SendMsg from "@/components/SendMsg.vue";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {httpPost} from "@/utils/http";
|
||||
import {validateEmail, validateMobile} from "@/utils/validate";
|
||||
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
username: String
|
||||
});
|
||||
|
||||
const showDialog = computed(() => {
|
||||
return props.show
|
||||
})
|
||||
|
||||
const title = ref('重置登录账号')
|
||||
const form = ref({
|
||||
username: '',
|
||||
code: ''
|
||||
})
|
||||
|
||||
const emits = defineEmits(['hide']);
|
||||
|
||||
const save = () => {
|
||||
if (!validateMobile(form.value.username) && !validateEmail(form.value.username)) {
|
||||
return ElMessage.error("请输入合法的手机号/邮箱地址")
|
||||
}
|
||||
if (form.value.code === '') {
|
||||
return ElMessage.error("请输入验证码");
|
||||
}
|
||||
|
||||
httpPost('/api/user/bind/username', form.value).then(() => {
|
||||
ElMessage.success({
|
||||
message: '绑定成功',
|
||||
duration: 1000,
|
||||
onClose: () => emits('hide', false)
|
||||
})
|
||||
}).catch(e => {
|
||||
ElMessage.error("绑定失败:" + e.message);
|
||||
})
|
||||
}
|
||||
|
||||
const close = function () {
|
||||
emits('hide', false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
#bind-mobile-form {
|
||||
.el-form-item__content {
|
||||
.el-row {
|
||||
width 100%
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
108
new-ui/projects/web/src/components/ResetPass.vue
Normal file
108
new-ui/projects/web/src/components/ResetPass.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div class="reset-pass">
|
||||
<el-dialog
|
||||
v-model="showDialog"
|
||||
:close-on-click-modal="true"
|
||||
width="540px"
|
||||
:before-close="close"
|
||||
:title="title"
|
||||
>
|
||||
<div class="form">
|
||||
|
||||
<el-form :model="form" label-width="120px" label-position="left">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="form.username" placeholder="手机号/邮箱地址"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="验证码">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="16">
|
||||
<el-input v-model="form.code" maxlength="6"/>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<send-msg size="" :receiver="form.username"/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码">
|
||||
<el-input v-model="form.password" type="password"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="重复密码">
|
||||
<el-input v-model="form.repass" type="password"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="save" round>
|
||||
重置密码
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, ref} from "vue";
|
||||
import SendMsg from "@/components/SendMsg.vue";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {httpPost} from "@/utils/http";
|
||||
import {validateEmail, validateMobile} from "@/utils/validate";
|
||||
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
mobile: String
|
||||
});
|
||||
|
||||
const showDialog = computed(() => {
|
||||
return props.show
|
||||
})
|
||||
|
||||
const title = ref('重置密码')
|
||||
const form = ref({
|
||||
username: '',
|
||||
code: '',
|
||||
password: '',
|
||||
repass: ''
|
||||
})
|
||||
|
||||
const emits = defineEmits(['hide']);
|
||||
|
||||
const save = () => {
|
||||
if (!validateMobile(form.value.username) && !validateEmail(form.value.username)) {
|
||||
return ElMessage.error("请输入正确的手机号码/邮箱地址");
|
||||
}
|
||||
if (form.value.code === '') {
|
||||
return ElMessage.error("请输入验证码");
|
||||
}
|
||||
if (form.value.repass !== form.value.password) {
|
||||
return ElMessage.error("两次输入密码不一致");
|
||||
}
|
||||
|
||||
httpPost('/api/user/resetPass', form.value).then(() => {
|
||||
ElMessage.success({
|
||||
message: '重置密码成功', duration: 1000, onClose: () => emits('hide', false)
|
||||
})
|
||||
}).catch(e => {
|
||||
ElMessage.error("重置密码失败:" + e.message);
|
||||
})
|
||||
}
|
||||
|
||||
const close = function () {
|
||||
emits('hide', false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.reset-pass {
|
||||
.form {
|
||||
padding 10px 40px
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
text-align center
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
86
new-ui/projects/web/src/components/RewardVerify.vue
Normal file
86
new-ui/projects/web/src/components/RewardVerify.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="showDialog"
|
||||
:close-on-click-modal="true"
|
||||
:show-close="mobile !== ''"
|
||||
:before-close="close"
|
||||
:width="450"
|
||||
:title="title"
|
||||
>
|
||||
<div class="form" id="bind-mobile-form">
|
||||
<el-alert v-if="mobile !== ''" type="info" show-icon :closable="false" style="margin-bottom: 20px;">
|
||||
<p>请输入您参与众筹的 <strong style="color:#F56C6C">微信支付转账单号</strong> 兑换相应的对话次数。</p>
|
||||
</el-alert>
|
||||
|
||||
<el-form :model="form">
|
||||
<el-form-item label="转账单号">
|
||||
<el-input v-model="form.tx_id"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-form :model="form">
|
||||
<el-form-item label="兑换类别">
|
||||
<el-radio-group v-model="form.type">
|
||||
<el-radio label="chat" border>对话聊天</el-radio>
|
||||
<el-radio label="img" border>AI绘图</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button type="primary" @click="save">
|
||||
确认核销
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, ref} from "vue";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {httpPost} from "@/utils/http";
|
||||
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
mobile: String
|
||||
});
|
||||
|
||||
const showDialog = computed(() => {
|
||||
return props.show
|
||||
})
|
||||
|
||||
const title = ref('众筹码核销')
|
||||
const form = ref({
|
||||
tx_id: '',
|
||||
type: 'chat'
|
||||
})
|
||||
|
||||
const emits = defineEmits(['hide']);
|
||||
|
||||
const save = () => {
|
||||
if (form.value.tx_id === '') {
|
||||
return ElMessage.error({message: "请输入微信支付转账单号"});
|
||||
}
|
||||
|
||||
httpPost('/api/reward/verify', form.value).then(() => {
|
||||
ElMessage.success({
|
||||
message: '核销成功',
|
||||
duration: 1000,
|
||||
onClose: () => location.reload()
|
||||
})
|
||||
}).catch(e => {
|
||||
ElMessage.error({message: "核销失败:" + e.message});
|
||||
})
|
||||
}
|
||||
|
||||
const close = function () {
|
||||
emits('hide', false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
138
new-ui/projects/web/src/components/SendMsg.vue
Normal file
138
new-ui/projects/web/src/components/SendMsg.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<el-container class="captcha-box">
|
||||
<el-button type="primary" class="send-btn" :size="props.size" :disabled="!canSend" @click="loadCaptcha" plain>
|
||||
{{ btnText }}
|
||||
</el-button>
|
||||
|
||||
<el-dialog
|
||||
v-model="showCaptcha"
|
||||
:close-on-click-modal="true"
|
||||
:show-close="false"
|
||||
style="width:90%;max-width: 360px;"
|
||||
>
|
||||
<captcha-plus
|
||||
:max-dot="maxDot"
|
||||
:image-base64="imageBase64"
|
||||
:thumb-base64="thumbBase64"
|
||||
width="300"
|
||||
@close="showCaptcha = false"
|
||||
@refresh="handleRequestCaptCode"
|
||||
@confirm="handleConfirm"
|
||||
/>
|
||||
|
||||
</el-dialog>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 发送短信验证码组件
|
||||
import {ref} from "vue";
|
||||
import lodash from 'lodash'
|
||||
import {validateEmail, validateMobile} from "@/utils/validate";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import CaptchaPlus from "@/components/CaptchaPlus.vue";
|
||||
|
||||
const props = defineProps({
|
||||
receiver: String,
|
||||
size: String,
|
||||
});
|
||||
const btnText = ref('发送验证码')
|
||||
const canSend = ref(true)
|
||||
const showCaptcha = ref(false)
|
||||
const maxDot = ref(5)
|
||||
const imageBase64 = ref('')
|
||||
const thumbBase64 = ref('')
|
||||
const captKey = ref('')
|
||||
const dots = ref(null)
|
||||
|
||||
const handleRequestCaptCode = () => {
|
||||
|
||||
httpGet('/api/captcha/get').then(res => {
|
||||
const data = res.data
|
||||
imageBase64.value = data.image
|
||||
thumbBase64.value = data.thumb
|
||||
captKey.value = data.key
|
||||
}).catch(e => {
|
||||
ElMessage.error('获取人机验证数据失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const handleConfirm = (dots) => {
|
||||
if (lodash.size(dots) <= 0) {
|
||||
return ElMessage.error('请进行人机验证再操作')
|
||||
}
|
||||
|
||||
let dotArr = []
|
||||
lodash.forEach(dots, (dot) => {
|
||||
dotArr.push(dot.x, dot.y)
|
||||
})
|
||||
dots.value = dotArr.join(',')
|
||||
httpPost('/api/captcha/check', {
|
||||
dots: dots.value,
|
||||
key: captKey.value
|
||||
}).then(() => {
|
||||
// ElMessage.success('人机验证成功')
|
||||
showCaptcha.value = false
|
||||
sendMsg()
|
||||
}).catch(() => {
|
||||
ElMessage.error('人机验证失败')
|
||||
handleRequestCaptCode()
|
||||
})
|
||||
}
|
||||
|
||||
const loadCaptcha = () => {
|
||||
if (!validateMobile(props.receiver) && !validateEmail(props.receiver)) {
|
||||
return ElMessage.error("请输入合法的手机号/邮箱地址")
|
||||
}
|
||||
|
||||
showCaptcha.value = true
|
||||
handleRequestCaptCode() // 每次点开都刷新验证码
|
||||
}
|
||||
|
||||
const sendMsg = () => {
|
||||
if (!canSend.value) {
|
||||
return
|
||||
}
|
||||
|
||||
canSend.value = false
|
||||
httpPost('/api/sms/code', {receiver: props.receiver, key: captKey.value, dots: dots.value}).then(() => {
|
||||
ElMessage.success('验证码发送成功')
|
||||
let time = 120
|
||||
btnText.value = time
|
||||
const handler = setInterval(() => {
|
||||
time = time - 1
|
||||
if (time <= 0) {
|
||||
clearInterval(handler)
|
||||
btnText.value = '重新发送'
|
||||
canSend.value = true
|
||||
} else {
|
||||
btnText.value = time
|
||||
}
|
||||
}, 1000)
|
||||
}).catch(e => {
|
||||
canSend.value = true
|
||||
ElMessage.error('验证码发送失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
|
||||
.captcha-box {
|
||||
.send-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-dialog {
|
||||
.el-dialog__header {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
//padding 0
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
107
new-ui/projects/web/src/components/UserOrder.vue
Normal file
107
new-ui/projects/web/src/components/UserOrder.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="user-bill" v-loading="loading">
|
||||
<el-row v-if="items.length > 0">
|
||||
<el-table :data="items" :row-key="row => row.id" table-layout="auto" border
|
||||
style="--el-table-border-color:#373C47;
|
||||
--el-table-tr-bg-color:#2D323B;
|
||||
--el-table-row-hover-bg-color:#373C47;
|
||||
--el-table-header-bg-color:#474E5C;
|
||||
--el-table-text-color:#d1d1d1">
|
||||
<el-table-column prop="order_no" label="订单号">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.order_no }}</span>
|
||||
<el-icon class="copy-order-no" :data-clipboard-text="scope.row.order_no">
|
||||
<DocumentCopy/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="subject" label="产品名称"/>
|
||||
<el-table-column prop="amount" label="订单金额"/>
|
||||
<el-table-column label="订单算力">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.remark?.power }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="支付时间">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row['pay_time']">{{ dateFormat(scope.row['pay_time']) }}</span>
|
||||
<el-tag v-else>未支付</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-row>
|
||||
<el-empty :image-size="100" v-else/>
|
||||
<div class="pagination">
|
||||
<el-pagination v-if="total > 0" background
|
||||
layout="total,prev, pager, next"
|
||||
:hide-on-single-page="true"
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
@current-change="fetchData()"
|
||||
:total="total"/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref, watch} from "vue";
|
||||
import {httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {dateFormat} from "@/utils/libs";
|
||||
import {DocumentCopy} from "@element-plus/icons-vue";
|
||||
import Clipboard from "clipboard";
|
||||
|
||||
const items = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const loading = ref(true)
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
const clipboard = new Clipboard('.copy-order-no');
|
||||
clipboard.on('success', () => {
|
||||
ElMessage.success("复制成功!");
|
||||
})
|
||||
|
||||
clipboard.on('error', () => {
|
||||
ElMessage.error('复制失败!');
|
||||
})
|
||||
})
|
||||
|
||||
// 获取数据
|
||||
const fetchData = () => {
|
||||
httpPost('/api/order/list', {page: page.value, page_size: pageSize.value}).then((res) => {
|
||||
if (res.data) {
|
||||
items.value = res.data.items
|
||||
total.value = res.data.total
|
||||
page.value = res.data.page
|
||||
pageSize.value = res.data.page_size
|
||||
}
|
||||
loading.value = false
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取数据失败:" + e.message);
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
.user-bill {
|
||||
.pagination {
|
||||
margin: 20px 0 0 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.copy-order-no {
|
||||
cursor pointer
|
||||
position relative
|
||||
left 6px
|
||||
top 2px
|
||||
color #20a0ff
|
||||
}
|
||||
}
|
||||
</style>
|
||||
125
new-ui/projects/web/src/components/UserProfile.vue
Normal file
125
new-ui/projects/web/src/components/UserProfile.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div class="user-info" id="user-info">
|
||||
<el-form v-if="user.id" :model="user" label-width="150px">
|
||||
<el-row>
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="afterRead"
|
||||
accept=".png,.jpg,.jpeg,.bmp"
|
||||
>
|
||||
<el-avatar v-if="user.avatar" :src="user.avatar" shape="circle" :size="100"/>
|
||||
<el-icon v-else class="avatar-uploader-icon">
|
||||
<Plus/>
|
||||
</el-icon>
|
||||
</el-upload>
|
||||
</el-row>
|
||||
<el-form-item label="昵称">
|
||||
<el-input v-model="user['nickname']"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="账号">
|
||||
<span>{{ user.username }}</span>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="light"
|
||||
content="您已经是 VIP 会员"
|
||||
placement="right"
|
||||
>
|
||||
<el-image v-if="user.vip" :src="vipImg" style="height: 25px;margin-left: 10px"/>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item label="剩余算力">
|
||||
<el-tag>{{ user['power'] }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="会员到期时间" v-if="user['expired_time'] > 0">
|
||||
<el-tag type="danger">{{ dateFormat(user['expired_time']) }}</el-tag>
|
||||
</el-form-item>
|
||||
|
||||
<el-row class="opt-line">
|
||||
<el-button color="#47fff1" :dark="false" round @click="save">保存</el-button>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref} from "vue"
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {Plus} from "@element-plus/icons-vue";
|
||||
import Compressor from "compressorjs";
|
||||
import {dateFormat} from "@/utils/libs";
|
||||
import {checkSession} from "@/action/session";
|
||||
|
||||
const user = ref({
|
||||
vip: false,
|
||||
username: '',
|
||||
nickname: '',
|
||||
avatar: '',
|
||||
mobile: '',
|
||||
power: 0,
|
||||
})
|
||||
const vipImg = ref("/images/vip.png")
|
||||
|
||||
onMounted(() => {
|
||||
checkSession().then(() => {
|
||||
// 获取最新用户信息
|
||||
httpGet('/api/user/profile').then(res => {
|
||||
user.value = res.data
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取用户信息失败:" + e.message)
|
||||
});
|
||||
}).catch(e => {
|
||||
console.log(e)
|
||||
})
|
||||
})
|
||||
|
||||
const afterRead = (file) => {
|
||||
// 压缩图片并上传
|
||||
new Compressor(file.file, {
|
||||
quality: 0.6,
|
||||
success(result) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', result, result.name);
|
||||
// 执行上传操作
|
||||
httpPost('/api/upload', formData).then((res) => {
|
||||
user.value.avatar = res.data.url
|
||||
ElMessage.success({message: "上传成功", duration: 500})
|
||||
}).catch((e) => {
|
||||
ElMessage.error('图片上传失败:' + e.message)
|
||||
})
|
||||
},
|
||||
error(err) {
|
||||
console.log(err.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
httpPost('/api/user/profile/update', user.value).then(() => {
|
||||
ElMessage.success({message: '更新成功', duration: 500})
|
||||
}).catch((e) => {
|
||||
ElMessage.error('更新失败:' + e.message)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.user-info {
|
||||
padding 20px
|
||||
|
||||
.el-row {
|
||||
justify-content center
|
||||
margin-bottom 10px
|
||||
}
|
||||
|
||||
.opt-line {
|
||||
padding-top 20px
|
||||
|
||||
.el-button {
|
||||
width 100%
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
167
new-ui/projects/web/src/components/Welcome.vue
Normal file
167
new-ui/projects/web/src/components/Welcome.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<div class="container">
|
||||
<h1 class="title">{{ title }}</h1>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<div class="grid-content">
|
||||
<div class="item-title">
|
||||
<div><i class="iconfont icon-quick-start"></i></div>
|
||||
<div>小试牛刀</div>
|
||||
</div>
|
||||
|
||||
<div class="list-box">
|
||||
<ul>
|
||||
<li v-for="item in samples" :key="item"><a @click="send(item)">{{ item }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="grid-content">
|
||||
<div class="item-title">
|
||||
<div><i class="iconfont icon-plugin"></i></div>
|
||||
<div>插件增强</div>
|
||||
</div>
|
||||
|
||||
<div class="list-box">
|
||||
<ul>
|
||||
<li v-for="item in plugins" :key="item.value"><a @click="send(item.value)">{{ item.text }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="grid-content">
|
||||
<div class="item-title">
|
||||
<div><i class="iconfont icon-control"></i></div>
|
||||
<div>能力扩展</div>
|
||||
</div>
|
||||
|
||||
<div class="list-box">
|
||||
<ul>
|
||||
<li v-for="item in capabilities" :key="item">
|
||||
<span v-if="item.value === ''">{{ item.text }}</span>
|
||||
<a @click="send(item.value)" v-else>{{ item.text }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
import {onMounted, ref} from "vue";
|
||||
import {httpGet} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
|
||||
const title = ref(process.env.VUE_APP_TITLE)
|
||||
|
||||
const samples = ref([
|
||||
"用小学生都能听懂的术语解释什么是量子纠缠",
|
||||
"能给一位6岁男孩的生日会提供一些创造性的建议吗?",
|
||||
"如何用 Go 语言实现支持代理 Http client 请求?"
|
||||
])
|
||||
|
||||
const plugins = ref([
|
||||
{
|
||||
value: "今日早报",
|
||||
text: "今日早报:获取当天全球的热门新闻事件列表"
|
||||
},
|
||||
{
|
||||
value: "微博热搜",
|
||||
text: "微博热搜:新浪微博热搜榜,微博当日热搜榜单"
|
||||
},
|
||||
{
|
||||
value: "今日头条",
|
||||
text: "今日头条:给用户推荐当天的头条新闻,周榜热文"
|
||||
}
|
||||
])
|
||||
|
||||
const capabilities = ref([
|
||||
{
|
||||
text: "轻松扮演翻译专家,程序员,AI 女友,文案高手...",
|
||||
value: ""
|
||||
},
|
||||
{
|
||||
text: "国产大语言模型支持,百度文心,科大讯飞,ChatGLM...",
|
||||
value: ""
|
||||
},
|
||||
{
|
||||
text: "绘画:马斯克开拖拉机,20世纪,中国农村。3:2",
|
||||
value: "绘画:马斯克开拖拉机,20世纪,中国农村。3:2"
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
httpGet("/api/admin/config/get?key=system").then(res => {
|
||||
title.value = res.data.title
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取系统配置失败:" + e.message)
|
||||
})
|
||||
})
|
||||
|
||||
const emits = defineEmits(['send']);
|
||||
const send = (text) => {
|
||||
emits('send', text)
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="stylus">
|
||||
.welcome {
|
||||
text-align center
|
||||
display flex
|
||||
justify-content center
|
||||
margin-top 8vh
|
||||
|
||||
.container {
|
||||
max-width 768px;
|
||||
width 100%
|
||||
|
||||
.title {
|
||||
font-size: 2.25rem
|
||||
line-height: 2.5rem
|
||||
font-weight 600
|
||||
margin-bottom: 4rem
|
||||
}
|
||||
|
||||
.grid-content {
|
||||
.item-title {
|
||||
div {
|
||||
padding 6px 10px;
|
||||
|
||||
.iconfont {
|
||||
font-size 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-box {
|
||||
ul {
|
||||
padding 10px;
|
||||
|
||||
li {
|
||||
font-size 14px;
|
||||
padding .75rem
|
||||
border-radius 5px;
|
||||
background-color: rgba(247, 247, 248, 1);
|
||||
|
||||
line-height 1.5
|
||||
color #666666
|
||||
|
||||
a {
|
||||
cursor pointer
|
||||
display block
|
||||
width 100%
|
||||
}
|
||||
margin-top 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
271
new-ui/projects/web/src/components/mobile/ChatMidJourney.vue
Normal file
271
new-ui/projects/web/src/components/mobile/ChatMidJourney.vue
Normal file
@@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<div class="mobile-message-mj">
|
||||
<div class="chat-icon">
|
||||
<van-image :src="icon"/>
|
||||
</div>
|
||||
|
||||
<div class="chat-item">
|
||||
<div class="triangle"></div>
|
||||
<div class="content-box">
|
||||
<div class="content">
|
||||
<div class="content-inner">
|
||||
<div class="text" v-html="data.html"></div>
|
||||
<div class="images" v-if="data.image?.url !== ''">
|
||||
<el-image :src="data.image?.url"
|
||||
:zoom-rate="1.2"
|
||||
:preview-src-list="[data.image?.url]"
|
||||
fit="cover"
|
||||
:initial-index="0" loading="lazy">
|
||||
<template #placeholder>
|
||||
<div class="image-slot"
|
||||
:style="{height: height+'px', lineHeight:height+'px'}">
|
||||
正在加载图片<span class="dot">...</span></div>
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<Picture/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="opt" v-if="data.showOpt &&data.image?.hash !== ''">
|
||||
<div class="opt-line">
|
||||
<ul>
|
||||
<li><a @click="upscale(1)">U1</a></li>
|
||||
<li><a @click="upscale(2)">U2</a></li>
|
||||
<li><a @click="upscale(3)">U3</a></li>
|
||||
<li><a @click="upscale(4)">U4</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="opt-line">
|
||||
<ul>
|
||||
<li><a @click="variation(1)">V1</a></li>
|
||||
<li><a @click="variation(2)">V2</a></li>
|
||||
<li><a @click="variation(3)">V3</a></li>
|
||||
<li><a @click="variation(4)">V4</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, watch} from "vue";
|
||||
import {Picture} from "@element-plus/icons-vue";
|
||||
import {httpPost} from "@/utils/http";
|
||||
import {getSessionId} from "@/store/session";
|
||||
import {showNotify} from "vant";
|
||||
|
||||
const props = defineProps({
|
||||
content: Object,
|
||||
icon: String,
|
||||
chatId: String,
|
||||
roleId: Number,
|
||||
createdAt: String
|
||||
});
|
||||
|
||||
const data = ref(props.content)
|
||||
const cacheKey = "img_placeholder_height"
|
||||
const item = localStorage.getItem(cacheKey);
|
||||
const loading = ref(false)
|
||||
const height = ref(0)
|
||||
if (item) {
|
||||
height.value = parseInt(item)
|
||||
}
|
||||
if (data.value["image"]?.width > 0) {
|
||||
height.value = 350 * data.value["image"]?.height / data.value["image"]?.width
|
||||
localStorage.setItem(cacheKey, height.value)
|
||||
}
|
||||
data.value["showOpt"] = data.value["content"]?.indexOf("- Image #") === -1;
|
||||
// console.log(data.value)
|
||||
|
||||
watch(() => props.content, (newVal) => {
|
||||
data.value = newVal;
|
||||
});
|
||||
const emits = defineEmits(['disable-input', 'disable-input']);
|
||||
const upscale = (index) => {
|
||||
send('/api/mj/upscale', index)
|
||||
}
|
||||
|
||||
const variation = (index) => {
|
||||
send('/api/mj/variation', index)
|
||||
}
|
||||
|
||||
const send = (url, index) => {
|
||||
loading.value = true
|
||||
emits('disable-input')
|
||||
httpPost(url, {
|
||||
index: index,
|
||||
src: "chat",
|
||||
message_id: data.value?.["message_id"],
|
||||
message_hash: data.value?.["image"]?.hash,
|
||||
session_id: getSessionId(),
|
||||
key: data.value?.["key"],
|
||||
prompt: data.value?.["prompt"],
|
||||
chat_id: props.chatId,
|
||||
role_id: props.roleId,
|
||||
icon: props.icon,
|
||||
}).then(() => {
|
||||
showNotify({type: "success", message: "任务推送成功,请耐心等待任务执行..."})
|
||||
loading.value = false
|
||||
}).catch(e => {
|
||||
showNotify({type: "danger", message: "任务推送失败:" + e.message})
|
||||
emits('disable-input')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.mobile-message-mj {
|
||||
display flex
|
||||
justify-content: flex-start;
|
||||
|
||||
.chat-icon {
|
||||
margin-right 5px
|
||||
|
||||
.van-image {
|
||||
width 32px
|
||||
|
||||
img {
|
||||
border-radius 5px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 0 0 0 5px;
|
||||
overflow: hidden;
|
||||
|
||||
.triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 5px solid transparent;
|
||||
border-bottom: 5px solid transparent;
|
||||
border-right: 5px solid #fff;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 13px;
|
||||
}
|
||||
|
||||
.content-box {
|
||||
|
||||
display flex
|
||||
flex-direction row
|
||||
|
||||
.content {
|
||||
text-align left
|
||||
width 100%
|
||||
overflow-x auto
|
||||
min-height 20px;
|
||||
word-break break-word;
|
||||
padding: 5px 10px;
|
||||
color #444444
|
||||
background-color: #ffffff;
|
||||
font-size: 16px
|
||||
border-radius: 5px;
|
||||
|
||||
.content-inner {
|
||||
word-break break-word;
|
||||
padding: 6px 10px;
|
||||
color #374151;
|
||||
font-size: var(--content-font-size);
|
||||
border-radius: 5px;
|
||||
overflow: auto;
|
||||
|
||||
.text {
|
||||
p:first-child {
|
||||
margin-top 0
|
||||
}
|
||||
}
|
||||
|
||||
.images {
|
||||
max-width 350px;
|
||||
|
||||
.el-image {
|
||||
border-radius 10px;
|
||||
|
||||
.image-slot {
|
||||
color #c1c1c1
|
||||
width 350px
|
||||
text-align center
|
||||
border-radius 10px;
|
||||
border 1px solid #e1e1e1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.opt {
|
||||
.opt-line {
|
||||
margin 6px 0
|
||||
|
||||
ul {
|
||||
display flex
|
||||
flex-flow row
|
||||
padding-left 10px
|
||||
|
||||
li {
|
||||
margin-right 10px
|
||||
|
||||
a {
|
||||
padding 3px 0
|
||||
width 50px
|
||||
text-align center
|
||||
border-radius 5px
|
||||
display block
|
||||
cursor pointer
|
||||
background-color #4E5058
|
||||
color #ffffff
|
||||
|
||||
&:hover {
|
||||
background-color #6D6F78
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.van-theme-dark {
|
||||
.mobile-message-reply {
|
||||
.chat-item {
|
||||
.triangle {
|
||||
border-right: 5px solid #404042;
|
||||
}
|
||||
|
||||
.content-box {
|
||||
.content {
|
||||
color #c1c1c1
|
||||
background-color: #404042;
|
||||
|
||||
p > code {
|
||||
color #c1c1c1
|
||||
background-color #2b2b2b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
102
new-ui/projects/web/src/components/mobile/ChatPrompt.vue
Normal file
102
new-ui/projects/web/src/components/mobile/ChatPrompt.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="mobile-message-prompt">
|
||||
<div class="chat-item">
|
||||
<div ref="contentRef" :data-clipboard-text="content" class="content" v-html="content"></div>
|
||||
<div class="triangle"></div>
|
||||
</div>
|
||||
|
||||
<div class="chat-icon">
|
||||
<van-image :src="icon"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref} from "vue";
|
||||
import Clipboard from "clipboard";
|
||||
import {showNotify} from "vant";
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '/images/user-icon.png',
|
||||
}
|
||||
});
|
||||
const contentRef = ref(null)
|
||||
onMounted(() => {
|
||||
const clipboard = new Clipboard(contentRef.value);
|
||||
clipboard.on('success', () => {
|
||||
showNotify({type: 'success', message: '复制成功', duration: 1000})
|
||||
})
|
||||
clipboard.on('error', () => {
|
||||
showNotify({type: 'danger', message: '复制失败', duration: 2000})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.mobile-message-prompt {
|
||||
display flex
|
||||
justify-content: flex-end
|
||||
|
||||
.chat-icon {
|
||||
margin-left 5px
|
||||
|
||||
.van-image {
|
||||
width 32px
|
||||
|
||||
img {
|
||||
border-radius 5px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
position: relative;
|
||||
padding: 0 5px 0 0;
|
||||
overflow: hidden;
|
||||
|
||||
.triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 5px solid transparent;
|
||||
border-bottom: 5px solid transparent;
|
||||
border-left: 5px solid #98E165;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.content {
|
||||
word-break break-word;
|
||||
text-align left
|
||||
padding: 5px 10px;
|
||||
background-color: #98E165;
|
||||
color #444444
|
||||
font-size: 14px
|
||||
border-radius: 5px
|
||||
line-height 1.5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.van-theme-dark {
|
||||
.mobile-message-prompt {
|
||||
.chat-item {
|
||||
|
||||
.triangle {
|
||||
border-left: 5px solid #223A34
|
||||
}
|
||||
|
||||
.content {
|
||||
background-color: #223A34
|
||||
color #c1c1c1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
224
new-ui/projects/web/src/components/mobile/ChatReply.vue
Normal file
224
new-ui/projects/web/src/components/mobile/ChatReply.vue
Normal file
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<div class="mobile-message-reply">
|
||||
<div class="chat-icon">
|
||||
<van-image :src="icon"/>
|
||||
</div>
|
||||
|
||||
<div class="chat-item">
|
||||
<div class="triangle"></div>
|
||||
<div class="content-box" ref="contentRef">
|
||||
<div :data-clipboard-text="orgContent" class="content content-mobile" v-html="content"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {nextTick, onMounted, ref} from "vue"
|
||||
|
||||
import {showImagePreview} from "vant";
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
orgContent: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '/images/gpt-icon.png',
|
||||
}
|
||||
});
|
||||
|
||||
const contentRef = ref(null)
|
||||
onMounted(() => {
|
||||
const imgs = contentRef.value.querySelectorAll('img')
|
||||
for (let i = 0; i < imgs.length; i++) {
|
||||
if (!imgs[i].src) {
|
||||
continue
|
||||
}
|
||||
imgs[i].addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
showImagePreview([imgs[i].src]);
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.mobile-message-reply {
|
||||
display flex
|
||||
justify-content: flex-start;
|
||||
|
||||
.chat-icon {
|
||||
margin-right 5px
|
||||
|
||||
.van-image {
|
||||
width 32px
|
||||
|
||||
img {
|
||||
border-radius 5px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 0 0 0 5px;
|
||||
overflow: hidden;
|
||||
|
||||
.triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 5px solid transparent;
|
||||
border-bottom: 5px solid transparent;
|
||||
border-right: 5px solid #fff;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 13px;
|
||||
}
|
||||
|
||||
.content-box {
|
||||
|
||||
display flex
|
||||
flex-direction row
|
||||
|
||||
.content {
|
||||
text-align left
|
||||
width 100%
|
||||
overflow-x auto
|
||||
min-height 20px;
|
||||
word-break break-word;
|
||||
padding: 5px 10px;
|
||||
color #444444
|
||||
background-color: #ffffff;
|
||||
font-size: 14px
|
||||
border-radius: 5px;
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
p:first-child {
|
||||
margin-top 0
|
||||
}
|
||||
|
||||
p {
|
||||
code {
|
||||
color #2b2b2b
|
||||
background-color #c1c1c1
|
||||
padding 2px 5px
|
||||
border-radius 5px
|
||||
}
|
||||
|
||||
img {
|
||||
max-width 100%
|
||||
}
|
||||
}
|
||||
|
||||
.code-container {
|
||||
position relative
|
||||
|
||||
.hljs {
|
||||
border-radius 10px
|
||||
line-height 1.5
|
||||
}
|
||||
|
||||
.copy-code-mobile {
|
||||
position: absolute;
|
||||
right 10px
|
||||
top 10px
|
||||
cursor pointer
|
||||
font-size 12px
|
||||
color #c1c1c1
|
||||
|
||||
&:hover {
|
||||
color #20a0ff
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.lang-name {
|
||||
display none
|
||||
position absolute;
|
||||
right 10px
|
||||
bottom 50px
|
||||
padding 2px 6px 4px 6px
|
||||
background-color #444444
|
||||
border-radius 10px
|
||||
color #00e0e0
|
||||
}
|
||||
|
||||
|
||||
// 设置表格边框
|
||||
|
||||
table {
|
||||
width 100%
|
||||
margin-bottom 1rem
|
||||
color #212529
|
||||
border-collapse collapse;
|
||||
border 1px solid #dee2e6;
|
||||
background-color #ffffff
|
||||
|
||||
thead {
|
||||
th {
|
||||
border 1px solid #dee2e6
|
||||
vertical-align: bottom
|
||||
border-bottom: 2px solid #dee2e6
|
||||
padding 10px
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
border 1px solid #dee2e6
|
||||
padding 10px
|
||||
}
|
||||
}
|
||||
|
||||
// 代码快
|
||||
|
||||
blockquote {
|
||||
margin 0
|
||||
background-color: #ebfffe;
|
||||
padding: 0.8rem 1.5rem;
|
||||
border-left: 0.5rem solid;
|
||||
border-color: #026863;
|
||||
color: #2c3e50;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.van-theme-dark {
|
||||
.mobile-message-reply {
|
||||
.chat-item {
|
||||
.triangle {
|
||||
border-right: 5px solid #404042;
|
||||
}
|
||||
|
||||
.content-box {
|
||||
.content {
|
||||
color #c1c1c1
|
||||
background-color: #404042;
|
||||
|
||||
p > code {
|
||||
color #c1c1c1
|
||||
background-color #2b2b2b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user