初始化导入 hjsq H5 项目代码

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
谢宇宁
2026-11-05 12:01:36 +07:00
co-authored by Claude Opus 5.5
commit f61585f786
1367 changed files with 136187 additions and 0 deletions
@@ -0,0 +1,68 @@
<template>
<div class="accountCertificate">
<Header title="账号凭证" />
<div class="content overflow-y-auto">
<Promote />
<div class="promoteBtnBox">
<div class="promoteBtn saved" @click="onSaved">我已保存</div>
<div class="promoteBtn primary" @click="saveImg">保存图片</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { showToast } from 'vant';
import { useRouter } from 'vue-router';
const router = useRouter();
const saveImg = () => {
showToast('请截图保存图片');
};
const onSaved = () => {
router.back();
};
</script>
<style lang="scss" scoped>
.accountCertificate {
height: 100%;
width: 100%;
background: #0f0f0f;
.content {
height: calc(100vh - 44px);
padding: 28px 0 40px;
.promoteBtnBox {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
.promoteBtn {
height: 44px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 500;
box-sizing: border-box;
&.saved {
width: 94px;
border-radius: 90px;
background: #dcdcdc;
color: #292929;
font-size: 14px;
font-weight: 400;
}
&.primary {
width: 130px;
border-radius: 999px;
background: #ffc000;
color: #1f2029;
font-size: 16px;
}
}
}
}
}
</style>
+368
View File
@@ -0,0 +1,368 @@
<template>
<div class="wrap">
<div class="header">
<svg-icon @click="$router.go(-1)" class="back" name="back_white" />
<div class="recharge-box">
<div class="points">积分:{{ balance }}</div>
<div class="btn" @click="openRecharge">充值</div>
</div>
</div>
<div class="go" @click="getLogin"></div>
<van-popup class="recharge-popup" v-model:show="visibleRecharge" position="bottom" z-index="2001">
<div class="box">
<div class="box_line" />
<div class="header">
<h2>AI女友</h2>
<p>充值积分</p>
</div>
<Loading v-if="isLoading" style="height: 150px" />
<div v-else class="list">
<div
class="item"
:class="{ active: index === currentIndex }"
v-for="(item, index) in list"
:key="item.id"
@click="currentIndex = index"
>
<p class="coins">{{ item.coins }}积分</p>
<p class="price">{{ item.price }}金豆</p>
</div>
</div>
<div class="tips">
<h3>支付小贴士:</h3>
<p>1.因超时支付无法到账,请重新发起</p>
<p>2.连续发起且未支付,账号可能被加入黑名单</p>
<p>3.充值成功后会在1~5分钟内到账,可重新刷新进入当前页面</p>
</div>
<div class="btn" @click="exchange">{{ currentItem.price }}金豆/立即支付</div>
</div>
</van-popup>
<div class="mianBox" v-if="loginUrl">
<svg-icon @click="closeUrl" name="back_white" class="back" />
<iframe
class="iframe"
:src="loginUrl"
frameborder="0"
sandbox="allow-scripts allow-same-origin allow-forms"
></iframe>
</div>
<div v-if="isNoOpen" class="mask">
<svg-icon name="back_white" @click="$router.go(-1)" class="back" />
<div class="tips">{{ noOpenTip }}</div>
</div>
</div>
</template>
<script>
import MineApi from '@/service/mine';
import useLoading from '@/hooks/loading';
const { openLoading, closeLoading } = useLoading();
export default {
name: 'AiGirlFriend',
data() {
return {
visibleRecharge: false,
balance: 0,
isLoading: true,
list: [],
currentIndex: 0,
loginUrl: '',
isNoOpen: false,
noOpenTip: '',
};
},
computed: {
currentItem() {
if (this.list.length === 0) return { price: 0 };
else return this.list[this.currentIndex];
},
},
mounted() {
this.getBalance();
},
methods: {
getBalance() {
MineApi.getAimateGetBalance()
.then((data) => {
this.balance = (data && data.balance) || 0;
})
.catch((err) => {
const code = err.message;
this.isNoOpen = code == 404;
this.noOpenTip = err.resData.data.tip;
});
},
openRecharge() {
this.visibleRecharge = true;
if (this.list.length === 0) {
this.isLoading = true;
MineApi.getAimateCurrencys()
.then((data) => {
this.list = data.list || [];
})
.finally(() => {
this.isLoading = false;
});
}
},
exchange() {
openLoading();
MineApi.postAimateExchange({
id: this.currentItem.id,
})
.then(() => {
this.getBalance();
this.$toast('充值成功');
this.visibleRecharge = false;
})
.catch((err) => {
const code = err.message;
if (code == 8000) {
this.$router.push('wallet');
}
})
.finally(() => {
closeLoading();
});
},
getLogin() {
openLoading();
MineApi.getAimateLogin()
.then((data) => {
this.loginUrl = (data && data.url) || '';
})
.finally(() => {
closeLoading();
});
},
closeUrl() {
this.loginUrl = false;
this.getBalance();
},
},
};
</script>
<style lang="scss" scoped>
.wrap {
width: 100%;
height: 100%;
background: url('@/assets/images/png/AiGirlFriend-bg.png') 0 100%/100% auto no-repeat;
> .header {
position: fixed;
z-index: 10;
top: 0;
left: 0;
width: 100%;
height: 68px;
background: url('@/assets/images/png/AiGirlFriend-header.png') 0 100%/100% auto no-repeat;
display: flex;
align-items: center;
justify-content: space-between;
.back {
margin-left: 16px;
width: 24px;
height: 24px;
flex-shrink: 0;
}
.recharge-box {
margin-right: 16px;
display: flex;
padding: 2px 2px 2px 10px;
align-items: center;
border-radius: 100px;
border: 1px solid #1e3c72;
background: #000;
color: #fff;
font-size: 18px;
font-style: normal;
font-weight: 400;
line-height: normal;
}
.btn {
display: flex;
padding: 4px 20px;
justify-content: center;
align-items: center;
margin-left: 10px;
border-radius: 100px;
background: linear-gradient(90deg, #1e3c72 0%, #1e3c72 1%, #5f40b6 54.5%, #2a5298 100%);
box-shadow: 0px 1px 0px 0px #ffffff40 inset;
}
}
> .go {
width: 393px;
height: 146px;
position: fixed;
bottom: 120px;
left: 0;
background: url('@/assets/images/png/AiGirlFriend-btn.png') 0 0/100% auto no-repeat;
}
.mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 100;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
.back {
position: absolute;
top: 20px;
left: 16px;
z-index: 2;
width: 24px;
height: 24px;
}
.tips {
font-size: 16px;
color: #fff;
}
}
}
.recharge-popup {
.box {
border-radius: 20px 20px 0px 0px;
background: #1d2236;
width: 100%;
min-height: 463px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 18px 16px;
color: #fff;
.box_line {
width: 36px;
height: 5px;
border-radius: 99px;
background: #34384a;
margin-bottom: 20px;
}
.header {
h2 {
text-align: center;
font-size: 20px;
font-style: normal;
font-weight: 600;
line-height: 24px; /* 120% */
margin-bottom: 12px;
}
p {
color: #898c96;
text-align: center;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
}
}
.list {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-column-gap: 12px;
grid-row-gap: 12px;
margin: 18px 0;
.item {
padding: 9px 12px;
border-radius: 6px;
border: 1px solid, rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.05);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
&.active {
border-color: #30509c;
background: #4039a1;
.coins {
color: #afabff;
}
}
.coins {
color: #7d95d5;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 137.5% */
}
.price {
color: rgba(255, 255, 255, 0.3);
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
}
}
}
.tips {
padding-left: 12px;
width: 100%;
h3 {
color: #fff;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
margin-bottom: 6px;
}
p {
color: #898c96;
text-align: left;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
}
}
.btn {
margin: 18px 0;
width: 343px;
height: 44px;
line-height: 44px;
text-align: center;
border-radius: 44px;
background: linear-gradient(90deg, #1e3c72 0%, #1e3c72 1%, #5f40b6 54.5%, #2a5298 100%), #ffc170;
color: #fff;
font-size: 16px;
font-style: normal;
font-weight: 400;
}
}
}
.mianBox {
width: 100%;
height: 100vh;
background-color: #000;
position: fixed;
top: 0;
left: 0;
z-index: 8000;
.back {
position: absolute;
top: 20px;
left: 16px;
z-index: 8001;
width: 30px;
height: 30px;
flex-shrink: 0;
}
.iframe {
width: 100%;
height: 100%;
}
}
</style>
+79
View File
@@ -0,0 +1,79 @@
<template>
<div class="ai-girlfriend-web">
<Header title="AI女友">
<template #left>
<SvgIcon name="back_white" class="back" @click="handleBack" />
</template>
</Header>
<div class="content" v-if="aiMateLink">
<iframe class="iframe" :src="aiMateLink" allow="clipboard-write" frameborder="0"></iframe>
</div>
</div>
</template>
<script setup lang="ts">
import MineApi from '@/service/mine';
import { useUserStore } from '@/stores/user';
import { onMounted, ref } from 'vue';
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router';
const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const aiMateLink = ref('');
// 记录进入页面时的 history 基线,用于跨过 iframe 内部 pushState 的历史条目
let enterHistoryLen = 0;
const fetchAiMateLink = () => {
MineApi.getAimatev2Url()
.then((res) => {
if (res?.url) {
aiMateLink.value = res.url;
}
})
.catch(() => {
aiMateLink.value = '';
});
};
const handleBack = () => {
const extra = Math.max(0, window.history.length - enterHistoryLen);
router.go(-(extra + 1));
};
onMounted(() => {
enterHistoryLen = window.history.length;
const queryUrl = route.query.authUrl;
if (typeof queryUrl === 'string' && queryUrl) {
aiMateLink.value = queryUrl;
return;
}
fetchAiMateLink();
});
onBeforeRouteLeave(() => {
userStore.updateWallet();
});
</script>
<style scoped lang="scss">
.ai-girlfriend-web {
height: 100%;
width: 100%;
.back {
width: 24px;
height: 24px;
}
.content {
width: 100%;
height: calc(100% - 44px);
.iframe {
width: 100%;
height: 100%;
}
}
}
</style>
@@ -0,0 +1,305 @@
<template>
<div class="aiRecordList overflow-y-auto">
<PullRefresh v-if="!state.loading" :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="recordBox" v-for="item in state.list" :key="item.id">
<div class="createdAt">
<span>创建时间 {{ dateFormat(item.createdAt) }}</span>
<SvgIcon
name="aiClose"
class="aiClose"
v-if="state.status == '2' || state.status == '3'"
@click.stop="openDelChangeImgFaceDialog(item.id)"
></SvgIcon>
</div>
<div class="imageBox">
<div class="modCover" @click.stop="openBigImg([item.modPic])">
<div class="lable">模版</div>
<lazyImg class="cover" :src="item.modPic"></lazyImg>
</div>
<SvgIcon name="aiAdd" class="equalSign"></SvgIcon>
<div class="originPicBox" @click.stop="openBigImg([item.originPic])">
<div class="lable">素材</div>
<lazyImg class="cover" :src="item.originPic"></lazyImg>
</div>
<SvgIcon name="equalSign" class="equalSign"></SvgIcon>
<div class="resultImg" @click.stop="state.status == '2' ? openBigImg(item.newPic) : ''">
<div class="mask" v-if="state.status !== '2'">
<SvgIcon name="aiFail" class="aiFail" v-if="state.status == '3'"></SvgIcon>
</div>
<lazyImg
class="cover"
:src="(item.newPic && item.newPic[0]) || item.originPic"
:key="(item.newPic && item.newPic[0]) || item.originPic"
></lazyImg>
</div>
</div>
<div class="statusBox">
<div class="statusBtn" v-if="state.status == '1'">生成中</div>
<div class="statusBtn" v-else-if="state.status == '2'">已完成</div>
<div class="failBtn" v-else-if="state.status == '3'">生成失败</div>
<div class="statusText errorText" v-if="state.status == '3'">
{{ item.remark }}
</div>
</div>
</div>
</PullRefresh>
<Loading v-else />
</div>
</template>
<script lang="ts" setup>
import { reactive, inject } from 'vue';
import { dateFormat } from '@/utils/index';
import MineApi from '@/service/mine';
const props = defineProps<{
status: string;
}>();
const state = reactive({
pageNumber: <number>1,
pageSize: <number>10,
list: <any>[{}],
loading: <boolean>true,
status: props.status,
delId: '', // 当前删除ID
});
const openBigImg: any = inject('openBigImg');
const openDelChangeImgFaceDialog: any = inject('openDelChangeImgFaceDialog');
// const openReasonDialog: any = inject('openReasonDialog');
/**
* 刷新组件
*/
const moreParams = reactive({
loading: <boolean>true,
refreshing: <boolean>false,
finished: <boolean>false,
isNoData: <boolean>false,
error: <boolean>false,
isHigehtMax: <boolean>false,
});
/**
* 获取列表
*/
const getList = async (type: string) => {
try {
let res: any = await MineApi.getAiChangeFaceImgRecord({
pageNumber: state.pageNumber,
pageSize: state.pageSize,
status: state.status,
});
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (type === 'refresh') {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1 && state.list.length == 0) {
moreParams.isNoData = true;
}
if (list.length < state.pageSize) {
moreParams.finished = true;
}
} catch (e: any) {
moreParams.loading = false;
moreParams.error = true;
}
};
getList('refresh');
const delId = (id: string) => {
let newArr = [];
newArr = state.list.filter((item: any) => {
if (item.id != id) return item;
});
state.list = newArr;
if (state.list.length == 0) {
moreParams.finished = false;
moreParams.isNoData = true;
}
};
/**
* 上拉刷新
*/
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList('');
};
/**
* 下拉刷新
*/
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
moreParams.isNoData = false;
getList('refresh');
};
defineExpose({ delId });
</script>
<style lang="scss" scoped>
.aiRecordList {
height: calc(100vh - 44px * 3);
padding: 0 16px;
.recordBox {
margin: 0 auto 10px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 12px;
justify-content: flex-start;
.createdAt {
font-size: 12px;
color: rgba(220, 220, 220, 1);
display: flex;
align-items: center;
justify-content: space-between;
.aiClose {
height: 12px;
width: 12px;
}
}
.processing {
width: 18px;
height: 18px;
}
.delBtn {
display: flex;
align-items: center;
color: #f52c56;
font-size: 12px;
}
.aiClose {
width: 18px;
height: 18px;
margin-right: 4px;
}
.imageBox {
display: flex;
align-items: center;
justify-content: flex-start;
border-bottom: 1px solid rgba(0, 0, 0, 0.02);
padding-bottom: 12px;
margin: 12px 0 12px;
.modCover {
width: 90px;
height: 90px;
border-radius: 8px;
overflow: hidden;
position: relative;
.lable {
position: absolute;
top: 0;
left: 0;
width: 36px;
height: 18px;
font-size: 12px;
border-radius: 8px 0px;
color: rgba(41, 41, 41, 1);
background: rgba(255, 212, 96, 1);
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
:deep(img) {
width: 100%;
height: 100% !important;
object-fit: cover !important;
}
}
.originPicBox {
width: 90px;
height: 90px;
border-radius: 8px;
overflow: hidden;
position: relative;
.lable {
position: absolute;
top: 0;
left: 0;
width: 36px;
height: 18px;
font-size: 12px;
border-radius: 8px 0px;
color: #000;
background: #03fceb;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
}
.equalSign {
width: 18px;
height: 18px;
margin: 0 3px;
color: rgba(152, 152, 152, 1);
}
.resultImg {
width: 90px;
height: 90px;
border-radius: 8px;
overflow: hidden;
position: relative;
.mask {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
.aiFail {
width: 24px;
height: 24px;
}
}
}
}
.statusBox {
display: flex;
align-items: center;
justify-content: space-between;
.statusText {
width: 240px;
font-size: 12px;
color: rgba(152, 152, 152, 1);
word-break: break-all;
}
.errorText {
color: #e7166b;
line-height: 18px;
}
}
.statusBtn {
display: flex;
align-items: center;
justify-content: center;
width: 55px;
height: 22px;
border-radius: 4px;
background: rgba(255, 219, 158, 0.1);
color: #ffdb9e;
font-size: 12px;
font-weight: 400;
line-height: normal;
}
.failBtn {
display: flex;
align-items: center;
height: 22px;
border-radius: 4px;
color: rgba(255, 255, 255, 0.55);
font-size: 12px;
font-weight: 400;
line-height: normal;
}
}
}
</style>
@@ -0,0 +1,87 @@
<template>
<div class="aiRecordTab">
<van-tabs
v-model:active="state.activeValue"
animated
shrink
class="child_tabs"
title-active-color="#fff"
title-inactive-color="#989898"
>
<van-tab name="queue" title="排队">
<AiChangeImgFaceList ref="undressChildRef1" status="1" />
</van-tab>
<van-tab name="success" title="成功">
<AiChangeImgFaceList ref="undressChildRef2" status="2" />
</van-tab>
<van-tab name="fail" title="失败">
<AiChangeImgFaceList ref="undressChildRef3" status="3" />
</van-tab>
</van-tabs>
</div>
</template>
<script lang="ts" setup>
import { ref, nextTick, reactive } from 'vue';
import { useRouter } from 'vue-router';
import AiChangeImgFaceList from './aiChangeImgFaceList.vue';
const router = useRouter();
const undressChildRef1 = ref<any>();
const undressChildRef2 = ref<any>();
const undressChildRef3 = ref<any>();
const state = reactive({
activeValue: <string>'queue',
});
nextTick(() => {
let name: any = router.currentRoute.value.query['name'];
if (name) {
state.activeValue = name;
} else {
state.activeValue = 'queue';
}
});
const delMethod = (id: string) => {
if (state.activeValue == 'queue') {
undressChildRef1.value.delId(id);
} else if (state.activeValue == 'success') {
undressChildRef2.value.delId(id);
} else if (state.activeValue == 'fail') {
undressChildRef3.value.delId(id);
}
};
defineExpose({ delMethod });
</script>
<style lang="scss" scoped>
.aiRecordTab {
.child_tabs {
:deep(> .van-tabs__wrap) {
padding: 0 16px;
.van-tabs__nav {
background: transparent;
padding: 0;
}
.van-tab {
padding: 0 16px;
}
.van-tab::before {
content: '';
width: 1px;
height: 12px;
background: #ffffff1a;
position: absolute;
right: 0;
}
.van-tab:first-child {
padding-left: 0;
}
.van-tab:nth-child(3)::before {
display: none;
}
.van-tabs__line {
display: none;
}
}
}
}
</style>
@@ -0,0 +1,308 @@
<template>
<div class="aiRecordList overflow-y-auto">
<PullRefresh v-if="!state.loading" :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="recordBox" v-for="item in state.list" :key="item.id">
<div class="createdAt">
<span>创建时间 {{ dateFormat(item.createdAt) }}</span>
<SvgIcon
name="aiClose"
class="aiClose"
v-if="state.status == '-1' || state.status == '1'"
@click.stop="openDelChangeVideoFaceDialog(item.id)"
></SvgIcon>
</div>
<div class="imageBox">
<div class="modCover" @click.stop="openBigImgArr(item.modCover)">
<div class="lable">模版</div>
<lazyImg class="cover" :src="item.modCover"></lazyImg>
</div>
<SvgIcon name="aiAdd" class="equalSign"></SvgIcon>
<div class="originPicBox" @click="openBigImg(item.picture)">
<div class="lable">素材</div>
<lazyImg class="cover" :src="item.picture && item.picture[0]"></lazyImg>
</div>
<SvgIcon name="equalSign" class="equalSign"></SvgIcon>
<div
class="resultImg"
@click="item.status != 1 ? openBigImg(item.picture) : openPlayVideo(item.url, item.h265Url)"
>
<div class="mask" v-if="status !== '1'">
<SvgIcon name="aiFail" class="aiFail" v-if="state.status == '-1'"></SvgIcon>
</div>
<lazyImg class="cover" :src="item.picture && item.picture[0]"></lazyImg>
</div>
</div>
<div class="statusBox">
<div class="statusBtn" v-if="state.status == '0'">生成中</div>
<div class="statusBtn" v-else-if="state.status == '1'">已完成</div>
<div class="failBtn" v-else-if="state.status == '-1'">生成失败</div>
<div class="statusText errorText" v-if="state.status == '-1'">
{{ item.remark }}
</div>
</div>
</div>
</PullRefresh>
<Loading v-else />
</div>
</template>
<script lang="ts" setup>
import { reactive, inject } from 'vue';
import { dateFormat } from '@/utils/index';
import MineApi from '@/service/mine';
const props = defineProps<{
status: string;
}>();
const state = reactive({
pageNumber: <number>1,
pageSize: <number>10,
list: <Array<any>>[{ picture: [] }], // 换脸模版
loading: <boolean>true,
status: props.status,
delId: '', // 当前删除ID
});
const openBigImg: any = inject('openBigImg');
const openPlayVideo: any = inject('openPlayVideo');
const openDelChangeVideoFaceDialog: any = inject('openDelChangeVideoFaceDialog');
// const openReasonDialog: any = inject('openReasonDialog');
/**
* 刷新组件
*/
const moreParams = reactive({
loading: <boolean>true,
refreshing: <boolean>false,
finished: <boolean>false,
isNoData: <boolean>false,
error: <boolean>false,
isHigehtMax: <boolean>false,
});
/**
* 获取列表
*/
const getList = async (type: string) => {
try {
let res: any = await MineApi.getAiChangeFaceVideoRecord({
pageNumber: state.pageNumber,
pageSize: state.pageSize,
status: state.status,
});
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (type === 'refresh') {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1 && state.list.length == 0) {
moreParams.isNoData = true;
}
if (list.length < state.pageSize) {
moreParams.finished = true;
}
// console.log(state.list, state.status + '-----');
} catch (e: any) {
moreParams.loading = false;
moreParams.error = true;
}
};
getList('refresh');
const delId = (id: string) => {
let newArr = [];
newArr = state.list.filter((item: any) => {
if (item.id != id) return item;
});
state.list = newArr;
if (state.list.length == 0) {
moreParams.finished = false;
moreParams.isNoData = true;
}
};
/**
* 上拉刷新
*/
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList('');
};
/**
* 下拉刷新
*/
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
moreParams.isNoData = false;
getList('refresh');
};
const openBigImgArr = (img: any) => {
openBigImg([img]);
};
defineExpose({ delId });
</script>
<style lang="scss" scoped>
.aiRecordList {
height: calc(100vh - 44px * 3);
padding: 0 16px;
.recordBox {
margin: 0 auto 10px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 12px;
justify-content: flex-start;
.createdAt {
font-size: 12px;
color: rgba(220, 220, 220, 1);
display: flex;
align-items: center;
justify-content: space-between;
.aiClose {
height: 12px;
width: 12px;
}
}
.processing {
width: 18px;
height: 18px;
}
.delBtn {
display: flex;
align-items: center;
color: #f52c56;
font-size: 12px;
}
.aiClose {
width: 18px;
height: 18px;
margin-right: 4px;
}
.imageBox {
display: flex;
align-items: center;
justify-content: flex-start;
border-bottom: 1px solid #ffffff0d;
padding-bottom: 12px;
margin: 12px 0 12px;
.modCover {
width: 90px;
height: 90px;
border-radius: 8px;
overflow: hidden;
position: relative;
.lable {
position: absolute;
top: 0;
left: 0;
width: 36px;
height: 18px;
font-size: 12px;
border-radius: 8px 0px;
color: rgba(41, 41, 41, 1);
background: rgba(255, 212, 96, 1);
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
:deep(img) {
width: 100%;
height: 100% !important;
object-fit: cover !important;
}
}
.originPicBox {
width: 90px;
height: 90px;
border-radius: 8px;
overflow: hidden;
position: relative;
.lable {
position: absolute;
top: 0;
left: 0;
width: 36px;
height: 18px;
font-size: 12px;
border-radius: 8px 0px;
color: #000;
background: #03fceb;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
}
.equalSign {
width: 18px;
height: 18px;
margin: 0 3px;
color: rgba(152, 152, 152, 1);
}
.resultImg {
width: 90px;
height: 90px;
border-radius: 8px;
overflow: hidden;
position: relative;
.mask {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
.aiFail {
width: 24px;
height: 24px;
}
}
}
}
.statusBox {
display: flex;
align-items: center;
justify-content: space-between;
.statusText {
width: 240px;
font-size: 12px;
color: rgba(152, 152, 152, 1);
word-break: break-all;
}
.errorText {
color: #e7166b;
line-height: 18px;
}
}
.statusBtn {
display: flex;
align-items: center;
justify-content: center;
width: 55px;
height: 22px;
border-radius: 4px;
background: rgba(255, 219, 158, 0.1);
color: #ffdb9e;
font-size: 12px;
font-weight: 400;
line-height: normal;
}
.failBtn {
display: flex;
align-items: center;
height: 22px;
border-radius: 4px;
color: rgba(255, 255, 255, 0.55);
font-size: 12px;
font-weight: 400;
line-height: normal;
}
}
}
</style>
@@ -0,0 +1,87 @@
<template>
<div class="aiRecordTab">
<van-tabs
v-model:active="state.activeValue"
animated
shrink
class="child_tabs"
title-active-color="#fff"
title-inactive-color="#989898"
>
<van-tab name="queue" title="排队">
<aiChangeVideoFaceList ref="changeFaceChildRef1" status="0" />
</van-tab>
<van-tab name="success" title="成功">
<aiChangeVideoFaceList ref="changeFaceChildRef2" status="1" />
</van-tab>
<van-tab name="fail" title="失败">
<aiChangeVideoFaceList ref="changeFaceChildRef3" status="-1" />
</van-tab>
</van-tabs>
</div>
</template>
<script lang="ts" setup>
import { ref, nextTick, reactive } from 'vue';
import { useRouter } from 'vue-router';
import aiChangeVideoFaceList from './aiChangeVideoFaceList.vue';
const router = useRouter();
const changeFaceChildRef1 = ref<any>();
const changeFaceChildRef2 = ref<any>();
const changeFaceChildRef3 = ref<any>();
const state = reactive({
activeValue: <string>'queue',
});
nextTick(() => {
let name: any = router.currentRoute.value.query['name'];
if (name) {
state.activeValue = name;
} else {
state.activeValue = 'queue';
}
});
const delMethod = (id: string) => {
if (state.activeValue == 'queue') {
changeFaceChildRef1.value.delId(id);
} else if (state.activeValue == 'success') {
changeFaceChildRef2.value.delId(id);
} else if (state.activeValue == 'fail') {
changeFaceChildRef3.value.delId(id);
}
};
defineExpose({ delMethod });
</script>
<style lang="scss" scoped>
.aiRecordTab {
.child_tabs {
:deep(> .van-tabs__wrap) {
padding: 0 16px;
.van-tabs__nav {
background: transparent;
padding: 0;
}
.van-tab {
padding: 0 16px;
}
.van-tab::before {
content: '';
width: 1px;
height: 12px;
background: #ffffff1a;
position: absolute;
right: 0;
}
.van-tab:first-child {
padding-left: 0;
}
.van-tab:nth-child(3)::before {
display: none;
}
.van-tabs__line {
display: none;
}
}
}
}
</style>
@@ -0,0 +1,277 @@
<template>
<div class="aiRecordList overflow-y-auto">
<PullRefresh v-if="!state.loading" :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="recordBox" v-for="item in state.list" :key="item.id">
<div class="createdAt">
<span>创建时间:{{ dateFormat(item.createdAt) }}</span>
<SvgIcon
name="aiClose"
class="aiClose"
v-if="state.status == '2' || state.status == '3'"
@click.stop="openDelChangeImgToVideoDialog(item.id)"
></SvgIcon>
</div>
<div class="imageBox">
<div class="originPicBox" @click.stop="openBigImgArr(item.imgUrl)">
<div class="lable">素材</div>
<lazyImg class="cover" :src="item.imgUrl" />
</div>
<SvgIcon name="equalSign" class="equalSign"></SvgIcon>
<div class="resultImg" @click.stop="openBigImgArr(state.status == '2' ? item.newImgUrl : item.imgUrl)">
<div class="mask" v-if="state.status != '2'">
<SvgIcon name="aiFail" class="aiFail" v-if="state.status == '3'"></SvgIcon>
</div>
<lazyImg class="cover" :src="state.status == '2' ? item.newImgUrl : item.imgUrl"></lazyImg>
</div>
</div>
<div class="statusBox">
<div class="statusBtn" v-if="state.status == '1'">生成中</div>
<div class="statusBtn" v-else-if="state.status == '2'">已完成</div>
<div class="failBtn" v-else-if="state.status == '3'">生成失败</div>
<div class="statusText errorText" v-if="state.status == '3'">
{{ item.remark }}
</div>
</div>
</div>
</PullRefresh>
<Loading v-else />
</div>
</template>
<script lang="ts" setup>
import { reactive, inject } from 'vue';
import { dateFormat } from '@/utils/index';
import MineApi from '@/service/mine';
const props = defineProps<{
status: string;
}>();
const state = reactive({
pageNumber: <number>1,
pageSize: <number>10,
list: <Array<any>>[], // 换脸模版
loading: <boolean>true,
status: props.status,
delId: '', // 当前删除ID
});
const openBigImg: any = inject('openBigImg');
const openPlayVideo: any = inject('openPlayVideo');
const openDelChangeImgToVideoDialog: any = inject('openDelChangeImgToVideoDialog');
/**
* 刷新组件
*/
const moreParams = reactive({
loading: <boolean>true,
refreshing: <boolean>false,
finished: <boolean>false,
isNoData: <boolean>false,
error: <boolean>false,
isHigehtMax: <boolean>false,
});
/**
* 获取列表
*/
let flag = false;
const getList = async () => {
if (flag) return;
flag = true;
try {
let res: any = await MineApi.getAiImageToVideoRecord({
pageNumber: state.pageNumber,
pageSize: state.pageSize,
status: state.status,
});
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (state.pageNumber == 1) {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1 && state.list.length == 0) {
moreParams.isNoData = true;
}
if (list.length < state.pageSize) {
moreParams.finished = true;
}
} catch (e: any) {
moreParams.loading = false;
moreParams.error = true;
} finally {
flag = false;
}
};
getList();
const delId = (id: string) => {
let newArr = [];
newArr = state.list.filter((item) => {
if (item.id != id) return item;
});
state.list = newArr;
if (state.list.length == 0) {
moreParams.finished = false;
moreParams.isNoData = true;
}
};
/**
* 上拉刷新
*/
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
/**
* 下拉刷新
*/
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
moreParams.isNoData = false;
getList();
};
const openBigImgArr = (img: any) => {
openBigImg([img]);
};
defineExpose({ delId });
</script>
<style lang="scss" scoped>
.aiRecordList {
height: calc(100vh - 44px * 3);
padding: 0 16px;
.recordBox {
margin: 0 auto 10px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 14px;
justify-content: flex-start;
.createdAt {
font-size: 12px;
color: #dcdcdc;
display: flex;
align-items: center;
justify-content: space-between;
.aiClose {
height: 12px;
width: 12px;
}
}
.processing {
width: 18px;
height: 18px;
}
.delBtn {
display: flex;
align-items: center;
color: #f52c56;
font-size: 12px;
}
.aiClose {
width: 18px;
height: 18px;
margin-right: 4px;
}
.imageBox {
display: flex;
align-items: center;
justify-content: flex-start;
border-bottom: 1px solid #ffffff0d;
padding-bottom: 12px;
margin: 12px 0 12px;
.originPicBox {
width: 90px;
height: 90px;
border-radius: 8px;
overflow: hidden;
position: relative;
.lable {
position: absolute;
top: 0;
left: 0;
width: 36px;
height: 18px;
font-size: 12px;
border-radius: 8px 0px;
color: #000;
background: #03fceb;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
}
.equalSign {
width: 18px;
height: 18px;
margin: 0 3px;
}
.resultImg {
width: 90px;
height: 90px;
border-radius: 8px;
overflow: hidden;
position: relative;
.mask {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
.aiFail {
width: 24px;
height: 24px;
}
}
}
}
.statusBox {
display: flex;
align-items: center;
justify-content: space-between;
.statusText {
width: 240px;
font-size: 12px;
color: rgba(152, 152, 152, 1);
word-break: break-all;
}
.errorText {
color: #f52c56;
line-height: 18px;
}
}
.statusBtn {
display: flex;
align-items: center;
justify-content: center;
width: 55px;
height: 22px;
border-radius: 4px;
background: rgba(255, 219, 158, 0.1);
color: #ffdb9e;
font-size: 12px;
font-weight: 400;
line-height: normal;
}
.failBtn {
display: flex;
align-items: center;
height: 22px;
border-radius: 4px;
color: rgba(255, 255, 255, 0.55);
font-size: 12px;
font-weight: 400;
line-height: normal;
}
}
}
</style>
@@ -0,0 +1,87 @@
<template>
<div class="aiRecordTab">
<van-tabs
v-model:active="state.activeValue"
animated
shrink
class="child_tabs"
title-active-color="#fff"
title-inactive-color="#989898"
>
<van-tab name="queue" title="排队">
<aiImgToVidList ref="changeFaceChildRef1" status="1" />
</van-tab>
<van-tab name="success" title="成功">
<aiImgToVidList ref="changeFaceChildRef2" status="2" />
</van-tab>
<van-tab name="fail" title="失败">
<aiImgToVidList ref="changeFaceChildRef3" status="3" />
</van-tab>
</van-tabs>
</div>
</template>
<script lang="ts" setup>
import { ref, nextTick, reactive } from 'vue';
import { useRouter } from 'vue-router';
import aiImgToVidList from './aiImgToVidList.vue';
const router = useRouter();
const changeFaceChildRef1 = ref<any>();
const changeFaceChildRef2 = ref<any>();
const changeFaceChildRef3 = ref<any>();
const state = reactive({
activeValue: <string>'queue',
});
nextTick(() => {
let name: any = router.currentRoute.value.query['name'];
if (name) {
state.activeValue = name;
} else {
state.activeValue = 'queue';
}
});
const delMethod = (id: string) => {
if (state.activeValue == 'queue') {
changeFaceChildRef1.value.delId(id);
} else if (state.activeValue == 'success') {
changeFaceChildRef2.value.delId(id);
} else if (state.activeValue == 'fail') {
changeFaceChildRef3.value.delId(id);
}
};
defineExpose({ delMethod });
</script>
<style lang="scss" scoped>
.aiRecordTab {
.child_tabs {
:deep(> .van-tabs__wrap) {
padding: 0 16px;
.van-tabs__nav {
background: transparent;
padding: 0;
}
.van-tab {
padding: 0 16px;
}
.van-tab::before {
content: '';
width: 1px;
height: 12px;
background: #ffffff1a;
position: absolute;
right: 0;
}
.van-tab:first-child {
padding-left: 0;
}
.van-tab:nth-child(3)::before {
display: none;
}
.van-tabs__line {
display: none;
}
}
}
}
</style>
+246
View File
@@ -0,0 +1,246 @@
<template>
<div class="aiRecordList overflow-y-auto" ref="aiNovelListRef">
<PullRefresh v-if="!state.loading" :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="recordBox" v-for="item in state.list" :key="item.id" @click="openPreview(item)">
<div class="createdAt">
<span>创建时间:{{ dateFormat(item.createdAt) }}</span>
<SvgIcon
name="aiClose"
class="aiClose"
v-if="state.status == '2' || state.status == '3'"
@click.stop="openDelNovelDialog(item.id)"
></SvgIcon>
</div>
<div class="imageBox">
<div class="novelBox">
<div class="content">
<p>人物设定:{{ item.characterSetting }}</p>
<p>地点场景:{{ item.locationScene }}</p>
<p>故事情节:{{ item.description }}</p>
<p>细节说明:{{ item.details }}</p>
</div>
</div>
</div>
<div class="statusBox">
<div class="statusBtn" v-if="state.status == '1'">生成中</div>
<div class="statusBtn" v-else-if="state.status == '2'">已完成</div>
<div class="failBtn" v-else-if="state.status == '3'">生成失败</div>
<div class="statusText errorText" v-if="state.status == '3'">
{{ item.remark }}
</div>
</div>
</div>
</PullRefresh>
<Loading v-else />
</div>
</template>
<script lang="ts" setup>
import { reactive, inject, ref, onMounted } from 'vue';
import { dateFormat } from '@/utils/index';
import MineApi from '@/service/mine';
import { useRouter } from 'vue-router';
import useCommon from '@/hooks/common';
const { routeLeaveRecord, routeBackRecord } = useCommon();
const router = useRouter();
const aiNovelListRef = ref();
const props = defineProps<{
status: string;
}>();
const state = reactive({
pageNumber: <number>1,
pageSize: <number>10,
list: <any>[],
loading: <boolean>true,
status: props.status,
delId: '', // 当前删除ID
});
const openDelNovelDialog: any = inject('openDelNovelDialog');
/**
* 刷新组件
*/
const moreParams = reactive({
loading: <boolean>true,
refreshing: <boolean>false,
finished: <boolean>false,
isNoData: <boolean>false,
error: <boolean>false,
isHigehtMax: <boolean>false,
});
onMounted(() => {
routeLeaveRecord(aiNovelListRef.value);
routeBackRecord(aiNovelListRef.value);
});
const openPreview = (item: any) => {
if (item.status != 3) return;
router.push('/aiNovelDetail?id=' + item.id);
};
/**
* 获取列表
*/
const getList = async (type: string) => {
try {
let res: any = await MineApi.getAiTextNovelRecord({
pageNumber: state.pageNumber,
pageSize: state.pageSize,
status: state.status,
});
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (type === 'refresh') {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1 && state.list.length == 0) {
moreParams.isNoData = true;
}
if (list.length < state.pageSize) {
moreParams.finished = true;
}
console.log(res);
} catch (e: any) {
moreParams.loading = false;
moreParams.error = true;
}
};
getList('');
const delId = (id: string) => {
let newArr = [];
newArr = state.list.filter((item: any) => {
if (item.id != id) return item;
});
state.list = newArr;
if (state.list.length == 0) {
moreParams.finished = false;
moreParams.isNoData = true;
}
};
/**
* 上拉刷新
*/
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList('');
};
/**
* 下拉刷新
*/
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
moreParams.isNoData = false;
getList('refresh');
};
defineExpose({ delId });
</script>
<style lang="scss" scoped>
.aiRecordList {
height: calc(100vh - 44px * 3);
padding: 0 16px;
.recordBox {
margin: 0 auto 10px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 14px;
justify-content: flex-start;
.createdAt {
font-size: 12px;
color: #dcdcdc;
display: flex;
align-items: center;
justify-content: space-between;
.aiClose {
height: 12px;
width: 12px;
}
}
.imageBox {
display: flex;
align-items: center;
justify-content: flex-start;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
padding-bottom: 12px;
margin: 12px 0 12px;
.novelBox {
padding: 11px 10px;
background: rgba(255, 255, 255, 0.05);
width: 100%;
.content {
width: 100%;
border-radius: 4px;
color: #999;
font-size: 12px;
font-weight: 400;
line-height: 18px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 18;
-webkit-box-orient: vertical;
}
}
}
.processing {
width: 18px;
height: 18px;
}
.delBtn {
display: flex;
align-items: center;
color: #f52c56;
font-size: 12px;
}
.aiClose {
width: 18px;
height: 18px;
margin-right: 4px;
}
.statusBox {
display: flex;
align-items: center;
justify-content: space-between;
// padding-top: 12px;
.statusText {
width: 240px;
font-size: 12px;
color: #7a7c85;
word-break: break-all;
}
.errorText {
color: #f52c56;
line-height: 18px;
}
}
.statusBtn {
display: flex;
align-items: center;
justify-content: center;
width: 55px;
height: 22px;
border-radius: 4px;
background: rgba(255, 219, 158, 0.1);
color: #ffdb9e;
font-size: 12px;
font-weight: 400;
line-height: normal;
}
.failBtn {
display: flex;
align-items: center;
height: 22px;
border-radius: 4px;
color: rgba(255, 255, 255, 0.55);
font-size: 12px;
font-weight: 400;
line-height: normal;
}
}
}
</style>
+92
View File
@@ -0,0 +1,92 @@
<template>
<div class="aiRecordTab">
<van-tabs
v-model:active="state.activeValue"
animated
shrink
class="child_tabs"
title-active-color="#fff"
title-inactive-color="#989898"
>
<van-tab name="queue" title="排队">
<AiNovelList ref="novelChildRef1" status="1" />
</van-tab>
<van-tab name="success" title="成功">
<AiNovelList ref="novelChildRef2" status="2" />
</van-tab>
<van-tab name="fail" title="失败">
<AiNovelList ref="novelChildRef3" status="3" />
</van-tab>
</van-tabs>
</div>
</template>
<script lang="ts" setup>
import { ref, nextTick, reactive } from 'vue';
import { useRouter } from 'vue-router';
import AiNovelList from './aiNovelList.vue';
const router = useRouter();
const novelChildRef1 = ref<any>();
const novelChildRef2 = ref<any>();
const novelChildRef3 = ref<any>();
const state = reactive({
activeValue: <string>'queue',
});
nextTick(() => {
let name: any = router.currentRoute.value.query['name'];
if (name) {
state.activeValue = name;
} else {
state.activeValue = 'queue';
}
});
const delMethod = (id: string) => {
if (state.activeValue == 'queue') {
novelChildRef1.value.delId(id);
} else if (state.activeValue == 'success') {
novelChildRef2.value.delId(id);
} else if (state.activeValue == 'fail') {
novelChildRef3.value.delId(id);
}
};
defineExpose({ delMethod });
</script>
<style lang="scss" scoped>
.aiRecordTab {
.child_tabs {
:deep(> .van-tabs__wrap) {
height: 44px !important;
width: 200px !important;
.van-tabs__nav {
padding: 0;
background: transparent;
}
.van-tab {
flex: 1;
background: transparent !important;
&::after {
content: '';
width: 1px;
height: 12px;
background: rgba(255, 255, 255, 0.1);
position: absolute;
right: 0;
top: 15px;
}
}
.van-tab:nth-child(3) {
&::after {
display: none;
}
}
.van-tab--active {
background: transparent !important;
}
.van-tabs__line {
display: none;
}
}
}
}
</style>
@@ -0,0 +1,255 @@
<template>
<div class="aiRecordList overflow-y-auto">
<PullRefresh v-if="!state.loading" :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="recordBox" v-for="item in state.list" :key="item.id">
<div class="createdAt">
<span>创建时间:{{ dateFormat(item.createdAt) }}</span>
<SvgIcon
name="aiClose"
class="aiClose"
v-if="state.status == '2' || state.status == '3'"
@click.stop="openDelChangeTextCoverDialog(item.id)"
></SvgIcon>
</div>
<div class="imageBox">
<div class="originPicBox">
{{ item.text }}
</div>
<SvgIcon name="equalSign" class="equalSign"></SvgIcon>
<div class="resultImg" @click.stop="state.status == '2' ? openBigImg([item.newImgUrl]) : ''">
<div class="mask" v-if="state.status !== '2'">
<SvgIcon name="aiFail" class="aiFail" v-if="state.status == '3'"></SvgIcon>
</div>
<lazyImg class="cover" :src="state.status == '2' ? item.newImgUrl : item.styleUrl"></lazyImg>
</div>
</div>
<div class="statusBox">
<div class="statusBtn" v-if="state.status == '1'">生成中</div>
<div class="statusBtn" v-else-if="state.status == '2'">已完成</div>
<div class="failBtn" v-else-if="state.status == '3'">生成失败</div>
<div class="statusText errorText" v-if="state.status == '3'">
{{ item.remark }}
</div>
</div>
</div>
</PullRefresh>
<Loading v-else />
</div>
</template>
<script lang="ts" setup>
import { reactive, inject } from 'vue';
import { dateFormat } from '@/utils/index';
import MineApi from '@/service/mine';
const props = defineProps<{
status: string;
}>();
const state = reactive({
pageNumber: <number>1,
pageSize: <number>10,
list: <any>[],
loading: <boolean>true,
status: props.status,
delId: '', // 当前删除ID
});
const openBigImg: any = inject('openBigImg');
const openDelChangeTextCoverDialog: any = inject('openDelChangeTextCoverDialog');
const openReasonDialog: any = inject('openReasonDialog');
/**
* 刷新组件
*/
const moreParams = reactive({
loading: <boolean>true,
refreshing: <boolean>false,
finished: <boolean>false,
isNoData: <boolean>false,
error: <boolean>false,
isHigehtMax: <boolean>false,
});
/**
* 获取列表
*/
const getList = async (type: string) => {
try {
let res: any = await MineApi.getChangeTextCoverMod({
pageNumber: state.pageNumber,
pageSize: state.pageSize,
status: state.status,
});
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (type === 'refresh') {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1 && state.list.length == 0) {
moreParams.isNoData = true;
}
if (list.length < state.pageSize) {
moreParams.finished = true;
}
console.log(res);
} catch (e: any) {
moreParams.loading = false;
moreParams.error = true;
}
};
getList('');
const delId = (id: string) => {
let newArr = [];
newArr = state.list.filter((item: any) => {
if (item.id != id) return item;
});
state.list = newArr;
if (state.list.length == 0) {
moreParams.finished = false;
moreParams.isNoData = true;
}
};
/**
* 上拉刷新
*/
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList('');
};
/**
* 下拉刷新
*/
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
moreParams.isNoData = false;
getList('refresh');
};
defineExpose({ delId });
</script>
<style lang="scss" scoped>
.aiRecordList {
height: calc(100vh - 44px * 3);
padding: 0 16px;
.recordBox {
margin: 0 auto 10px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 14px;
justify-content: flex-start;
.createdAt {
font-size: 12px;
color: #dcdcdc;
display: flex;
align-items: center;
justify-content: space-between;
.aiClose {
height: 12px;
width: 12px;
}
}
.processing {
width: 18px;
height: 18px;
}
.delBtn {
display: flex;
align-items: center;
color: #f52c56;
font-size: 12px;
}
.aiClose {
width: 18px;
height: 18px;
margin-right: 4px;
}
.imageBox {
display: flex;
align-items: center;
justify-content: flex-start;
border-bottom: 1px solid #ffffff0d;
padding-bottom: 12px;
margin: 12px 0 12px;
.originPicBox {
width: 181px;
padding: 10px 12px;
border-radius: 8px;
color: #999999;
font-size: 12px;
line-height: 20px;
background: #ffffff0d;
}
.equalSign {
width: 18px;
height: 18px;
margin: 0 3px;
}
.resultImg {
width: 90px;
height: 90px;
border-radius: 8px;
overflow: hidden;
position: relative;
.mask {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
.aiFail {
width: 24px;
height: 24px;
}
}
}
}
.statusBox {
display: flex;
align-items: center;
justify-content: space-between;
.statusText {
width: 240px;
font-size: 12px;
color: #7a7c85;
word-break: break-all;
}
.errorText {
color: #f52c56;
line-height: 18px;
}
}
.statusBtn {
display: flex;
align-items: center;
justify-content: center;
width: 55px;
height: 22px;
border-radius: 4px;
background: rgba(255, 219, 158, 0.1);
color: #ffdb9e;
font-size: 12px;
font-weight: 400;
line-height: normal;
}
.failBtn {
display: flex;
align-items: center;
height: 22px;
border-radius: 4px;
color: rgba(255, 255, 255, 0.55);
font-size: 12px;
font-weight: 400;
line-height: normal;
}
}
}
</style>
@@ -0,0 +1,88 @@
<template>
<div class="aiRecordTab">
<van-tabs
v-model:active="state.activeValue"
animated
shrink
class="child_tabs"
title-active-color="#fff"
title-inactive-color="#989898"
>
<van-tab name="queue" title="排队">
<AiTextCoverList ref="textCoverChildRef1" status="1" />
</van-tab>
<van-tab name="success" title="成功">
<AiTextCoverList ref="textCoverChildRef2" status="2" />
</van-tab>
<van-tab name="fail" title="失败">
<AiTextCoverList ref="textCoverChildRef3" status="3" />
</van-tab>
</van-tabs>
</div>
</template>
<script lang="ts" setup>
import { ref, nextTick, reactive } from 'vue';
import { useRouter } from 'vue-router';
import AiTextCoverList from './aiTextCoverList.vue';
const router = useRouter();
const textCoverChildRef1 = ref<any>();
const textCoverChildRef2 = ref<any>();
const textCoverChildRef3 = ref<any>();
const state = reactive({
activeValue: <string>'queue',
});
nextTick(() => {
let name: any = router.currentRoute.value.query['name'];
if (name) {
state.activeValue = name;
} else {
state.activeValue = 'queue';
}
});
const delMethod = (id: string) => {
if (state.activeValue == 'queue') {
textCoverChildRef1.value.delId(id);
} else if (state.activeValue == 'success') {
textCoverChildRef2.value.delId(id);
} else if (state.activeValue == 'fail') {
textCoverChildRef3.value.delId(id);
}
};
defineExpose({ delMethod });
</script>
<style lang="scss" scoped>
.aiRecordTab {
.child_tabs {
:deep(> .van-tabs__wrap) {
padding: 0 16px;
.van-tabs__nav {
background: transparent;
padding: 0;
}
.van-tab {
padding: 0 16px;
}
.van-tab::before {
content: '';
width: 1px;
height: 12px;
background: #ffffff1a;
position: absolute;
right: 0;
}
.van-tab:first-child {
padding-left: 0;
}
.van-tab:nth-child(3)::before {
display: none;
}
.van-tabs__line {
display: none;
}
}
}
}
</style>
@@ -0,0 +1,299 @@
<template>
<div class="aiRecordList overflow-y-auto">
<PullRefresh v-if="!state.loading" :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="recordBox" v-for="item in state.list" :key="item.id">
<div class="createdAt">
<span>创建时间:{{ dateFormat(item.createdAt) }}</span>
<SvgIcon
name="aiClose"
class="aiClose"
v-if="state.status == '2' || state.status == '3'"
@click.stop="openDelUndressDialog(item.id)"
></SvgIcon>
</div>
<div class="imageBox">
<div class="originPicBox" @click.stop="openBigImg(item?.originPics)">
<div class="lable">素材</div>
<lazyImg class="cover" :src="item?.originPics && item?.originPics[0]" />
</div>
<SvgIcon name="equalSign" class="equalSign"></SvgIcon>
<div class="resultImg" @click.stop="item.status == 2 ? openBigImg(item.newPic) : ''">
<div class="mask" v-if="state.status !== '2'">
<SvgIcon name="aiFail" class="aiFail" v-if="state.status == '3'"></SvgIcon>
</div>
<lazyImg
class="cover"
:src="state.status == '2' ? item.newPic && item.newPic[0] : item.originPics && item.originPics[0]"
></lazyImg>
</div>
</div>
<div class="statusBox">
<div class="statusBtn" v-if="state.status == '1'">生成中</div>
<div class="statusBtn" v-else-if="state.status == '2'">已完成</div>
<div class="failBtn" v-else-if="state.status == '3'">生成失败</div>
<div class="statusText errorText" v-if="state.status == '3'">
{{ item.remark }}
</div>
</div>
</div>
</PullRefresh>
<Loading v-else />
</div>
</template>
<script lang="ts" setup>
import { reactive, inject } from 'vue';
import { dateFormat } from '@/utils/index';
import MineApi from '@/service/mine';
const props = defineProps<{
status: string;
}>();
const state = reactive({
pageNumber: <number>1,
pageSize: <number>10,
list: <any>[],
loading: <boolean>true,
status: props.status,
delId: '', // 当前删除ID
});
const openBigImg: any = inject('openBigImg');
const openDelUndressDialog: any = inject('openDelUndressDialog');
// const openReasonDialog: any = inject('openReasonDialog');
/**
* 刷新组件
*/
const moreParams = reactive({
loading: <boolean>true,
refreshing: <boolean>false,
finished: <boolean>false,
isNoData: <boolean>false,
error: <boolean>false,
isHigehtMax: <boolean>false,
});
/**
* 获取列表
*/
const getList = async (type: string) => {
try {
let res: any = await MineApi.getAiUndressRecord({
pageNumber: state.pageNumber,
pageSize: state.pageSize,
status: state.status,
});
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (type === 'refresh') {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1 && state.list.length == 0) {
moreParams.isNoData = true;
}
if (list.length < state.pageSize) {
moreParams.finished = true;
}
console.log(res);
} catch (e: any) {
moreParams.loading = false;
moreParams.error = true;
}
};
getList('');
const delId = (id: string) => {
let newArr = [];
newArr = state.list.filter((item: any) => {
if (item.id != id) return item;
});
state.list = newArr;
if (state.list.length == 0) {
moreParams.finished = false;
moreParams.isNoData = true;
}
};
/**
* 上拉刷新
*/
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList('');
};
/**
* 下拉刷新
*/
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
moreParams.isNoData = false;
getList('refresh');
};
defineExpose({ delId });
</script>
<style lang="scss" scoped>
.aiRecordList {
height: calc(100vh - 44px * 3);
padding: 0 16px;
.recordBox {
margin: 0 auto 10px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 12px;
justify-content: flex-start;
.createdAt {
font-size: 12px;
color: #dcdcdc;
display: flex;
align-items: center;
justify-content: space-between;
.aiClose {
height: 12px;
width: 12px;
}
}
.processing {
width: 18px;
height: 18px;
}
.delBtn {
display: flex;
align-items: center;
color: #f52c56;
font-size: 12px;
}
.aiClose {
width: 18px;
height: 18px;
margin-right: 4px;
}
.imageBox {
display: flex;
align-items: center;
justify-content: flex-start;
border-bottom: 1px solid #ffffff0d;
padding-bottom: 12px;
margin: 12px 0 12px;
.modCover {
width: 90px;
height: 90px;
border-radius: 8px;
overflow: hidden;
position: relative;
.lable {
position: absolute;
top: 0;
left: 0;
width: 36px;
height: 18px;
font-size: 12px;
border-radius: 8px 0px;
color: rgba(41, 41, 41, 1);
background: rgba(255, 212, 96, 1);
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
:deep(img) {
width: 100%;
height: 100% !important;
object-fit: cover !important;
}
}
.originPicBox {
width: 90px;
height: 90px;
border-radius: 8px;
overflow: hidden;
position: relative;
.lable {
position: absolute;
top: 0;
left: 0;
width: 36px;
height: 18px;
font-size: 12px;
border-radius: 8px 0px;
color: #000;
background: #03fceb;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
}
.equalSign {
width: 18px;
height: 18px;
margin: 0 3px;
color: rgba(152, 152, 152, 1);
}
.resultImg {
width: 90px;
height: 90px;
border-radius: 8px;
overflow: hidden;
position: relative;
.mask {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
.aiFail {
width: 24px;
height: 24px;
}
}
}
}
.statusBox {
display: flex;
align-items: center;
justify-content: space-between;
.statusText {
width: 240px;
font-size: 12px;
color: rgba(152, 152, 152, 1);
word-break: break-all;
}
.errorText {
color: #f52c56;
line-height: 18px;
}
}
.statusBtn {
display: flex;
align-items: center;
justify-content: center;
width: 55px;
height: 22px;
border-radius: 4px;
background: rgba(255, 219, 158, 0.1);
color: #ffdb9e;
font-size: 12px;
font-weight: 400;
line-height: normal;
}
.failBtn {
display: flex;
align-items: center;
height: 22px;
border-radius: 4px;
color: rgba(255, 255, 255, 0.55);
font-size: 12px;
font-weight: 400;
line-height: normal;
}
}
}
</style>
@@ -0,0 +1,87 @@
<template>
<div class="aiRecordTab">
<van-tabs
v-model:active="state.activeValue"
animated
shrink
class="child_tabs"
title-active-color="#fff"
title-inactive-color="#989898"
>
<van-tab name="queue" title="排队">
<AiUndressList ref="undressChildRef1" status="1" />
</van-tab>
<van-tab name="success" title="成功">
<AiUndressList ref="undressChildRef2" status="2" />
</van-tab>
<van-tab name="fail" title="失败">
<AiUndressList ref="undressChildRef3" status="3" />
</van-tab>
</van-tabs>
</div>
</template>
<script lang="ts" setup>
import { ref, nextTick, reactive } from 'vue';
import { useRouter } from 'vue-router';
import AiUndressList from './aiUndressList.vue';
const router = useRouter();
const undressChildRef1 = ref<any>();
const undressChildRef2 = ref<any>();
const undressChildRef3 = ref<any>();
const state = reactive({
activeValue: <string>'queue',
});
nextTick(() => {
let name: any = router.currentRoute.value.query['name'];
if (name) {
state.activeValue = name;
} else {
state.activeValue = 'queue';
}
});
const delMethod = (id: string) => {
if (state.activeValue == 'queue') {
undressChildRef1.value.delId(id);
} else if (state.activeValue == 'success') {
undressChildRef2.value.delId(id);
} else if (state.activeValue == 'fail') {
undressChildRef3.value.delId(id);
}
};
defineExpose({ delMethod });
</script>
<style lang="scss" scoped>
.aiRecordTab {
.child_tabs {
:deep(> .van-tabs__wrap) {
padding: 0 16px;
.van-tabs__nav {
background: transparent;
padding: 0;
}
.van-tab {
padding: 0 16px;
}
.van-tab::before {
content: '';
width: 1px;
height: 12px;
background: #ffffff1a;
position: absolute;
right: 0;
}
.van-tab:first-child {
padding-left: 0;
}
.van-tab:nth-child(3)::before {
display: none;
}
.van-tabs__line {
display: none;
}
}
}
}
</style>
+407
View File
@@ -0,0 +1,407 @@
<template>
<div class="aiRecord">
<Header title="生成记录" />
<van-tabs
v-model:active="state.active"
title-active-color="#fff"
title-inactive-color="#FFFFFF8C"
class="aiRecord_tabs openX"
shrink
swipeable
animated
@touchmove.stop
>
<van-tab v-for="(feature, index) in enabledAiFeatures" :key="feature.type" :name="feature.type">
<template #title>
<!-- 图生视频 (type: 4) -->
<template v-if="feature.type === 4">图生视频</template>
<!-- 文生图 (type: 5) -->
<template v-else-if="feature.type === 5">AI绘画</template>
<!-- AI脱衣 (type: 1) -->
<template v-else-if="feature.type === 1">AI脱衣</template>
<!-- AI视频换脸 (type: 2) -->
<template v-else-if="feature.type === 2">视频换脸</template>
<!-- AI图片换脸 (type: 3) -->
<template v-else-if="feature.type === 3">图片换脸</template>
<!-- AI小说 (type: 6) -->
<template v-else-if="feature.type === 6">AI小说</template>
</template>
<div class="content overflow-y-auto">
<!-- 图生视频内容 -->
<AiImgToVidListTab v-if="feature.type === 4" ref="picture2VideoRef" />
<!-- 文生图内容 -->
<AiTextCoverTab v-else-if="feature.type === 5" ref="textCoverRef" />
<!-- AI脱衣内容 -->
<AiUndressTab v-else-if="feature.type === 1" ref="undressRef" />
<!-- AI视频换脸内容 -->
<AiChangeVideoFaceTab v-else-if="feature.type === 2" ref="changeVideoFaceRef" />
<!-- AI图片换脸内容 -->
<AiChangeImgFaceTab v-else-if="feature.type === 3" ref="changeImgFaceRef" />
<!-- AI小说内容 -->
<AiNovelTab v-else-if="feature.type === 6" ref="aiNovelRef" />
</div>
</van-tab>
</van-tabs>
<van-overlay :show="state.showBigImg" @click="state.showBigImg = false" v-if="state.showBigImg" z-index="9999">
<div class="swiperWrapper">
<van-swipe @change="onChange" class="my-swipe" :show-indicators="false" ref="swipeRef">
<van-swipe-item v-for="item in state.imgUrls" :key="item">
<lazyImg class="tag-img" :src="item" :key="item"></lazyImg>
</van-swipe-item>
</van-swipe>
<div class="custom-indicator">
{{ Number(state.currentIndex) + 1 }}/{{ state.imgUrls ? state.imgUrls.length : 0 }}
</div>
</div>
</van-overlay>
<van-overlay :show="state.showPlayVideo" z-index="9999">
<SvgIcon name="close" class="close" @click="state.showPlayVideo = false"></SvgIcon>
<div class="wrapper blackBg">
<SmallVideo
v-if="state.showPlayVideo"
class="videoBox"
:sourceURL="state.sourceURL"
:h265Url="state.h265Url"
:key="state.sourceURL"
/>
</div>
</van-overlay>
<van-overlay :show="state.showDelDialog" @click="state.showDelDialog = false" z-index="9999">
<div class="wrapper">
<div class="delBox">
<div class="title">删除</div>
<div class="content_text">您是否移除该内容?</div>
<div class="btnBox flex-center-between">
<div class="cancel" @click="state.showDelDialog = false">取消</div>
<div class="confirm" @click="confirm">确定删除</div>
</div>
</div>
</div>
</van-overlay>
<van-overlay :show="state.showReasonDialog" @click="state.showReasonDialog = false" z-index="9999">
<div class="wrapper">
<div class="delBox">
<div class="title">拒绝原因</div>
<div class="reason">{{ state.reason }}</div>
<div class="confirm2" @click="state.showReasonDialog = false">我知道了</div>
</div>
</div>
</van-overlay>
</div>
</template>
<script lang="ts">
export default {
name: 'aiRecord',
};
</script>
<script lang="ts" setup>
import { ref, reactive, provide, computed } from 'vue';
import AiUndressTab from './aiUndressTab.vue';
import AiImgToVidListTab from './aiImgToVidListTab.vue';
import AiChangeVideoFaceTab from './aiChangeVideoFaceTab.vue';
import AiChangeImgFaceTab from './aiChangeImgFaceTab.vue';
import AiTextCoverTab from './aiTextCoverTab.vue';
import AiNovelTab from './aiNovelTab.vue';
import MineApi from '@/service/mine';
import { showToast } from 'vant';
import { useRoute } from 'vue-router';
import { useConfigStore } from '@/stores/config';
const picture2VideoRef = ref<any>();
const undressRef = ref<any>();
const textCoverRef = ref<any>();
const aiNovelRef = ref<any>();
const changeVideoFaceRef = ref<any>();
const changeImgFaceRef = ref<any>();
const route = useRoute();
const configStore = useConfigStore();
const state = reactive({
active: 0,
showBigImg: false,
imgUrls: [], // 需要显示的图片数组
currentIndex: 0,
showDelDialog: false,
showReasonDialog: false,
reason: '', // 拒绝原因
type: '', // 脱衣删除还是换脸删除
delId: '', // 删除ID
showPlayVideo: false,
sourceURL: '', // 换脸后的视频链接
h265Url: '' as string | null,
});
if (route.query.activeTab) {
state.active = Number(route.query.activeTab);
}
const aiSwitchConf = computed(() => {
if (configStore.appConfig.aiSwitchConf) {
return configStore.appConfig.aiSwitchConf.map((item, index) => {
return item;
});
} else {
return [];
}
});
// 根据配置获取启用的AI功能列表,按sort排序
const enabledAiFeatures = computed(() => {
return aiSwitchConf.value.filter((item) => item.isOpen && item.type !== 7).sort((a, b) => a.sort - b.sort);
});
const confirm = async () => {
let req = {
id: state.delId,
};
try {
if (state.type == 'undress') {
await MineApi.delelteUndress(req);
undressRef.value.delMethod(state.delId);
} else if (state.type == 'changeVideoFace') {
await MineApi.delelteChangeVideoFace(req);
changeVideoFaceRef.value.delMethod(state.delId);
} else if (state.type == 'changeImgFace') {
await MineApi.delelteChangeImgFace(req);
changeImgFaceRef.value.delMethod(state.delId);
} else if (state.type === 'imgToVideo') {
await MineApi.delelteImageToVideo(req);
picture2VideoRef.value.delMethod(state.delId);
} else if (state.type === 'textCover') {
await MineApi.delelteChangeTextCover(req);
textCoverRef.value.delMethod(state.delId);
} else if (state.type === 'aiNovel') {
await MineApi.delelteAiTextNovel(req);
aiNovelRef.value.delMethod(state.delId);
}
state.delId = '';
state.showDelDialog = false;
showToast('删除成功');
} catch (e) {
console.log(e);
}
};
const openBigImg = (urls: any) => {
state.currentIndex = 0;
state.imgUrls = urls;
state.showBigImg = true;
};
const openPlayVideo = (sourceURL: string, h265Url?: string | null) => {
state.sourceURL = sourceURL;
state.h265Url = h265Url || '';
state.showPlayVideo = true;
};
const onChange = (index: any) => {
state.currentIndex = index;
};
const openDelUndressDialog = (delId: string) => {
state.delId = delId;
state.type = 'undress';
state.showDelDialog = true;
};
const openDelChangeVideoFaceDialog = (delId: string) => {
state.delId = delId;
state.type = 'changeVideoFace';
state.showDelDialog = true;
};
const openDelChangeImgFaceDialog = (delId: string) => {
state.delId = delId;
state.type = 'changeImgFace';
state.showDelDialog = true;
};
const openReasonDialog = (reason: string) => {
state.reason = reason;
state.showReasonDialog = true;
};
const openDelChangeImgToVideoDialog = (delId: string) => {
state.delId = delId;
state.type = 'imgToVideo';
state.showDelDialog = true;
};
const openDelChangeTextCoverDialog = (delId: string) => {
state.delId = delId;
state.type = 'textCover';
state.showDelDialog = true;
};
const openDelNovelDialog = (delId: string) => {
state.delId = delId;
state.type = 'aiNovel';
state.showDelDialog = true;
};
provide('openBigImg', openBigImg);
provide('openPlayVideo', openPlayVideo);
provide('openDelUndressDialog', openDelUndressDialog);
provide('openDelChangeVideoFaceDialog', openDelChangeVideoFaceDialog);
provide('openDelChangeImgToVideoDialog', openDelChangeImgToVideoDialog);
provide('openDelChangeImgFaceDialog', openDelChangeImgFaceDialog);
provide('openDelChangeTextCoverDialog', openDelChangeTextCoverDialog);
provide('openReasonDialog', openReasonDialog);
provide('openDelNovelDialog', openDelNovelDialog);
</script>
<style lang="scss" scoped>
.aiRecord {
width: 100%;
height: 100%;
background: #0f0f0f;
.aiRecord_tabs {
:deep(> .van-tabs__wrap) {
// padding: 0 8px;
z-index: 10;
.van-tabs__nav {
padding: 0;
background: #000;
justify-content: space-between;
}
.van-tab {
font-size: 14px;
}
.van-tab--active {
font-size: 16px;
}
.van-tabs__line {
bottom: 4px;
width: 13px;
height: 3px;
background: linear-gradient(90deg, rgba(255, 192, 0, 0) 0%, #ffc000 100%);
}
}
}
}
.swiperWrapper {
background: black;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
.my-swipe {
height: 80vh;
background: black;
color: #fff;
text-align: center;
.tag-img {
width: 100vw;
height: 100%;
}
}
.custom-indicator {
position: fixed;
bottom: 40px;
left: 50%;
transform: translate(-50%, 0);
font-size: 16px;
color: white;
}
}
.close {
height: 30px;
width: 30px;
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
}
.wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
.delBox {
width: 303px;
background: #202133;
padding: 20px 16px;
border-radius: 8px;
.title {
font-size: 16px;
color: #3476ff;
text-align: center;
padding-bottom: 20px;
border-bottom: 1px solid #ffffff1a;
}
.content_text {
margin: 24px 0;
text-align: center;
}
.reason {
text-align: center;
font-size: 12px;
line-height: 22px;
margin: 24px 0;
}
.btnBox {
margin-top: 20px;
.cancel {
width: 112px;
height: 42px;
text-align: center;
line-height: 42px;
border-radius: 99px;
font-size: 14px;
color: #fff;
background: #30324d;
}
.confirm {
width: 144px;
height: 42px;
text-align: center;
line-height: 42px;
border-radius: 99px;
font-size: 14px;
color: #fff;
background: #3476ff;
}
}
.confirm2 {
margin: 0 auto;
width: 244px;
height: 42px;
text-align: center;
line-height: 42px;
border-radius: 99px;
font-size: 14px;
color: #fff;
background: #3476ff;
}
}
.videoBox {
height: 100%;
width: 100%;
background-color: pink;
}
.reasonBox {
.title {
font-size: 16px;
text-align: center;
}
.reason {
font-size: 14px;
text-align: center;
margin-top: 20px;
padding: 0 16px;
}
}
}
.blackBg {
background: black;
}
:deep(.vanImage) {
display: flex;
align-items: center;
}
:deep(img) {
height: auto !important;
}
// :deep(.van-overlay) {
// background: black;
// }
:deep(.van-popup--center) {
max-width: unset;
}
</style>
+104
View File
@@ -0,0 +1,104 @@
<template>
<!-- 列表数据 -->
<div class="aiPiazza overflow-y-auto" id="aiPiazzaId">
<template v-if="!state.loading">
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<AiCard v-for="(item, index) in state.list" :key="index" :item="item" @actionType="onActionType" />
</PullRefresh>
</template>
<Loading v-else />
</div>
</template>
<script setup lang="ts">
import { reactive, nextTick } from 'vue';
import CommunityApi from '@/service/community';
import AiCard from './components/aiCard.vue';
import TabPageScroll from '@/utils/resetTabPageScrollTop';
const tabPageScroll = new TabPageScroll();
setTimeout(() => {
tabPageScroll.add(`#aiPiazzaId`);
}, 100);
const emit = defineEmits(['actionType']);
// 列表数据项
const state = reactive({
list: <Types.Media.AiMediaBoxItem[]>[],
pageNumber: 1,
pageSize: 10,
loading: true,
});
// PullRefresh 参数
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 列表获取
const getList = async (type = '') => {
const pamrms = {
pageNumber: state.pageNumber,
pageSize: state.pageSize,
sortType: 1,
// type: , //1-ai图片换脸 2-ai视频换脸 3-ai脱衣 4-ai图生视频 5-ai绘画
};
try {
let res: any = await CommunityApi.queryAiSquareList(pamrms);
const list = res.list || [];
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
// 数据重置
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
// 触底加载
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
// 下拉刷新
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
const onActionType = (item: any) => {
emit('actionType', item);
};
</script>
<style scoped lang="scss">
.aiPiazza {
height: calc(100vh - 44px * 2 - 55px);
padding: 16px 16px 0;
}
</style>
@@ -0,0 +1,72 @@
<template>
<div class="ai-text-novel-detail">
<Header :title="title" />
<div class="ai-text-novel-detail__content overflow-y-auto">
<rich-preview :text="text"></rich-preview>
</div>
</div>
</template>
<script setup lang="ts">
import MineApi from '@/service/mine';
import { onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
const text = ref('');
const title = ref('小说标题');
onMounted(async () => {
try {
const id = route.query.id;
const r: any = await MineApi.getAiTextNovelDetail({ id });
text.value = r.content;
title.value = r.title || r.name || '小说标题';
} catch (error) {
console.error(error);
}
});
</script>
<style scoped lang="scss">
.ai-text-novel-detail {
height: 100%;
background: #000;
:deep(.header) {
height: 48px;
.title {
color: rgba(255, 255, 255, 0.9);
font-size: 18px;
font-weight: 600;
line-height: 24px;
max-width: 260px;
}
}
&__content {
height: calc(100% - 48px);
padding: 12px 16px;
:deep(.rich-preview .content) {
margin: 0;
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
font-weight: 400;
line-height: 20px;
text-align: justify;
white-space: pre-wrap;
word-break: break-word;
p {
margin: 0 0 18px;
&:last-child {
margin-bottom: 0;
}
}
}
}
}
</style>
@@ -0,0 +1,208 @@
<template>
<div class="ai-text overflow-y-auto">
<div class="ai-text__content">
<section v-for="(io, i) in inputOptions" :key="i">
<label for="">
{{ io.label }}
<span v-if="io.required">*</span>
</label>
<van-field
v-model="io.text"
:maxlength="io.limit"
:placeholder="io.placeholder"
rows="3"
autosize
type="textarea"
show-word-limit
/>
</section>
<section class="model-types">
<label for="">选择模型</label>
<div class="model-types__value">
<van-button
v-for="(mo, i) in modelOptions"
:key="i"
:type="modelIndex === i ? 'primary' : 'default'"
@click="modelIndex = i"
>
{{ mo.label }}
</van-button>
</div>
</section>
<footer>
<p class="price">
生成一次AI小说费用是
<span>{{ store.appConfig.aiTextToNovelPrice }}金币</span>
</p>
<van-button class="submit-btn" type="primary" @click="submit">立即提交</van-button>
</footer>
</div>
</div>
</template>
<script setup lang="ts">
import MineApi from '@/service/mine';
import { useConfigStore } from '@/stores/config';
import { showToast } from 'vant';
import { useRouter } from 'vue-router';
import { reactive, ref } from 'vue';
const store = useConfigStore();
const router = useRouter();
const inputOptions = reactive([
{
label: '人物设定(选填)',
placeholder: '示例:“总裁、公主、小萝莉.”',
limit: 20,
required: false,
key: 'characterSetting',
text: '',
},
{
label: '地点场景(选填)',
placeholder: '示例:“办公室、酒吧”',
limit: 20,
required: false,
key: 'locationScene',
text: '',
},
{
label: '故事情节(必填)',
placeholder: '故事大致情节,例如:35岁女强人在酒吧遇到跳钢管舞的男模后,欲火焚身、欲罢不能。',
limit: 200,
required: true,
key: 'description',
text: '',
},
{
label: '细节说明(选填)',
placeholder: '例如:请详细描写女主身材美貌,与霸总的做爱过程',
limit: 200,
required: false,
key: 'details',
text: '',
},
]);
const modelIndex = ref(0);
const modelOptions = [
{ label: 'AI小艺', value: 1 },
{ label: 'AI小萌', value: 2 },
];
const validateInputInfo = () => {
const inputObj: Record<string, any> = {};
for (const io of inputOptions) {
if (io.required && !io.text) {
showToast('请输入' + io.label);
return false;
}
if (io.text) inputObj[io.key] = io.text;
}
return inputObj;
};
const resetInputs = () => {
inputOptions.forEach((io) => (io.text = ''));
};
const submit = async () => {
try {
const pms = validateInputInfo();
if (!pms) return;
pms.modelType = modelOptions[modelIndex.value].value;
await MineApi.generateTextToNovel(pms);
resetInputs();
showToast('提交成功,等待生成');
} catch (error) {
console.error('generate text to novel error:', error);
router.push('/wallet');
}
};
</script>
<style scoped lang="scss">
.ai-text {
padding: 0 10px 120px;
height: calc(100vh - 78px - 48px * 2);
&__content {
padding: 12px 0;
label {
display: block;
color: #fff;
font-size: 14px;
margin: 12px 0;
span {
color: $themeColor;
}
}
.van-field {
--van-cell-vertical-padding: 5px;
--van-cell-background: #1e1d1d;
--van-field-word-limit-color: #999;
--van-field-input-text-color: #fff;
--van-field-placeholder-text-color: #747171;
--van-cell-vertical-padding: 11px;
border-radius: 8px;
font-size: 12px;
line-height: 1.5;
&::after {
border: none;
}
}
.model-types {
&__value {
display: flex;
flex-wrap: wrap;
gap: 10px;
.van-button {
--van-button-default-background: rgba(255, 255, 255, 0.05);
--van-button-primary-background: #ffc000;
--van-button-primary-border-color: #ffc000;
--van-button-primary-color: #1f2029;
--van-button-default-border-color: transparent;
--van-button-border-width: 0;
--van-button-normal-padding: 0 10px;
--van-button-default-color: #fff;
--van-button-default-color: rgba(255, 255, 255, 0.5);
height: 28px;
font-size: 14px;
}
}
}
}
.price {
font-size: 12px;
color: #747171;
text-align: center;
margin-top: 18px;
span {
color: $themeColor;
}
}
.submit-btn {
margin: 20px auto 0;
display: block;
width: 343px;
height: 44px;
border: none;
background-color: $themeColor;
border-radius: 88px;
color: #1f2029;
font-weight: 500;
}
footer {
//position: sticky;
//padding-top: 10px;
//padding-bottom: calc(50px + env(safe-area-inset-bottom));
//bottom: 0;
//background-color: #0f0f0f;
}
}
</style>
@@ -0,0 +1,627 @@
<template>
<div class="mainbox">
<div class="header">
<SvgIcon name="back_white" class="back" @click="$router.go(-1)" />
<div class="title">{{ state.deatil.title }}</div>
</div>
<div class="content overflow-y-auto">
<!-- 视频 -->
<div class="cover" v-if="state.type == 'video'" @click="state.showPlayVideo = true">
<lazyImg :src="state.deatil.cover" class="modCover">
<div class="play_box">
<SvgIcon name="playIcon" />
</div>
</lazyImg>
</div>
<!-- 图片 -->
<div class="changImage" v-if="state.type == 'image'">
<lazyImg :src="state.deatil.cover" class="modCover"></lazyImg>
</div>
<div class="mess">
<h2>注意事项:</h2>
<p>1. 选择一张人脸清晰,不得有任何遮挡的照片上传(注意:只含一个人物和脸部,图片不能过暗)</p>
<p>2. 选择一个心仪的视频或图片模板,点击生成,生成时间需要3-5分钟,耐心等待。(图片模板可自行上传)</p>
<p>3. 在右上角记录查看生成进度,生成成功后可以点击进行下载,也可以在线观看。</p>
<p>4. 不支持多人图片,禁止未成年人图片</p>
</div>
<div class="imgmess">
<div>
<img src="@/assets/images/png/aiTip3.png" alt="" />
正面无遮挡
</div>
<div>
<img src="@/assets/images/png/aiTip2.png" alt="" />
不遮挡脸部
</div>
<div>
<img src="@/assets/images/png/aiTip1.png" alt="" />
不夸张表情
</div>
</div>
<div class="title">
上传脸部信息
<span>图片大小请低于2Mb</span>
</div>
<van-uploader
@click.stop
v-model="state.fileList"
:after-read="afterRead"
:before-read="beforeRead"
:before-delete="beforeDelete"
>
<template #default>
<div class="uploadBox flex-center-center">
<img src="@/assets/images/png/addFile.png" alt="" />
<!-- 图片大小不超过2Mb -->
上传图片
</div>
</template>
<template #preview-delete>
<SvgIcon name="previewDelete" class="previewDelete"></SvgIcon>
</template>
</van-uploader>
<div class="tips-box">
<div class="tips-item" v-if="state.type == 'image'">你当前免费体验为{{ state.freeCount }}次</div>
<div class="tips-item" v-if="state.type == 'image'">当日免费{{ state.todayFreeCount }}次数</div>
<div class="tips-item">
处理一张照片的费用是
<span class="red-text">{{ state.aiUndressPrice }}金币</span>
</div>
<div class="tips-item" v-if="state.couponDetail?.id">
已抵扣
<span class="red-text">{{ state.couponDetail.goodsValue || 0 }}金币</span>
</div>
<div class="tips-item" v-if="state.type == 'video'">
<span v-if="state.couponDetail?.id" class="green-text" @click="state.showAiGoldPopup = true">更换抵扣券</span>
<span v-else class="green-text" @click="state.showAiGoldPopup = true">使用抵扣券</span>
<span v-if="state.couponDetail?.id" class="cancel-btn" @click="state.couponDetail = {}">取消</span>
</div>
</div>
<!-- <ShareBox @checkedChange="checkedChange" @textChange="textChange" ref="shareBoxRef" />-->
</div>
<div class="bottom">
<!-- <div class="moneys" v-if="state.type == 'video'">
消耗金币:{{ userStore.isVip ? state.deatil.vipCoin : state.deatil.coin }}
</div>
<div class="moneys" v-else>
消耗金币:{{ state.aiUndressPrice || 0 }}
<p v-if="state.freeCount > 0">
免费次数:
<span>{{ state.freeCount }}</span>
今日可用次数:
<span>{{ state.todayFreeCount }}</span>
</p>
<p v-else>你当前没有免费体验次数</p>
<p>
免费次数:
<span>{{ state.freeCount }}</span>
</p>
</div> -->
<!-- {{ state.aiUndressPrice || 0 }}金币/ -->
<div class="btn" @click="onSubmit">立即提交</div>
</div>
<van-overlay :show="state.showPlayVideo" z-index="9999">
<SvgIcon name="back_white" class="close" @click="state.showPlayVideo = false"></SvgIcon>
<div class="wrapper blackBg">
<SmallVideo
v-if="state.showPlayVideo"
class="videoBox"
:sourceURL="state.deatil.m3u8Url"
:h265Url="state.deatil.h265Url"
:key="state.deatil.m3u8Url"
/>
</div>
</van-overlay>
<AiGoldPopup v-model:isOpen="state.showAiGoldPopup" @useCoupon="handleUseCoupon" />
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { showToast } from 'vant';
import AppApi from '@/service/app';
import MineApi from '@/service/mine';
import UserApi from '@/service/user';
import { useGlobalStore } from '@/stores/global';
import { useUserStore } from '@/stores/user';
import { useRoute } from 'vue-router';
import ShareBox from './components/share_box.vue';
const route = useRoute();
const userStore = useUserStore();
const globalStore = useGlobalStore();
const state = reactive({
showPlayVideo: false,
uploadImages: <Array<string>>[],
userWallet: <any>{},
fileSize: 2 * 1024 * 1024,
fileList: [],
freeCount: 0,
totalPrice: 0, // ai脱衣总售价
aiUndressPrice: 0, // ai脱衣的售价
deatil: <any>{},
timer: <any>'',
type: <any>'',
todayFreeCount: 0,
showAiGoldPopup: false,
couponDetail: <any>{},
shareStatus: 1, // 分享状态
shareTitle: '', // 分享标题
});
const init = () => {
UserApi.queryWallet().then((res) => {
state.userWallet = res;
state.freeCount = state.userWallet.aiUndressFreeTimes || 0;
state.todayFreeCount = state.userWallet.todayAiFreeTimes || 0;
});
};
init();
const shareBoxRef = ref();
const checkedChange = (status: number) => {
state.shareStatus = status;
};
const textChange = (text: string) => {
state.shareTitle = text;
};
// 获取详情
const getDeatil = async () => {
let data = localStorage.getItem('deatilData');
if (data) {
state.deatil = JSON.parse(data);
state.aiUndressPrice = userStore.isVip ? state.deatil.vipCoin : state.deatil.coin || 0;
}
state.type = route.query.type;
};
getDeatil();
const beforeRead = (file: any) => {
if (file.size > state.fileSize) {
showToast('文件不可超过2M');
return false;
}
return true;
};
// 图片上传
const afterRead = async (file: any) => {
if (file.length) {
file.map(async (item: any) => {
let newFile = await compress(item.file);
item.file = newFile;
await uploadImg(item);
});
} else {
let newFile = await compress(file.file);
file.file = newFile;
uploadImg(file);
}
};
//图片上传
const uploadImg = async (fileInfo: any) => {
let req = new FormData();
req.append('upload', fileInfo.file);
fileInfo.status = 'uploading';
fileInfo.message = '上传中...';
try {
let ret: any = await AppApi.uploadFile(req);
fileInfo.status = 'done';
fileInfo.message = '上传成功';
fileInfo.status = '';
fileInfo.message = '';
state.uploadImages.push(ret.coverImg);
state.totalPrice = showPrice(state.uploadImages.length, state.aiUndressPrice);
} catch (e) {
fileInfo.status = 'failed';
fileInfo.message = '上传失败';
}
};
//将base64转换为文件对象
const convertBase64ToBlob = (base64: any) => {
var base64Arr = base64.split(',');
var imgtype = '';
var base64String = '';
if (base64Arr.length > 1) {
//如果是图片base64,去掉头信息
base64String = base64Arr[1];
imgtype = base64Arr[0].substring(base64Arr[0].indexOf(':') + 1, base64Arr[0].indexOf(';'));
}
// 将base64解码
var bytes = atob(base64String);
//var bytes = base64;
var bytesCode = new ArrayBuffer(bytes.length);
// 转换为类型化数组
var byteArray = new Uint8Array(bytesCode);
// 将base64转换为ascii码
for (var i = 0; i < bytes.length; i++) {
byteArray[i] = bytes.charCodeAt(i);
}
// 生成Blob对象(文件对象)
return new Blob([bytesCode], { type: imgtype });
};
// 压缩图片
const compress = (file: any) => {
if (!file || !window.FileReader) return;
if (/^image/.test(file.type)) {
let reader = new FileReader();
reader.readAsDataURL(file);
return new Promise((resolve) => {
reader.onloadend = function () {
let result: any = reader.result;
let img: any = new Image();
img.src = result;
if (result.length <= 200 * 1024) {
let blob = convertBase64ToBlob(result);
let files = new File([blob], file.name, { type: blob.type });
resolve(files);
return;
}
img.onload = function () {
let canvas = document.createElement('canvas');
let ctx: any = canvas.getContext('2d');
let width = img.width;
let height = img.height;
canvas.width = width;
canvas.height = height;
// 铺底色
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, width, height);
//进行最小压缩
let ndata = canvas.toDataURL('image/jpeg', 0.3);
let blob = convertBase64ToBlob(ndata);
let files = new File([blob], file.name, { type: blob.type });
resolve(files);
};
};
});
}
};
const beforeDelete = async (file: any, detail: any) => {
const newArr = await delElbyIndex(state.uploadImages, detail.index);
state.totalPrice = 0; //showPrice(newArr.length, state.aiUndressPrice);
state.uploadImages = newArr;
return true;
};
// 根据索引删除数组
const delElbyIndex = (arr: any, index: number) => {
for (let i = index, len = arr.length - 1; i < len; i++) {
arr[i] = arr[i + 1];
arr.length = len;
}
return arr;
};
// 价格优惠
const showPrice = (quantity: number, price: number) => {
let totalPrice: any = 0;
// totalPrice = quantity * price;
totalPrice = (quantity - state.todayFreeCount) * price; //freeCount
return Math.ceil(totalPrice);
};
const getGenerateImg = async () => {
// if (!userStore.isVip) return showToast('请先充值vip');
if (!state.uploadImages.length) return showToast('至少上传一张图片');
let req = {
originPic: state.uploadImages[0],
mId: state.deatil.id,
coin: state.totalPrice,
shareStatus: state.shareStatus,
shareTitle: state.shareTitle,
isNewVersion: true,
};
try {
await MineApi.generateChangeImg(req);
showToast('上传成功,等待审核');
if (
useGlobalStore().presaleObj.status != 0 &&
globalStore.advanceStatus.privilegeLimit.hasLimit &&
useGlobalStore().advanceStatus.privilegeLimit.remain.todayAiUndressCount
) {
useGlobalStore().advanceStatus.privilegeLimit.remain.todayAiUndressCount -= 1;
} else {
if (state.todayFreeCount > state.fileList.length) {
state.todayFreeCount -= state.fileList.length;
state.freeCount -= state.fileList.length;
} else {
state.freeCount -= state.todayFreeCount;
state.todayFreeCount = 0;
}
}
state.totalPrice = 0;
state.fileList = [];
state.uploadImages = <Array<string>>[];
state.shareTitle = '';
shareBoxRef.value?.resetData();
} catch (e) {
console.log(e);
}
};
const getGenerateVideo = async () => {
// if (!userStore.isVip) return showToast('请先充值vip');
if (!state.uploadImages.length) return showToast('至少上传一张图片');
let req: any = {
picture: state.uploadImages,
vidModId: state.deatil.id,
shareStatus: state.shareStatus,
shareTitle: state.shareTitle,
isNewVersion: true,
};
if (state.couponDetail?.id) {
req.discount = [state.couponDetail?.id];
}
try {
await MineApi.generateChangeVideo(req);
showToast('上传成功,等待审核');
state.fileList = [];
state.uploadImages = <Array<string>>[];
userStore.walletInfo.amount -= state.totalPrice;
state.totalPrice = 0;
if (state.couponDetail?.id) {
state.couponDetail = {};
}
state.shareTitle = '';
shareBoxRef.value?.resetData();
} catch (e) {
console.log(e);
}
};
const onSubmit = () => {
debounce(state.type == 'video' ? getGenerateVideo : getGenerateImg, 500);
};
const debounce = (fn: any, wait: any) => {
if (state.timer) {
clearTimeout(state.timer);
}
state.timer = setTimeout(() => {
fn();
}, wait);
};
const handleUseCoupon = (item: any) => {
state.couponDetail = item;
// console.log(item);
// getGenerateVideo(item.id);
};
</script>
<style lang="scss" scoped>
.header {
height: 44px;
padding: 0 14px;
display: flex;
align-items: center;
.back {
width: 24px;
height: 24px;
}
.title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: white;
font-size: 18px;
max-width: 220px;
margin-left: 16px;
}
}
.previewDelete {
margin-top: 4px;
margin-right: 4px;
color: #000;
}
.content {
height: calc(100vh - 44px);
width: 100%;
padding: 0 16px 160px 16px;
box-sizing: border-box;
background: #0f0f0f;
.changImage {
width: 100%;
height: 197px;
display: flex;
align-items: center;
justify-content: center;
padding: 10px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 16px;
.modCover {
width: 131px;
height: 100%;
}
}
.cover {
width: 100%;
height: 193px;
margin-bottom: 20px;
img {
width: 100%;
height: 100%;
position: relative;
}
.play_box {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
> svg {
width: 50px;
height: 50px;
border-radius: 50%;
}
}
}
.mess {
h2 {
margin: 8px 0;
}
p {
font-size: 12px;
line-height: 20px;
color: rgba(152, 152, 152, 1);
}
}
.imgmess {
margin: 22px 0;
box-sizing: border-box;
display: flex;
justify-content: space-around;
align-items: center;
> div {
width: 100px;
font-size: 14px;
color: #999999;
text-align: center;
img {
width: 100px;
height: 100px;
margin-bottom: 12px;
}
}
}
.title {
margin-bottom: 14px;
span {
font-size: 12px;
color: rgba(152, 152, 152, 1);
margin-left: 8px;
}
}
}
.close {
height: 30px;
width: 30px;
position: fixed;
top: 20px;
left: 20px;
z-index: 10000;
}
.wrapper {
display: flex;
align-items: center;
justify-content: center;
padding-bottom: 30px;
height: 100%;
width: 100%;
background: rgb(0, 0, 0);
.videoBox {
height: 100%;
width: 100%;
}
}
.bottom {
width: 100%;
height: 80px;
display: flex;
align-items: center;
// justify-content: space-between;
justify-content: center;
padding: 0 20px 0 20px;
box-sizing: border-box;
position: fixed;
bottom: 0;
left: 0;
// background: #1b1c21;
.moneys {
display: flex;
align-items: center;
font-size: 14px;
p {
margin-left: 20px;
span {
padding-right: 5px;
box-sizing: border-box;
}
}
}
.btn {
width: 343px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
border-radius: 88px;
background: #ffc000;
color: #1f2029;
font-weight: 500;
}
}
:deep(.van-uploader) {
width: 111px;
height: 111px;
.van-uploader__wrapper {
width: 100%;
height: 100%;
border: dashed 1px #4d4d4d;
border-radius: 8px;
overflow: hidden;
background: #1d1b1c;
}
.van-uploader__input-wrapper,
.uploadBox {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
img {
width: 24px;
height: 24px;
margin-bottom: 12px;
}
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
}
}
:deep(.van-uploader__preview) {
width: 146px;
height: 146px;
border-radius: 4px;
overflow: hidden;
margin: unset;
.van-uploader__preview-image {
width: 100%;
height: 100%;
}
}
.tips-box {
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
margin-top: 20px;
.tips-item {
font-size: 14px;
color: rgba(152, 152, 152, 1);
margin-bottom: 10px;
.red-text {
color: #e47e04;
}
.green-text {
color: rgba(53, 222, 188, 1);
text-decoration: underline;
}
.cancel-btn {
color: rgba(255, 255, 255, 0.55);
font-size: 14px;
margin-left: 15px;
}
}
}
</style>
@@ -0,0 +1,326 @@
<template>
<div class="changeFaceImage">
<van-tabs
v-model:active="state.active"
title-active-color="#FFF"
title-inactive-color="rgba(255,255,255,0.7)"
animated
class="child_tabs openX"
@change="handleChangeTab"
@touchstart.stop
@touchmove.stop
@touchend.stop
v-if="state.categoryList.length > 0"
>
<van-tab :title="item.name" :name="item.id" v-for="(item, index) in state.categoryList" :key="index">
<div class="list-container" v-if="!state.loading && state.active == item.id">
<div class="filter-box">
<div
class="filter-item"
v-for="(item, index) in state.filterList"
:class="{ active: state.activeFilter == item.value }"
:key="index"
@click="handleFilter(item)"
>
<div class="item-name">{{ item.name }}</div>
<div class="filter-item-box">
<SvgIcon name="arrow-up2" v-if="state.activeFilter == item.value && state.sort" />
<SvgIcon name="arrow-up1" v-else />
<SvgIcon name="arrow-down2" v-if="state.activeFilter == item.value && !state.sort" />
<SvgIcon name="arrow-down1" v-else />
</div>
</div>
</div>
<div class="modArea overflow-y-auto" :id="`modAreaImage${state.active}`">
<div class="modBox" v-for="item in state.list" @click="toPage(item)" :key="item.id">
<div class="modVideoBox">
<lazyImg :src="item.cover" class="modCover" />
</div>
</div>
</div>
<Empty v-if="state.list.length == 0" />
</div>
</van-tab>
</van-tabs>
<Loading v-if="state.loading" />
</div>
</template>
<script lang="ts" setup>
import { reactive, nextTick, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import MineApi from '@/service/mine';
import TabPageScroll from '@/utils/resetTabPageScrollTop';
const router = useRouter();
const tabPageScroll = new TabPageScroll();
const state = reactive({
pageNumber: <number>1,
pageSize: <number>10,
list: <any>[], // 换脸模版
loading: true,
categoryList: <any>[],
filterList: [
{
name: '上架时间',
value: '1',
},
{
name: '使用次数',
value: '2',
},
{
name: '价格排序',
value: '3',
},
],
activeFilter: '1',
sort: false,
active: 0,
});
const toPage = (item: any) => {
localStorage.setItem('deatilData', JSON.stringify(item));
router.push({
path: '/changeDetail',
query: { type: 'image' },
});
};
/**
* 获取模板列表
*/
const getList = async (type?: string) => {
state.loading = true;
try {
let res: any = await MineApi.getAiMod2({
categoryId: state.active,
type: 0,
});
// 初始化时按上架时间降序排序
if (type == 'init') {
state.categoryList = res.categoryList || [];
const categoryId = res.categoryList?.[0].id || res.categoryId;
state.active = categoryId;
if (categoryId) {
getList();
filterList();
}
} else {
state.list = res.templateList || [];
state.loading = false;
nextTick(() => {
tabPageScroll.add(`#modAreaImage${state.active}`);
});
}
} catch (e) {
console.log(e);
}
};
const handleChangeTab = async () => {
// 切换分类时重置筛选条件
state.activeFilter = '1';
state.sort = false;
state.list = [];
// 重新获取对应分类的数据
await getList();
};
const handleFilter = (item: any) => {
if (item.value == state.activeFilter) {
state.sort = !state.sort;
} else {
state.sort = false;
}
state.activeFilter = item.value;
filterList();
};
const filterList = () => {
const sortOrder = state.sort ? 1 : -1;
switch (state.activeFilter) {
case '1': // 上架时间
state.list.sort(
(a: any, b: any) => (new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) * sortOrder,
);
break;
case '2': // 使用次数
state.list.sort((a: any, b: any) => (a.usedCount - b.usedCount) * sortOrder);
break;
case '3': // 价格排序
state.list.sort((a: any, b: any) => (a.coin - b.coin) * sortOrder);
break;
}
};
onMounted(async () => {
await getList('init');
});
</script>
<style lang="scss" scoped>
.changeFaceImage {
height: 100%;
overflow: hidden;
.list-container {
height: calc(100vh - 44px * 3 - 78px);
overflow: hidden;
display: flex;
flex-direction: column;
}
.modArea {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-row-gap: 5px;
grid-column-gap: 5px;
padding: 0 16px 100px;
.modBox {
.modVideoBox {
border-radius: 9px;
overflow: hidden;
transform: translateZ(0px);
}
.modCover {
height: 148px;
width: 111px;
position: relative;
.hot {
position: absolute;
left: 0;
top: 0;
background: red;
color: #fff;
border-radius: 0px 0 9px 0;
font-size: 12px;
padding: 2px 5px;
}
.bottom {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 41px;
background: linear-gradient(
180deg,
rgba(34, 34, 34, 0) 0%,
rgba(34, 34, 34, 0.63) 66.67%,
rgba(34, 34, 34, 0.95) 100%
);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 7px;
}
.count {
margin-bottom: 8.5px;
align-self: flex-end;
box-sizing: border-box;
font-size: 12px;
color: #fff;
display: flex;
align-items: center;
svg {
width: 14px;
height: 14px;
margin: 0 4px 0 0;
}
span {
margin-top: 3px;
}
}
}
p {
margin: 11px 0 0 0;
width: 158px;
height: 18px;
}
}
}
.child_tabs {
:deep(> .van-tabs__wrap) {
height: 44px;
padding: 0 2px;
.van-tabs__nav {
background: transparent;
display: flex;
align-items: center;
justify-content: flex-start;
padding: 0;
flex-wrap: nowrap;
.van-tab__text {
font-size: 12px;
}
.van-tab {
flex: unset !important;
width: auto;
padding: 0 14px;
white-space: nowrap;
border: 0 !important;
height: 20px;
&:not(:nth-last-child(2))::after {
content: '';
position: absolute;
width: 1px;
height: 8px;
right: 0px;
top: 50%;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.8);
}
&:nth-last-child(2) {
margin-right: 0;
}
&.van-tab--active {
background: transparent;
}
}
}
.van-tabs__line {
display: block;
width: 8px;
height: 2px;
background: #ffc000;
bottom: 10px;
}
}
}
.filter-box {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px 12px 16px;
box-sizing: border-box;
.filter-item {
display: flex;
align-items: center;
justify-content: center;
width: 83px;
height: 26px;
line-height: 26px;
border-radius: 3px;
font-size: 12px;
background: rgba(255, 255, 255, 0.1);
color: #fff;
&.active {
background: #ffc000;
}
.item-name {
margin-right: 4px;
}
.filter-item-box {
display: flex;
align-items: center;
flex-direction: column;
svg {
width: 5px;
height: 4px;
&:nth-child(1) {
margin-bottom: 2px;
}
}
}
}
}
}
</style>
@@ -0,0 +1,358 @@
<template>
<div class="changeFaceVideo">
<van-tabs
v-model:active="state.active"
title-active-color="#FFF"
title-inactive-color="#999"
class="child_tabs openX"
animated
@change="handleChangeTab"
@touchstart.stop
@touchmove.stop
@touchend.stop
v-if="state.categoryList.length > 0"
>
<van-tab :title="item.name" :name="item.id" v-for="(item, index) in state.categoryList" :key="index">
<div class="list-container" v-if="!state.loading && state.active == item.id">
<div class="filter-box">
<div
class="filter-item"
v-for="(item, index) in state.filterList"
:class="{ active: state.activeFilter == item.value }"
:key="index"
@click="handleFilter(item)"
>
<div class="item-name">{{ item.name }}</div>
<div class="filter-item-box">
<SvgIcon name="arrow-up2" v-if="state.activeFilter == item.value && state.sort" />
<SvgIcon name="arrow-up1" v-else />
<SvgIcon name="arrow-down2" v-if="state.activeFilter == item.value && !state.sort" />
<SvgIcon name="arrow-down1" v-else />
</div>
</div>
</div>
<div class="modArea overflow-y-auto" :id="`modAreaVideo${state.active}`">
<div class="modBox" v-for="item in state.list" @click="toPage(item)" :key="item.id">
<div class="modVideoBox">
<lazyImg :src="item.cover" class="modCover">
<div class="hot" v-if="item.hotMark">{{ item.hotMark }}</div>
<div class="play-icon">
<SvgIcon name="playWhite"></SvgIcon>
</div>
</lazyImg>
</div>
</div>
</div>
<Empty v-if="state.list.length == 0" />
</div>
</van-tab>
</van-tabs>
<Loading v-if="state.loading" />
</div>
</template>
<script lang="ts" setup>
import { reactive, nextTick, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import MineApi from '@/service/mine';
import TabPageScroll from '@/utils/resetTabPageScrollTop';
const router = useRouter();
const tabPageScroll = new TabPageScroll();
const state = reactive({
pageNumber: <number>1,
pageSize: <number>10,
list: <any>[], // 换脸模版
loading: true,
categoryList: <any>[],
filterList: [
{
name: '上架时间',
value: '1',
},
{
name: '使用次数',
value: '2',
},
{
name: '价格排序',
value: '3',
},
],
activeFilter: '1',
sort: false,
active: 0,
});
const toPage = (item: any) => {
localStorage.setItem('deatilData', JSON.stringify(item));
router.push({
path: '/changeDetail',
query: { type: 'video' },
});
};
/**
* 获取模板列表
*/
const getList = async (type?: string) => {
state.loading = true;
try {
let res: any = await MineApi.getAiMod2({
categoryId: state.active,
type: 1,
});
// 初始化时按上架时间降序排序
if (type == 'init') {
state.categoryList = res.categoryList || [];
const categoryId = res.categoryList?.[0].id || res.categoryId;
state.active = categoryId;
if (categoryId) {
getList();
filterList();
}
} else {
state.list = res.templateList || [];
state.loading = false;
nextTick(() => {
tabPageScroll.add(`#modAreaVideo${state.active}`);
});
}
} catch (e) {
console.log(e);
}
};
const handleChangeTab = async () => {
// 切换分类时重置筛选条件
state.activeFilter = '1';
state.sort = false;
state.list = [];
// 重新获取对应分类的数据
await getList();
};
const handleFilter = (item: any) => {
if (item.value == state.activeFilter) {
state.sort = !state.sort;
} else {
state.sort = false;
}
state.activeFilter = item.value;
filterList();
};
const filterList = () => {
const sortOrder = state.sort ? 1 : -1;
switch (state.activeFilter) {
case '1': // 上架时间
state.list.sort(
(a: any, b: any) => (new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) * sortOrder,
);
break;
case '2': // 使用次数
state.list.sort((a: any, b: any) => (a.usedCount - b.usedCount) * sortOrder);
break;
case '3': // 价格排序
state.list.sort((a: any, b: any) => (a.coin - b.coin) * sortOrder);
break;
}
};
onMounted(() => {
getList('init');
});
</script>
<style lang="scss" scoped>
.changeFaceVideo {
height: 100%;
overflow: hidden;
.list-container {
height: calc(100vh - 44px * 3 - 78px);
overflow: hidden;
display: flex;
flex-direction: column;
}
.modArea {
display: grid;
grid-template-columns: 1fr 1fr;
grid-row-gap: 9px;
grid-column-gap: 7px;
padding: 0 16px 100px;
.modBox {
.modVideoBox {
border-radius: 9px;
overflow: hidden;
transform: translateZ(0px);
}
.modCover {
height: 99px;
position: relative;
.hot {
position: absolute;
left: 0;
top: 0;
background: red;
border-radius: 0px 0 9px 0;
font-size: 12px;
padding: 2px 5px;
color: #fff;
}
.bottom {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 41px;
background: linear-gradient(
180deg,
rgba(34, 34, 34, 0) 0%,
rgba(34, 34, 34, 0.63) 66.67%,
rgba(34, 34, 34, 0.95) 100%
);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 7px;
}
.count {
margin-bottom: 8.5px;
align-self: flex-end;
box-sizing: border-box;
font-size: 12px;
color: #fff;
display: flex;
align-items: center;
svg {
width: 14px;
height: 14px;
margin: 0 4px 0 0;
}
span {
margin-top: 3px;
}
}
.price {
font-size: 12px;
font-weight: 500;
color: #fff;
line-height: 17px;
padding: 2px 8px;
border-radius: 16px;
background: rgba(34, 34, 34, 0.5);
display: flex;
align-items: center;
img {
width: 12px;
height: 12px;
margin-right: 4px;
}
}
.play-icon {
position: absolute;
width: 40px;
height: 40px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
svg {
width: 100%;
height: 100%;
}
}
}
}
}
.child_tabs {
:deep(> .van-tabs__wrap) {
height: 44px;
padding: 0 2px;
.van-tabs__nav {
background: transparent;
display: flex;
align-items: center;
justify-content: flex-start;
flex-wrap: nowrap;
padding: 0;
.van-tab__text {
font-size: 12px;
}
.van-tab {
flex: unset !important;
width: auto;
padding: 0 14px;
white-space: nowrap;
border: 0 !important;
height: 20px;
&:not(:nth-last-child(2))::after {
content: '';
position: absolute;
width: 1px;
height: 8px;
right: 0px;
top: 50%;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.8);
}
&:nth-last-child(2) {
margin-right: 0;
}
&.van-tab--active {
background: transparent;
}
}
}
.van-tabs__line {
display: block;
width: 8px;
height: 2px;
background: #ffc000;
bottom: 10px;
}
}
}
.filter-box {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px 12px 16px;
box-sizing: border-box;
.filter-item {
display: flex;
align-items: center;
justify-content: center;
width: 83px;
height: 26px;
line-height: 26px;
border-radius: 3px;
font-size: 12px;
background: rgba(255, 255, 255, 0.1);
color: #fff;
&.active {
background: #ffc000;
}
.item-name {
margin-right: 4px;
}
.filter-item-box {
display: flex;
align-items: center;
flex-direction: column;
svg {
width: 5px;
height: 4px;
&:nth-child(1) {
margin-bottom: 2px;
}
}
}
}
}
}
</style>
@@ -0,0 +1,131 @@
<template>
<!-- AI 图片换脸/AI 脱衣 -->
<div class="ai_to_image" v-if="state.aiInfo.type == 1 || state.aiInfo.type == 3">
<LazyImg :src="item.originalImage" class="cover_1" @click="handlePreview(item.originalImage)" />
<LazyImg :src="item.generateImage" class="cover_1" @click="handlePreview(item.generateImage)" />
</div>
<!-- AI 视频换脸 -->
<div class="ai_to_video" v-if="state.aiInfo.type == 2">
<LazyImg :src="item.originalVideoCover" class="cover_2" :borderRadius="8" @click="handleVideo">
<SvgIcon name="playIcon" class="play_icon" />
</LazyImg>
</div>
<!-- 图生视频 -->
<div class="image_to_video" v-if="state.aiInfo.type == 4">
<LazyImg :src="item.originalImage" class="cover_4" :borderRadius="8" @click="handlePreview(item.originalImage)" />
<LazyImg :src="item.generateImage" class="cover_4" :borderRadius="8" @click="handlePreview(item.generateImage)" />
</div>
<!-- AI 绘画 -->
<div class="text_to_image" v-if="state.aiInfo.type == 5">
<div class="content">{{ item.originContent }}</div>
<LazyImg :src="item.generateImage" class="cover_5" :borderRadius="8" @click="handlePreview(item.generateImage)" />
</div>
<Teleport to="body">
<van-overlay :show="state.showPlayVideo" z-index="3000">
<div class="videoBox">
<SvgIcon name="back_white" class="close" @click="state.showPlayVideo = false" color="#fff" />
<SmallVideo
v-if="state.showPlayVideo"
:sourceURL="state.aiInfo.generateVideo"
:h265Url="state.aiInfo.h265Url"
/>
</div>
</van-overlay>
</Teleport>
</template>
<script setup lang="ts">
import { reactive } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
// {"1":"Ai图片换脸","2":"Ai视频换脸","3":"Ai脱衣","4":"Ai图生视频","5":"Ai绘画"}
const props = withDefaults(
defineProps<{
item: Types.Media.AiMediaBoxItem;
/**
* 是否可以预览图片和视频
*/
canPreview?: boolean;
}>(),
{
canPreview: true,
},
);
const state = reactive({
aiInfo: props.item,
showPlayVideo: false,
});
const handlePreview = (url: string) => {
if (!props.canPreview) return;
router.push({ path: '/picturePreview', query: { imgs: [url], index: 0 } });
};
const handleVideo = () => {
if (!props.canPreview) return;
state.showPlayVideo = true;
};
</script>
<style lang="scss" scoped>
.ai_to_image {
display: flex;
align-items: center;
border-radius: 8px;
overflow: hidden;
.cover_1 {
width: 172px;
height: 233px;
}
}
.ai_to_video {
.cover_2 {
width: 100%;
height: 193px;
.play_icon {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 36px;
height: 36px;
}
}
}
.text_to_image {
.content {
font-size: 14px;
color: #03fceb;
line-height: 20px;
margin-bottom: 14px;
}
.cover_5 {
width: 167px;
height: 223px;
}
}
.image_to_video {
display: flex;
align-items: center;
justify-content: space-between;
.cover_4 {
width: 167px;
height: 223px;
}
}
.videoBox {
height: 100%;
width: 100%;
background: rgb(0, 0, 0);
position: relative;
.close {
height: 30px;
width: 30px;
position: absolute;
top: 16px;
left: 16px;
z-index: 200;
}
}
</style>
@@ -0,0 +1,155 @@
<template>
<div class="post-card">
<!-- 用户信息 -->
<div class="post-header">
<LazyImg :src="item.portrait" round class="user-avatar" />
<div class="user-info">
<div class="nickname">{{ item.name }}</div>
<div class="post-meta">
<span class="type">{{ typeDict.get(item.type) }}</span>
<span class="time">{{ dateFormat1(item.reviewAt) }}</span>
</div>
</div>
<div type="primary" size="small" class="action-btn" @click.stop="onActionType" v-if="isActionBtn">
<SvgIcon name="madAi" />
制作同款
</div>
</div>
<!-- 角标 + 文本 -->
<div v-if="item.title" class="post-description">
<!-- <span v-if="item.sortCode > 0" class="label">置顶</span> -->
<span class="text">{{ item.title }}</span>
</div>
<AiMediaBoxItem :item="item" />
<!-- 操作栏 -->
<!-- <section class="post_menu">
<div class="item">
<SvgIcon class="icon" name="post_eye" />
<span class="ellipsis">{{ watchCount(item.pageViewCount) }}</span>
</div>
<div class="item">
<SvgIcon class="community_info icon" name="post_commit" />
<span class="ellipsis">{{ watchCount(item.commentCount) }}</span>
</div>
<div class="item">
<SvgIcon class="icon" v-if="item?.hasLiked" name="post_inheart" />
<SvgIcon class="icon" v-else name="post_heart" />
<span class="ellipsis">{{ watchCount(item.likeCount) }}</span>
</div>
</section> -->
<div class="split_line" />
</div>
</template>
<script setup lang="ts">
import { defineProps, PropType, ref } from 'vue';
import { dateFormat1, watchCount } from '@/utils/index';
import AiMediaBoxItem from './AiMediaBoxItem.vue';
const emit = defineEmits(['actionType']);
const props = defineProps({
item: Object as PropType<any>,
isActionBtn: {
type: Boolean,
default: true,
},
});
const typeDict = new Map([
[1, '图片换脸'],
[2, '视频换脸'],
[3, 'AI脱衣'],
[4, '图生视频'],
[5, 'AI绘画'],
]);
const onActionType = () => {
emit('actionType', props.item);
};
</script>
<style lang="scss" scoped>
.post-header {
display: flex;
align-items: center;
margin-bottom: 12px;
.user-avatar {
width: 48px;
height: 48px;
}
.user-info {
flex: 1;
margin-left: 12px;
}
.nickname {
font-size: 12px;
margin-bottom: 8px;
}
.post-meta {
display: flex;
align-items: center;
font-size: 10px;
color: rgba(255, 255, 255, 0.55);
.type {
color: #ffc000;
}
.time {
margin-left: 10px;
}
}
.action-btn {
font-size: 10px;
height: 24px;
width: 68px;
background: #ffc000;
border-radius: 4px;
color: #1f2029;
display: flex;
align-items: center;
justify-content: center;
> svg {
width: 14px;
height: 14px;
margin-right: 4px;
}
}
}
.post-description {
margin-bottom: 12px;
font-size: 14px;
line-height: 20px;
color: #fff;
}
.post_menu {
margin-top: 12px;
display: flex;
align-items: center;
.item {
display: flex;
align-items: center;
margin-right: 22px;
.icon {
width: 20px;
height: 20px;
margin-right: 3px;
color: #efefef;
}
.ellipsis {
font-size: 12px;
color: #efefef;
}
}
}
.split_line {
margin-top: 18px;
margin-bottom: 18px;
}
</style>
@@ -0,0 +1,72 @@
<template>
<van-popup v-model:show="isShow" round position="center" teleport="body" @click-overlay="onClose">
<div class="content_warp">
<p class="title">温馨提示</p>
<div class="line"></div>
<div class="info">当前免费次数不足或金币余额不足</div>
<div class="btn" @click="$router.push('/wallet'), emit('close')">去充值</div>
</div>
</van-popup>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const emit = defineEmits(['close']);
const props = withDefaults(
defineProps<{
isOpen: boolean;
}>(),
{
isOpen: false,
},
);
const isShow = computed(() => {
return props.isOpen;
});
const onClose = () => {
emit('close');
};
</script>
<style scoped lang="scss">
.content_warp {
width: 311px;
padding: 30px 24px 20px;
height: 198px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
background: #171717;
.title {
font-size: 18px;
text-align: center;
color: #fff;
margin-bottom: 8px;
}
.line {
width: 100%;
height: 1px;
background: linear-gradient(270deg, rgba(85, 54, 52, 0) 0%, #553634 50%, rgba(85, 54, 52, 0) 100%);
position: relative;
margin-bottom: 14px;
}
.info {
color: #ffffff8c;
margin-bottom: 16px;
font-size: 15px;
}
.btn {
width: 100%;
height: 44px;
line-height: 44px;
text-align: center;
border-radius: 99px;
background: #e1351f;
color: #fff;
}
}
</style>
@@ -0,0 +1,107 @@
<template>
<div class="share_box">
<div class="s_title">
分享给社区的狼友一同品鉴
<!-- <span>请确认所创建的AI绘图可见范围</span> -->
</div>
<div class="s_checked" @click="onChecked">
<img class="imgIcon" :src="state.shareStatus == 1 ? activeIcon : inactiveIcon" />
分享到AI广场
</div>
<van-field
@blur="blur"
class="text_input"
v-model="state.shareTitle"
rows="2"
autosize
:border="false"
type="textarea"
maxlength="300"
show-word-limit
placeholder="为她写个标题,吸引狼友来围观。例如:这是我暗恋多年的女老师!"
/>
</div>
</template>
<script setup lang="ts">
import activeIcon from '@/assets/images/png/mine/icheck-active.png';
import inactiveIcon from '@/assets/images/png/mine/icheck.png';
import { nextTick, reactive, watch } from 'vue';
const emit = defineEmits(['checkedChange', 'textChange']);
const state = reactive({
shareStatus: 1,
shareTitle: '',
});
const onChecked = () => {
state.shareStatus = state.shareStatus == 1 ? 0 : 1;
emit('checkedChange', state.shareStatus);
};
watch(
() => state.shareTitle,
() => {
emit('textChange', state.shareTitle);
},
);
const blur = () => {
nextTick(() => {
window.scrollTo(0, 0);
});
};
const resetData = () => {
state.shareTitle = '';
};
defineExpose({
resetData,
});
</script>
<style scoped lang="scss">
.share_box {
background: #ffffff0d;
border-radius: 6px;
padding: 20px 16px;
margin-top: 16px;
.s_title {
font-size: 14px;
display: flex;
align-items: center;
margin-bottom: 16px;
> span {
font-size: 12px;
margin-left: 6px;
color: #ffffff8c;
}
}
.s_checked {
font-size: 14px;
color: #ffffff8c;
display: flex;
align-items: center;
margin-bottom: 16px;
.imgIcon {
width: 24px;
height: 24px;
margin-right: 4px;
}
}
.text_input {
background: transparent;
border: 1px solid #ffc000;
border-radius: 4px;
font-size: 12px;
:deep(.van-field__control) {
color: #fff;
}
::placeholder {
color: #ffffff8c;
}
}
}
</style>
+64
View File
@@ -0,0 +1,64 @@
//将base64转换为文件对象
const convertBase64ToBlob = (base64: any) => {
const base64Arr = base64.split(',');
let imgtype = '';
let base64String = '';
if (base64Arr.length > 1) {
//如果是图片base64,去掉头信息
base64String = base64Arr[1];
imgtype = base64Arr[0].substring(base64Arr[0].indexOf(':') + 1, base64Arr[0].indexOf(';'));
}
// 将base64解码
const bytes = atob(base64String);
//var bytes = base64;
const bytesCode = new ArrayBuffer(bytes.length);
// 转换为类型化数组
const byteArray = new Uint8Array(bytesCode);
// 将base64转换为ascii码
for (let i = 0; i < bytes.length; i++) {
byteArray[i] = bytes.charCodeAt(i);
}
// 生成Blob对象(文件对象)
return new Blob([bytesCode], { type: imgtype });
};
// 压缩图片
const compress = (file: any) => {
if (!file || !window.FileReader) return;
if (/^image/.test(file.type)) {
const reader = new FileReader();
reader.readAsDataURL(file);
return new Promise((resolve) => {
reader.onloadend = function () {
const result: any = reader.result;
const img: any = new Image();
img.src = result;
if (result.length <= 200 * 1024) {
const blob = convertBase64ToBlob(result);
const files = new File([blob], file.name, { type: blob.type });
resolve(files);
return;
}
img.onload = function () {
const canvas = document.createElement('canvas');
const ctx: any = canvas.getContext('2d');
const width = img.width;
const height = img.height;
canvas.width = width;
canvas.height = height;
// 铺底色
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, width, height);
//进行最小压缩
const ndata = canvas.toDataURL('image/jpeg', 0.3);
const blob = convertBase64ToBlob(ndata);
const files = new File([blob], file.name, { type: blob.type });
resolve(files);
};
};
});
}
};
export default compress;
@@ -0,0 +1,29 @@
let graffiti = {
name: '',
path: '',
maskPic: [],
maskPicBase64: '',
} as {
name: string;
path: string;
maskPic: any[];
maskPicBase64: string;
[key: string]: any;
};
const setGraffiti = (obj: any) => {
for (const [key, value] of Object.entries(obj)) {
graffiti[key] = value;
}
};
const resetGraffiti = () => {
graffiti = {
name: '',
path: '',
maskPic: [],
maskPicBase64: '',
};
};
export { graffiti, setGraffiti, resetGraffiti };
@@ -0,0 +1,388 @@
<template>
<div class="main">
<div ref="container" class="container">
<canvas ref="backgroundCanvas"></canvas>
<canvas
ref="drawCanvas"
class="drawCanvas"
@touchstart="startDrawing"
@touchmove="draw"
@touchend="stopDrawing"
></canvas>
</div>
<footer class="footer">
<div class="action flex-vcenter">
画笔大小
<van-slider class="progress" :class="`step-${drawSize}`" v-model="drawSize" min="5" max="25" step="5" />
<div style="flex: 1"></div>
<SvgIcon name="draw-undo" class="do" @click.stop="undo" />
<SvgIcon name="draw-redo" class="do redo" @click.stop="redo" />
</div>
<div class="btn" @click.stop="saveDrawing">确认涂抹区</div>
</footer>
</div>
</template>
<script lang="ts">
export default {
name: 'graffiti2',
};
</script>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount, watch, defineProps } from 'vue';
import { setGraffiti } from './index';
const emit = defineEmits(['historyIndex', 'saveImage']);
const drawSize = ref(15);
const props = defineProps<{
imagePath?: string;
imageName?: string;
drawImage?: string;
}>();
const backgroundCanvas = ref<HTMLCanvasElement | null>(null);
const drawCanvas = ref<HTMLCanvasElement | null>(null);
const container = ref<HTMLCanvasElement | null>(null);
const isDrawing = ref(false);
const lastX = ref(0);
const lastY = ref(0);
// 历史记录
const history = ref<string[]>([]);
const historyIndex = ref(-1);
watch(historyIndex, (num) => {
emit('historyIndex', num);
});
const resizeCanvases = async () => {
if (!props.imagePath) return;
const { width, height } = (await getImageSize()) as { width: number; height: number };
const { clientHeight, clientWidth } = container.value as HTMLCanvasElement;
const imgAspectRatio = width / height;
const canvasAspectRatio = clientWidth / clientHeight;
let drawWidth, drawHeight, drawX, drawY;
if (imgAspectRatio > canvasAspectRatio) {
drawWidth = clientWidth;
drawHeight = (height / width) * drawWidth;
} else {
drawHeight = clientHeight;
drawWidth = (width / height) * drawHeight;
}
const bCanvas = backgroundCanvas.value!;
const dCanvas = drawCanvas.value!;
bCanvas.width = drawWidth;
bCanvas.height = drawHeight;
dCanvas.width = drawWidth;
dCanvas.height = drawHeight;
drawInitialImage();
};
const getImageSize = async () => {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
resolve({ width: img.width, height: img.height });
};
img.src = props.imagePath as string;
});
};
const drawInitialImage = () => {
if (props.imagePath) {
const img = new Image();
img.onload = () => {
const ctx = backgroundCanvas.value!.getContext('2d');
ctx?.clearRect(0, 0, backgroundCanvas.value!.width, backgroundCanvas.value!.height);
const canvasWidth = backgroundCanvas.value!.width;
const canvasHeight = backgroundCanvas.value!.height;
const imgAspectRatio = img.width / img.height;
const canvasAspectRatio = canvasWidth / canvasHeight;
let drawWidth, drawHeight, drawX, drawY;
if (imgAspectRatio > canvasAspectRatio) {
drawWidth = canvasWidth;
drawHeight = (img.height / img.width) * drawWidth;
} else {
drawHeight = canvasHeight;
drawWidth = (img.width / img.height) * drawHeight;
}
drawX = (canvasWidth - drawWidth) / 2;
drawY = (canvasHeight - drawHeight) / 2;
ctx?.drawImage(img, drawX, drawY, drawWidth, drawHeight);
};
img.src = props.imagePath;
}
if (props.drawImage) {
saveCanvasState();
redrawCanvas(props.drawImage);
setTimeout(() => {
saveCanvasState();
}, 50);
}
};
const getCanvasPosition = (e: MouseEvent | TouchEvent) => {
const rect = drawCanvas.value!.getBoundingClientRect();
return {
x: ((e as MouseEvent).clientX || (e as TouchEvent).touches[0].clientX) - rect.left,
y: ((e as MouseEvent).clientY || (e as TouchEvent).touches[0].clientY) - rect.top,
};
};
const beginPoints = [] as { x: number; y: number }[]; // 起始点
let beginPoint = { x: 0, y: 0 };
const drawColor = 'rgba(253, 5, 99, 0.5)';
const startDrawing = (e: MouseEvent | TouchEvent) => {
isDrawing.value = true;
const pos = getCanvasPosition(e);
lastX.value = pos.x;
lastY.value = pos.y;
beginPoints.push({ x: pos.x, y: pos.y });
beginPoint = { x: pos.x, y: pos.y };
saveCanvasState();
};
const draw = (e: MouseEvent | TouchEvent) => {
if (!isDrawing.value) return;
e.preventDefault();
const pos = getCanvasPosition(e);
beginPoints.push({ x: pos.x, y: pos.y });
if (beginPoints.length < 3) return;
const drawCtx = drawCanvas.value!.getContext('2d')!;
const lastTwoPoints = beginPoints.slice(-2);
const controlPoint = lastTwoPoints[0];
const endPoint = {
x: (lastTwoPoints[0].x + lastTwoPoints[1].x) / 2,
y: (lastTwoPoints[0].y + lastTwoPoints[1].y) / 2,
};
drawCtx.beginPath();
drawCtx.strokeStyle = drawColor;
drawCtx.fillStyle = drawColor;
drawCtx.lineWidth = drawSize.value;
drawCtx.moveTo(beginPoint.x, beginPoint.y);
drawCtx.quadraticCurveTo(controlPoint.x, controlPoint.y, endPoint.x, endPoint.y);
drawCtx.stroke();
drawCtx.closePath();
beginPoint = endPoint;
// drawCtx.strokeStyle = drawColor;
// drawCtx.fillStyle = drawColor;
// drawCtx.lineWidth = drawSize.value;
// drawCtx.lineJoin = 'round';
// drawCtx.lineCap = 'round';
// drawCtx.beginPath();
// drawCtx.moveTo(lastX.value, lastY.value);
// drawCtx.lineTo(pos.x, pos.y);
// drawCtx.stroke();
// drawCtx.closePath();
lastX.value = pos.x;
lastY.value = pos.y;
};
const stopDrawing = () => {
isDrawing.value = false;
saveCanvasState();
setGraffiti({
maskPic: [],
});
};
const saveCanvasState = () => {
const canvasData = drawCanvas.value!.toDataURL();
if (historyIndex.value < history.value.length - 1) {
history.value.splice(historyIndex.value + 1);
}
history.value.push(canvasData);
historyIndex.value++;
};
const undo = () => {
if (historyIndex.value > 0) {
historyIndex.value--;
redrawCanvas(history.value[historyIndex.value]);
}
};
const redo = () => {
if (historyIndex.value < history.value.length - 1) {
historyIndex.value++;
redrawCanvas(history.value[historyIndex.value]);
}
};
const redrawCanvas = (dataURL: string) => {
const drawCtx = drawCanvas.value!.getContext('2d')!;
const img = new Image();
img.onload = () => {
drawCtx.clearRect(0, 0, drawCanvas.value!.width, drawCanvas.value!.height);
drawCtx.drawImage(img, 0, 0);
};
img.src = dataURL;
};
const saveDrawing = () => {
const originalWidth = drawCanvas.value!.width;
const originalHeight = drawCanvas.value!.height;
const standard = 768;
let offscreenWidth, offscreenHeight;
// 根据逻辑调整尺寸
if (originalWidth > originalHeight) {
const r = originalWidth / standard;
offscreenWidth = standard;
offscreenHeight = Math.round(originalHeight / r);
} else {
const r = originalHeight / standard;
offscreenHeight = standard;
offscreenWidth = Math.round(originalWidth / r);
}
const offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = offscreenWidth;
offscreenCanvas.height = offscreenHeight;
const offscreenCtx = offscreenCanvas.getContext('2d')!;
offscreenCtx.fillStyle = 'black';
offscreenCtx.fillRect(0, 0, offscreenCanvas.width, offscreenCanvas.height);
offscreenCtx.drawImage(drawCanvas.value!, 0, 0, originalWidth, originalHeight, 0, 0, offscreenWidth, offscreenHeight);
// 替换为白色
const imageData = offscreenCtx.getImageData(0, 0, offscreenWidth, offscreenHeight);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
if (data[i] !== 0 || data[i + 1] !== 0 || data[i + 2] !== 0 || data[i + 3] !== 255) {
data[i] = 255;
data[i + 1] = 255;
data[i + 2] = 255;
data[i + 3] = 255;
}
}
offscreenCtx.putImageData(imageData, 0, 0);
offscreenCanvas.toBlob((blob: any) => {
const name = props.imageName + '-drawing.png';
const file = new File([blob], name, { type: 'image/png' });
emit('saveImage', { file, base64: drawCanvas.value!.toDataURL() });
}, 'image/png');
};
const clearDrawing = () => {
const drawCtx = drawCanvas.value!.getContext('2d')!;
drawCtx.clearRect(0, 0, drawCanvas.value!.width, drawCanvas.value!.height);
history.value = [];
historyIndex.value = -1;
};
onMounted(() => {
resizeCanvases();
window.addEventListener('resize', resizeCanvases);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', resizeCanvases);
});
watch(
() => props.imagePath,
(newPath) => {
if (newPath) {
drawInitialImage();
}
},
);
</script>
<style lang="scss" scoped>
@import '@/assets/styles/mixins.scss';
.main {
width: 100%;
height: 100%;
z-index: 10;
display: flex;
flex-direction: column;
.container {
flex: 1;
width: calc(100% - 20px);
margin: 20px 10px;
}
.footer {
width: 100%;
height: 156px;
@include SafeArea('padding', 'bottom', 26px);
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
.action {
width: 100%;
padding: 0 10px;
color: #fff;
text-align: center;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
--van-slider-active-background: #4d4d4d;
--van-slider-inactive-background: #4d4d4d;
--van-slider-bar-height: 10px;
--van-slider-button-width: 16px;
--van-slider-button-height: 16px;
--van-slider-button-shadow: none;
.progress {
width: 177px;
margin: 0 12px;
background: url('@/assets/images/png/ai-graffiti-progress.png') 0 0 / contain no-repeat,
url('@/assets/images/png/ai-graffiti-round.png') 25% 50% / 3px 3px no-repeat,
url('@/assets/images/png/ai-graffiti-round.png') 50% 50% / 3px 3px no-repeat,
url('@/assets/images/png/ai-graffiti-round.png') 75% 50% / 3px 3px no-repeat;
:deep(.van-slider__bar) {
background: transparent;
}
}
.do {
width: 24px;
height: 24px;
margin: 0 12px;
}
}
.btn {
background: $themeBgLgColor;
width: 290px;
height: 47px;
line-height: 47px;
text-align: center;
flex-shrink: 0;
border-radius: 4px;
}
}
canvas {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
&.drawCanvas {
z-index: 2;
}
}
}
</style>
+223
View File
@@ -0,0 +1,223 @@
<template>
<div class="aiUndress">
<div class="navBar">
<svg-icon @click="$router.go(-1)" class="navArrow" name="back_white" />
<div class="navTitle">AI科技</div>
<!-- <div class="rightBtn" @click="$router.push('/aiRecord')">记录</div> -->
</div>
<div class="list overflow-y-auto" id="aiListId">
<template v-for="(item, index) in state.list" :key="index">
<div class="item" @click="$router.push(item.router)" v-if="item.isShow">
<div class="bg" :class="[`bg-${item.id}`]"></div>
<div class="desc">{{ item.desc }}</div>
<div class="title">
<div class="icon" :class="[`icon-${item.id}`]"></div>
{{ item.name }}
<span class="label">({{ item.label }})</span>
<!-- <span class="beta" v-if="item.isBeta">beta</span> -->
</div>
<div class="tag" v-if="item.isTag"></div>
</div>
</template>
<div style="height: 44px"></div>
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'aiUndress',
};
</script>
<script lang="ts" setup>
import { reactive } from 'vue';
import TabPageScroll from '@/utils/resetTabPageScrollTop';
import { useConfigStore } from '@/stores/config';
const configStore = useConfigStore();
const tabPageScroll = new TabPageScroll();
setTimeout(() => {
tabPageScroll.add('#aiListId');
}, 100);
const state = reactive({
list: [
{
name: 'AI视频',
label: '图生视频',
isBeta: false,
router: '/aiUndress2?active=0',
isTag: true,
isShow: true,
desc: '图生视频 趣味无穷',
id: 2,
},
{
name: 'AI绘画',
label: '图片',
isBeta: false,
router: '/aiUndress2?active=1',
isTag: false,
isShow: true,
desc: '一键绘画 无限可能',
id: 6,
},
// {
// name: 'AI女友',
// label: '聊天',
// isBeta: false,
// router: '/AiGirlFriend',
// isTag: false,
// isShow: configStore.appConfig.aiGirlFriend,
// desc: '暖心陪伴 您的专属女友',
// id: 1,
// },
{
name: 'AI换脸',
label: '图片',
isBeta: true,
router: '/aiUndress2?active=3',
isTag: false,
isShow: configStore.appConfig.aiImageChangeFace,
desc: '轻松换脸 趣味无穷',
id: 3,
},
{
name: 'AI换脸',
label: '视频',
isBeta: true,
router: '/aiUndress2?active=2',
isTag: false,
isShow: configStore.appConfig.aiVideoChangeFace,
desc: 'AI换脸 玩出新花样',
id: 4,
},
{
name: 'AI脱衣',
label: '图片',
isBeta: true,
router: '/aiUndress2?active=4',
isTag: false,
isShow: configStore.appConfig.aiUndress,
desc: '在你面前一丝不挂',
id: 5,
},
],
});
</script>
<style lang="scss" scoped>
.aiUndress {
height: 100%;
background: rgba(3, 15, 24, 1);
.navBar {
height: 44px;
padding: 0 12px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(0, 0, 0, 1);
// background: #fff;
// border-bottom: 1px solid #e6e6e6;
.navArrow {
width: 24px;
height: 24px;
margin-right: 26px;
}
.navTitle {
flex: 2;
text-align: center;
font-size: 16px;
font-weight: 500;
color: #fff;
margin-right: 56px;
}
// .rightBtn {
// color: #666666;
// }
}
.list {
height: calc(100% - 44px);
$items: (1, 2, 3, 4, 5, 6);
@each $value in $items {
.bg-#{$value} {
background: url('@/assets/images/png/aiUndress-bg-#{$value}.gif') 0 0 / cover no-repeat;
}
.icon-#{$value} {
background: url('@/assets/images/png/aiUndress-icon-#{$value}.png') 0 0 / contain no-repeat;
}
}
padding: 0 18px;
.item {
width: 100%;
margin-top: 12px;
border-radius: 12px;
overflow: hidden;
.bg {
height: 180px;
}
.desc {
position: absolute;
bottom: 60px;
left: 0;
width: 100%;
color: #fff;
font-size: 14px;
font-weight: 400;
line-height: 17px;
padding: 0 16px;
}
.title {
height: 53px;
border-radius: 0px 0px 8px 8px;
background: rgba(255, 255, 255, 0.05);
// background: rgba(0, 0, 0, 0.04);
display: flex;
align-items: center;
padding: 0 16px;
color: rgba(255, 255, 255, 1);
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 26.25px; /* 154.412% */
.icon {
width: 24px;
height: 24px;
margin-right: 7.5px;
}
.label {
color: rgba(225, 53, 31, 1);
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 26.25px;
}
.beta {
color: rgba(225, 53, 31, 1);
width: 37px;
height: 17px;
font-size: 12px;
line-height: 17px; /* 125% */
border-radius: 16px;
border: 0.5px solid rgba(225, 53, 31, 1);
text-align: center;
margin-left: 5px;
}
}
.tag {
position: absolute;
top: 0;
right: 0;
z-index: 2;
width: 88px;
height: 88px;
background: url('@/assets/images/png/aiUndress-tag.png') 0 0 / contain no-repeat;
}
}
}
}
</style>
+236
View File
@@ -0,0 +1,236 @@
<template>
<div class="aiUndress holidays_bg">
<Header
title="AI科技"
leftIcon="xxx"
class="ai_hader"
rightTitle="记录"
@rightFuc="$router.push(`/aiRecord?activeTab=${state.active - 1}`)"
headerBg="transparent"
@change="changeTab"
/>
<template v-if="!state.loading">
<van-tabs
v-model:active="state.active"
title-active-color="#1F2029"
title-inactive-color="#FFF"
class="aiUndress_tabs openX"
animated
@touchmove.stop
>
<van-tab v-for="(feature, index) in enabledAiFeatures" :key="feature.type" :name="feature.type">
<template #title>
<!-- 图生视频 (type: 4) -->
<template v-if="feature.type === 4">
<SvgIcon name="picture2Video" :color="state.active === feature.type ? '#1F2029' : '#fff'" />
图生视频
</template>
<!-- 文生图 (type: 5) -->
<template v-else-if="feature.type === 5">
<SvgIcon name="aitextCover" :color="state.active === feature.type ? '#1F2029' : '#fff'" />
AI绘画
</template>
<!-- AI脱衣 (type: 1) -->
<template v-else-if="feature.type === 1">
<SvgIcon name="aiclothing" :color="state.active === feature.type ? '#1F2029' : '#fff'" />
AI脱衣
</template>
<!-- AI视频换脸 (type: 2) -->
<template v-else-if="feature.type === 2">
<SvgIcon name="aicamera" :color="state.active === feature.type ? '#1F2029' : '#fff'" />
视频换脸
</template>
<!-- AI图片换脸 (type: 3) -->
<template v-else-if="feature.type === 3">
<SvgIcon name="aigallery" :color="state.active === feature.type ? '#1F2029' : '#fff'" />
图片换脸
</template>
<!-- AI小说 (type: 6) -->
<template v-else-if="feature.type === 6">
<SvgIcon name="aiText" :color="state.active === feature.type ? '#1F2029' : '#fff'" />
AI小说
</template>
</template>
<div class="content overflow-y-auto">
<!-- 图生视频内容 -->
<pictureVideoList v-if="feature.type === 4" :list="state.aiImgToVideoMod" />
<!-- 文生图内容 -->
<TextCover v-else-if="feature.type === 5" :list="state.aiTextToImgMod" />
<!-- AI脱衣内容 -->
<undress v-else-if="feature.type === 1" :list="state.undresslList" />
<!-- AI视频换脸内容 -->
<changeFaceVideo v-else-if="feature.type === 2" :type="3" />
<!-- AI图片换脸内容 -->
<changeFaceImage v-else-if="feature.type === 3" :type="4" />
<!-- AI小说内容 -->
<AiText v-else-if="feature.type === 6" />
</div>
</van-tab>
</van-tabs>
</template>
<Loading v-else />
</div>
</template>
<script lang="ts">
export default {
name: 'aiUndress2',
};
</script>
<script lang="ts" setup>
import { computed, onActivated, onMounted, reactive, ref } from 'vue';
import Undress from './undress.vue';
import pictureVideoList from './pictureVideoList.vue';
import changeFaceVideo from './changeFaceVideo.vue';
import changeFaceImage from './changeFaceImage.vue';
import TextCover from './textCover.vue';
import AiPiazza from './aiPiazza.vue';
import AiText from './aiText/index.vue';
import MineApi from '@/service/mine';
import { useRoute, useRouter } from 'vue-router';
import { useConfigStore } from '@/stores/config';
const route = useRoute();
const configStore = useConfigStore();
const router = useRouter();
const getDefaultActive = () => {
const features = (configStore.appConfig.aiSwitchConf || [])
.filter((item) => item.isOpen && item.type !== 7)
.sort((a, b) => a.sort - b.sort);
return features[0]?.type ?? 1;
};
const state = reactive({
active: route.query.active != null && route.query.active !== '' ? Number(route.query.active) : getDefaultActive(),
undresslList: [],
aiImgToVideoMod: [],
aiTextToImgMod: [],
loading: true, // loading
});
const aiSwitchConf = computed(() => {
if (configStore.appConfig.aiSwitchConf) {
return configStore.appConfig.aiSwitchConf;
} else {
return [];
}
});
// 根据配置获取启用的AI功能列表,按sort排序
const enabledAiFeatures = computed(() => {
return aiSwitchConf.value.filter((item) => item.isOpen && item.type !== 7).sort((a, b) => a.sort - b.sort);
});
const changeTab = (index: number) => {
router.replace({
path: '/aiUndress2',
query: {
active: index,
},
});
};
const getList = async () => {
try {
let res: any = await MineApi.getAiMod();
state.undresslList = res.aiUndressMod || [];
state.aiImgToVideoMod = res.aiImgToVideoMod || [];
state.aiTextToImgMod = res.aiTextToImgMod || [];
state.loading = false;
} catch (e) {
console.log(e);
}
};
// getList();
const contentHeight = ref('');
onMounted(() => {
if (enabledAiFeatures.value) {
contentHeight.value = Math.ceil(enabledAiFeatures.value.length / 3) * 40 - 2 + 'px';
} else {
contentHeight.value = '12px';
}
});
onActivated(() => {
getList();
});
</script>
<style lang="scss" scoped>
.aiUndress {
width: 100%;
height: 100%;
background: #0f0f0f;
.ai_hader {
:deep(.right-btn) {
color: #969696;
}
}
.content {
height: calc(100dvh - v-bind('contentHeight') - 99px);
}
}
.aiUndress_tabs {
:deep(> .van-tabs__wrap) {
z-index: 10;
height: auto;
padding: 6px 16px;
.van-tabs__nav {
padding: 0;
background: transparent;
display: flex;
gap: 14px;
flex-wrap: wrap;
}
.van-tab {
width: 105px;
height: auto;
min-height: 26px;
padding: 4px;
font-size: 12px;
line-height: 18px;
flex: none;
font-weight: 400;
border-radius: 3px;
border: 1px solid rgba(255, 255, 255, 0.35);
background: rgba(255, 255, 255, 0.1);
color: #fff;
box-sizing: border-box;
svg {
height: 16px;
width: 16px;
margin-right: 4px;
color: #fff;
flex-shrink: 0;
}
.van-tab__text {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
}
.van-tab--active {
background: #ffc000;
border-color: transparent;
color: #1f2029;
svg {
color: #1f2029 !important;
}
}
.van-tabs__line {
display: none;
}
}
}
</style>
+163
View File
@@ -0,0 +1,163 @@
<template>
<div class="topic-wrap page">
<Header title="手动涂鸦" />
<div class="page-content">
<graffiti2
:imagePath="graffitiVisiblePath"
:imageName="graffitiName"
:drawImage="drawImage"
@saveImage="saveMaskPic"
@historyIndex="hanldeDraw"
></graffiti2>
</div>
</div>
<van-overlay :show="visibleTips" z-index="2001" @click.stop="goBack(false)">
<div class="wrapper flex-center-center">
<div class="content">
<div class="title">温馨提示</div>
<div class="horizontalLine"></div>
<div class="text">是否放弃当前手动涂抹?</div>
<div class="btn-box flex-center-between">
<div class="btn plan" @click="goBack(false)">取消</div>
<div class="btn" @click="goBack(true)">确定放弃</div>
</div>
</div>
</div>
</van-overlay>
</template>
<script lang="ts">
export default {
name: 'aiUndress2Draw',
};
</script>
<script lang="ts" setup>
import { ref, nextTick } from 'vue';
import { useRoute, useRouter, onBeforeRouteLeave, NavigationGuardNext } from 'vue-router';
import compress from './compress';
import AppApi from '@/service/app';
import graffiti2 from './graffiti2/index.vue';
import useLoading from '@/hooks/loading';
import { showToast } from 'vant';
import { graffiti, setGraffiti } from './graffiti2/index';
const { openLoading, closeLoading } = useLoading();
const route = useRoute();
const router = useRouter();
const isDrawing = ref(false);
const visibleTips = ref(false);
const hanldeDraw = (num: number) => {
isDrawing.value = num > -1;
};
let answer: NavigationGuardNext | null = null;
const goBack = (flag: boolean) => {
visibleTips.value = false;
if (answer === null) {
return;
}
if (flag) {
answer();
} else {
answer(false);
}
};
onBeforeRouteLeave((to, from, next) => {
answer = next;
if (isDrawing.value) {
if ((graffiti.maskPic || []).length > 0) {
next();
return;
}
visibleTips.value = true;
return;
}
next();
});
const { name = '', path = '', maskPicBase64 } = graffiti;
if (name === '' && path === '') {
router.back();
}
const graffitiVisiblePath = ref(path);
const graffitiName = ref(name);
const drawImage = ref(maskPicBase64);
const saveMaskPic = async ({ file, base64 }: { file: File; base64: string }) => {
if (!graffiti.path) {
showToast('请先返回上一页上传图片');
return;
}
openLoading();
let newFile = await compress(file);
let req = new FormData();
req.append('upload', newFile as File);
let ret: any = await AppApi.uploadFile(req);
setGraffiti({ maskPic: [ret.coverImg], maskPicBase64: base64 });
closeLoading();
router.back();
};
</script>
<style lang="scss" scoped>
.topic-wrap {
margin: 0;
height: 100vh;
background-color: #0e141e;
background-image: radial-gradient(circle at top left, rgba(62, 100, 234, 0.6), transparent 70%);
background-size: 300px 300px;
background-repeat: no-repeat;
height: 100%;
.page-content {
height: calc(100vh - 45px);
}
}
.wrapper {
width: 100%;
height: 100%;
.content {
width: 311px;
height: 220px;
font-size: 16px;
color: #fff;
background: #161e2c;
position: relative;
border-radius: 12px;
.title {
color: #fff;
padding: 32px 0 12px;
font-size: 20px;
font-weight: 500;
text-align: center;
}
.horizontalLine {
height: 1px;
background-color: rgba(255, 255, 255, 0.05);
}
.text {
text-align: center;
margin: 31px 0;
}
p {
text-align: center;
}
}
.btn-box {
padding: 0 23px;
.btn {
background: $themeColor;
height: 42px;
border-radius: 42px;
text-align: center;
line-height: 42px;
font-weight: 500;
font-size: 16px;
width: 134px;
&.plan {
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.8);
width: 108px;
}
}
}
}
</style>
@@ -0,0 +1,376 @@
<template>
<div class="picture2Video">
<Header :title="state.currentModel.title" />
<div class="picture_box overflow-y-auto">
<div class="undress_top">
<van-uploader
@click.stop
v-model="state.fileList"
:after-read="afterRead"
:before-read="beforeRead"
:before-delete="beforeDelete"
:preview-full-image="true"
>
<template #default>
<div class="uploadBox flex-center-center">
<img src="@/assets/images/png/addFile.png" alt="" />
上传图片
</div>
</template>
<template #preview-delete>
<SvgIcon name="previewDelete" class="previewDelete"></SvgIcon>
</template>
</van-uploader>
<div class="precautionsBox">
<h2>注意事项:</h2>
<p>1、素材仅供AI使用,绝无外泄风险,请放心使用.</p>
<p>2、素材需清晰,小于2MB,上传间隔大于60秒.</p>
<p>3、本功能不支持多人图片</p>
<p>4、生成失败退回金币,若违规作废.</p>
<p>5、禁止使用未成年图片!</p>
</div>
</div>
<div class="tip-text">案例鉴赏</div>
<!-- <van-swipe class="advSwiper" :autoplay="5000" indicator-color="#8859f9">-->
<!-- <van-swipe-item v-for="(item, index) in state.list" :key="index">-->
<div class="example flex">
<lazyImg class="img_Decypt" :src="state.currentModel.cover" :key="state.currentModel.cover" />
<lazyImg class="img_Decypt" :src="state.currentModel.newUrl" :key="state.currentModel.newUrl" />
</div>
<!-- </van-swipe-item>-->
<!-- </van-swipe>-->
<!-- <div class="tip-text">选择模型</div>
<div class="modelList">
<div
v-for="item in state.list"
:key="item.id"
@click="changeModel(item)"
:class="{ active: item.id === state.currentModel.id }"
>
{{ item.title }}
</div>
</div> -->
<!-- <ShareBox @checkedChange="checkedChange" @textChange="textChange" ref="shareBoxRef" />-->
<div class="tip-price">
处理一张照片的费用是
<span>{{ state.aiImageToVideoPrice }}金币</span>
</div>
<div class="submit-box">
<div class="submit-btn" @click="onSubmit">立即提交</div>
</div>
</div>
</div>
<!-- <ErrorPop :isOpen="state.errorOpen" @close="state.errorOpen = false" /> -->
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { showToast } from 'vant';
import { useUserStore } from '@/stores/user';
import { useConfigStore } from '@/stores/config';
import AppApi from '@/service/app';
import MineApi from '@/service/mine';
import UserApi from '@/service/user';
import { useRouter } from 'vue-router';
import compress from './compress';
// import ShareBox from './components/share_box.vue';
// import ErrorPop from './components/error_pop.vue';
const router = useRouter();
const configStore = useConfigStore();
const userStore = useUserStore();
// const props = withDefaults(
// defineProps<{
// list: any;
// }>(),
// {
// list: [],
// },
// );
const state = reactive({
uploadImages: <Array<string>>[],
fileList: <any>[],
timer: <any>'',
userInfo: userStore.userInfo,
userWallet: <any>{},
aiImageToVideoPrice: 0, // ai脱衣的售价
fileSize: 2 * 1024 * 1024,
// list: props.list,
loading: true,
shareStatus: 1, // 分享状态
shareTitle: '', // 分享标题
// errorOpen: false, //金币/次数不足
currentModel: <any>{},
});
const init = () => {
const pictureVideoDetailData = JSON.parse(localStorage.getItem('pictureVideoDetailData') || '{}');
state.currentModel = pictureVideoDetailData;
state.aiImageToVideoPrice = configStore.appConfig.aiImageToVideoPrice;
UserApi.queryWallet().then((res) => {
state.userWallet = res;
});
};
init();
// const shareBoxRef = ref();
const checkedChange = (status: number) => {
state.shareStatus = status;
};
const textChange = (text: string) => {
state.shareTitle = text;
};
const beforeRead = (file: any) => {
if (file.size > state.fileSize) {
showToast('文件不可超过2M');
return false;
}
return true;
};
// 图片上传
const afterRead = async (file: any) => {
if (file.length) {
file.map(async (item: any) => {
let newFile = await compress(item.file);
item.file = newFile;
await uploadImg(item);
});
} else {
let newFile = await compress(file.file);
file.file = newFile;
uploadImg(file);
}
};
//图片上传
const uploadImg = async (fileInfo: any) => {
let req = new FormData();
req.append('upload', fileInfo.file);
fileInfo.status = 'uploading';
fileInfo.message = '上传中...';
try {
let ret: any = await AppApi.uploadFile(req);
fileInfo.status = 'done';
fileInfo.message = '上传成功';
fileInfo.status = '';
fileInfo.message = '';
state.uploadImages.push(ret.coverImg);
} catch (e) {
fileInfo.status = 'failed';
fileInfo.message = '上传失败';
}
};
const beforeDelete = async (file: any, detail: any) => {
const newArr = await delElbyIndex(state.uploadImages, detail.index);
state.uploadImages = newArr;
return true;
};
// 根据索引删除数组
const delElbyIndex = (arr: any, index: number) => {
for (let i = index, len = arr.length - 1; i <= len; i++) {
arr[i] = arr[i + 1];
arr.length = len;
}
return arr;
};
const getGenerateImg = async () => {
if (!state.uploadImages.length) return showToast('请上传图片');
let req = {
mid: state.currentModel.id,
originPic: state.uploadImages[0],
shareStatus: state.shareStatus,
shareTitle: state.shareTitle,
};
try {
await MineApi.generateImageToVideo(req);
showToast('上传成功,等待审核');
state.fileList = [];
state.uploadImages = <Array<string>>[];
state.shareTitle = '';
// shareBoxRef.value?.resetData();
} catch (error: any) {
if (error.toString().indexOf('8000')) {
// state.errorOpen = true;
router.push('/wallet');
}
}
};
const onSubmit = () => {
debounce(getGenerateImg, 500);
};
const debounce = (fn: any, wait: any) => {
if (state.timer) {
clearTimeout(state.timer);
}
state.timer = setTimeout(() => {
fn();
}, wait);
};
const changeModel = (item: any) => {
state.currentModel = item;
};
</script>
<style lang="scss" scoped>
:deep(.van-uploader__preview) {
width: 115px;
height: 115px;
border-radius: 9px;
overflow: hidden;
margin: unset;
.van-uploader__preview-image {
width: unset;
height: unset;
}
}
.picture2Video {
height: 100%;
.picture_box {
box-sizing: border-box;
padding: 0 10px;
height: calc(100vh - 78px - 48px * 2);
padding-bottom: 120px;
.undress_top {
width: 100%;
display: flex;
align-items: center;
padding: 12px 0 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
:deep(.van-uploader) {
width: 115px;
height: 115px;
.van-uploader__wrapper {
width: 100%;
height: 100%;
border: dashed 1px #989898;
border-radius: 9px;
overflow: hidden;
}
.van-uploader__input-wrapper,
.uploadBox {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
img {
width: 24px;
height: 24px;
margin-bottom: 10px;
}
font-size: 12px;
color: #656565;
}
}
.previewDelete {
margin-top: 4px;
margin-right: 4px;
color: #000;
}
.precautionsBox {
flex: 1;
h2 {
font-size: 14px;
color: #fff;
margin-bottom: 7px;
}
margin-left: 12px;
font-size: 10px;
line-height: 18px;
color: #bdbdbd;
}
}
.tip-price {
color: #989898;
text-align: center;
font-size: 14px;
font-weight: 400;
padding: 18px 0;
border-top: 1px solid rgba(0, 0, 0, 0.1);
span {
color: #ffc000;
}
}
.tip-text {
color: #fff;
font-size: 18px;
font-weight: 600;
margin: 18px 0 0;
}
}
.submit-box {
//position: absolute;
//bottom: 0;
//@include SafeArea('padding', 'bottom', 55px);
width: 100%;
.submit-btn {
width: 343px;
height: 44px;
border-radius: 88px;
background: #ffc000;
color: #1f2029;
font-size: 16px;
font-weight: 500;
text-align: center;
line-height: 44px;
margin: 0 auto;
}
}
}
.modelList {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding: 0px 0 18px 0;
div {
width: 79.75px;
height: 28px;
line-height: 28px;
color: #757575;
text-align: center;
font-size: 14px;
font-style: normal;
font-weight: 400;
border-radius: 4px;
background: rgba(255, 255, 255, 0.05);
overflow: hidden;
&.active {
color: #1f2029;
background: #ffc000;
}
}
}
.example {
width: 100%;
height: 269px;
border-radius: 6px;
overflow: hidden;
margin-top: 12px;
.img_Decypt {
flex: 1;
}
}
:deep(.van-swipe__indicators) {
position: relative;
display: flex;
width: 100%;
bottom: 0;
justify-content: center;
padding-right: 14px;
margin-top: 14px;
}
:deep(.van-swipe__indicator) {
width: 6px;
height: 6px;
border: none;
background: #e1351f;
}
:deep(.van-swipe__indicator--active) {
width: 6px;
height: 6px;
border-radius: 99px;
}
</style>
@@ -0,0 +1,78 @@
<template>
<div class="pictureVideoList">
<div class="modArea overflow-y-auto" id="modAreaVideoList">
<div class="modBox" v-for="item in list" @click="toPage(item)" :key="item.id">
<div class="modVideoBox">
<lazyImg :src="item.newUrl || item.cover" class="modCover">
<div class="modCover_name">{{ item.title }}</div>
</lazyImg>
</div>
</div>
</div>
<Empty v-if="list.length == 0" />
</div>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router';
// import TabPageScroll from '@/utils/resetTabPageScrollTop';
const router = useRouter();
// const tabPageScroll = new TabPageScroll();
const props = defineProps({
list: {
type: Array,
default: () => [],
},
});
console.log(props.list);
const toPage = (item: any) => {
localStorage.setItem('pictureVideoDetailData', JSON.stringify(item));
router.push({
path: '/pictureVideoDetail',
query: { id: item.id, title: item.title },
});
};
</script>
<style lang="scss" scoped>
.pictureVideoList {
height: calc(100vh - 78px - 44px * 2);
.modArea {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-row-gap: 9px;
grid-column-gap: 7px;
padding: 12px 16px 50px;
box-sizing: border-box;
.modBox {
width: 100%;
.modVideoBox {
border-radius: 9px;
overflow: hidden;
transform: translateZ(0px);
}
.modCover {
height: 224px;
position: relative;
.modCover_name {
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 7px;
font-size: 12px;
color: #fff;
text-align: center;
box-sizing: border-box;
min-width: 80px;
max-width: 160px;
padding: 4px 16px;
background: rgba(255, 61, 106, 1);
border-radius: 999px;
}
}
}
}
}
</style>
+349
View File
@@ -0,0 +1,349 @@
<template>
<div class="textCover">
<div class="textCover_box overflow-y-auto" id="coverVideoId">
<div class="title_box">
形象描述 (必填)
<!-- <span>*</span> -->
</div>
<van-field
@blur="blur"
class="text_input"
v-model="state.text"
rows="3"
autosize
:border="false"
type="textarea"
maxlength="100"
show-word-limit
placeholder="示例:“女,大学生,身高165cm,体重50kg,穿着JK制服.”"
/>
<div class="title_box">选择长宽比例</div>
<!-- 宽高比 -->
<div class="type_line_box">
<div
class="line_item"
:class="{ line_item_active: state.aspectRatio == item.type }"
v-for="(item, index) in state.typelinelist"
:key="index"
@click="state.aspectRatio = item.type"
>
{{ item.type }}
</div>
</div>
<!-- 风格 -->
<van-collapse v-model="activeNames" :border="false" accordion>
<van-collapse-item name="cover" :border="false" class="type_cover_box">
<template #title>
<div class="cover_box_title">
<div class="title_left">
<SvgIcon name="ai_cover_video_type" />
风格
</div>
<div class="title_right">
<template v-if="props.list.length && !activeNames">
<lazyImg :src="getUpCover" class="right_avatar" :borderRadius="6" />
</template>
<SvgIcon name="ai_cover_video_up" v-if="activeNames == 'cover'" />
<SvgIcon name="ai_cover_video_down" v-else />
</div>
</div>
</template>
<template #right-icon></template>
<div class="cover_list">
<div
class="cover_item"
v-for="(item, index) in props.list"
:key="index"
@click="state.styleTypeId = item.id"
>
<lazyImg :src="item.cover" class="item_avatar" :borderRadius="6">
<SvgIcon name="ai_cover_video_item_active" color="#ffc000" v-if="state.styleTypeId == item.id" />
<SvgIcon name="ai_cover_video_item" v-else />
</lazyImg>
<div class="item_title">{{ item.title }}</div>
</div>
</div>
</van-collapse-item>
</van-collapse>
<div class="tip-price">
生成一次AI绘画费用是
<span>{{ state.aiTextToImagePrice }}金币</span>
</div>
<!-- <ShareBox @checkedChange="checkedChange" @textChange="textChange" ref="shareBoxRef" />-->
<div class="submit-btn" @click="onSubmit">立即提交</div>
<div class="sizeBox" />
</div>
<!-- <div class="submit-box">
<div class="submit-btn" @click="onSubmit">立即提交</div>
</div> -->
</div>
</template>
<script setup lang="ts">
import { reactive, nextTick, ref, watch, computed } from 'vue';
import { useConfigStore } from '@/stores/config';
import { showToast } from 'vant';
// import { useUserStore } from '@/stores/user';
import MineApi from '@/service/mine';
import ShareBox from './components/share_box.vue';
import TabPageScroll from '@/utils/resetTabPageScrollTop';
const tabPageScroll = new TabPageScroll();
setTimeout(() => {
tabPageScroll.add(`#coverVideoId`);
}, 100);
// const userStore = useUserStore();
const configStore = useConfigStore();
const props = withDefaults(
defineProps<{
list: any;
}>(),
{
list: [],
},
);
const state = reactive({
text: '',
aspectRatio: '1:1',
typelinelist: [{ type: '1:1' }, { type: '4:3' }, { type: '3:4' }, { type: '16:9' }, { type: '9:16' }],
styleTypeId: '', // 风格
aiTextToImagePrice: configStore.appConfig.aiTextToImagePrice, // ai绘图的售价
timer: <any>'',
shareStatus: 1, // 分享状态
shareTitle: '', // 分享标题
});
watch(
() => props.list,
(newVal) => {
if (newVal.length) {
state.styleTypeId = newVal[0].id;
}
},
{ immediate: true },
);
const activeNames = ref('cover');
const blur = () => {
nextTick(() => {
window.scrollTo(0, 0);
});
};
const shareBoxRef = ref();
const checkedChange = (status: number) => {
state.shareStatus = status;
};
const textChange = (text: string) => {
state.shareTitle = text;
};
const setAiTextToImageGenerate = async () => {
// if (!userStore.isVip) return showToast('请先充值vip');
if (!state.text) {
showToast('请输入形象描述');
return;
}
if (!state.styleTypeId) {
showToast('请选择绘图风格');
return;
}
let styleType;
if (props.list.length) {
styleType = props.list.find((item: any) => item.id == state.styleTypeId)?.styleType;
}
const params = {
text: state.text,
aspectRatio: state.aspectRatio,
styleType,
shareStatus: state.shareStatus,
shareTitle: state.shareTitle,
};
try {
await MineApi.setAiTextToImageGenerate(params);
showToast('上传成功,等待审核');
state.text = '';
state.aspectRatio = '1:1';
state.styleTypeId = props.list.length ? props.list[0].id : '';
state.shareTitle = '';
shareBoxRef.value?.resetData();
} catch (error) {
console.error(error);
}
};
const onSubmit = () => {
debounce(setAiTextToImageGenerate, 500);
};
const getUpCover = computed(() => {
return props.list.find((item: any) => item.id == state.styleTypeId)?.cover;
});
const debounce = (fn: any, wait: any) => {
if (state.timer) {
clearTimeout(state.timer);
}
state.timer = setTimeout(() => {
fn();
}, wait);
};
</script>
<style scoped lang="scss">
.textCover {
height: 100%;
.textCover_box {
padding: 0px 16px;
height: calc(100vh - 78px - 48px * 2);
.title_box {
font-size: 16px;
margin: 14px 0;
span {
color: #ff004f;
}
}
.text_input {
background: #ffffff0d;
border-radius: 4px;
font-size: 12px;
:deep(.van-field__control) {
color: #fff;
}
::placeholder {
color: #999999;
}
}
.type_line_box {
display: flex;
align-items: center;
margin-bottom: 28px;
.line_item {
height: 28px;
line-height: 28px;
padding: 0 10px;
border-radius: 4px;
font-size: 14px;
color: #7c7c82;
background: #ffffff0d;
margin-right: 12px;
&.line_item_active {
background: #ffc000;
color: #1f2029;
}
}
}
:deep(.van-collapse) {
border-radius: 4px;
overflow: hidden;
// margin: 0 8px;
.van-cell {
background: #ffffff0d;
padding: 12px;
}
.van-collapse-item__content {
background: #ffffff0d;
padding: 0 12px 12px;
}
}
.type_cover_box {
.cover_box_title {
display: flex;
align-items: center;
justify-content: space-between;
color: #fff;
.title_left {
display: flex;
align-items: center;
> svg {
width: 16px;
height: 16px;
margin-right: 4px;
}
}
.title_right {
display: flex;
align-items: center;
.right_avatar {
width: 32px;
height: 32px;
margin-right: 12px;
}
> svg {
width: 10px;
height: 10px;
}
}
}
.cover_list {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-row-gap: 4px;
grid-column-gap: 4px;
.cover_item {
display: flex;
flex-direction: column;
align-items: center;
.item_avatar {
width: 102px;
height: 102px;
margin-bottom: 6px;
svg {
position: absolute;
top: 4px;
right: 4px;
width: 22px;
height: 22px;
color: #fff;
}
}
.item_title {
color: #e7e8e9;
}
}
}
}
.tip-price {
color: #989898;
text-align: center;
font-size: 10.514px;
font-weight: 400;
margin-top: 24px;
span {
color: #ffc000;
}
}
.sizeBox {
height: 120px;
}
}
// .submit-box {
// position: absolute;
// bottom: 0;
// @include SafeArea('padding', 'bottom', 30px);
// background: #0e141e;
// width: 100%;
// .submit-btn {
// width: 343px;
// height: 44px;
// border-radius: 4px;
// background: #ffc000;
// text-align: center;
// line-height: 44px;
// margin: 0 auto;
// }
// }
.submit-btn {
width: 343px;
height: 44px;
border-radius: 88px;
background: #ffc000;
text-align: center;
line-height: 44px;
color: #1f2029;
font-size: 16px;
font-weight: 500;
margin: 20px auto 0;
}
}
</style>
+486
View File
@@ -0,0 +1,486 @@
<template>
<div class="undress">
<div class="undress_box overflow-y-auto" id="undressId">
<div class="undress_top">
<van-uploader
@click.stop
v-model="state.fileList"
:after-read="afterRead"
:before-read="beforeRead"
:before-delete="beforeDelete"
>
<template #default>
<div class="uploadBox flex-center-center">
<img src="@/assets/images/png/addFile.png" alt="" />
上传图片
</div>
</template>
<template #preview-delete>
<SvgIcon name="previewDelete" class="previewDelete"></SvgIcon>
</template>
</van-uploader>
<div class="precautionsBox">
<h2>注意事项:</h2>
<p>1、素材仅供AI使用,绝无外泄风险,请放心使用.</p>
<p>2、素材需清晰,小于2MB,上传间隔大于60秒.</p>
<p>3、本功能不支持多人图片</p>
<p>4、生成失败退回金币,若违规作废.</p>
<p>5、禁止使用未成年图片!</p>
</div>
</div>
<div class="aiImg">
<p>案列鉴赏</p>
<AdvSwiper class="advSwiper" :list="props.list" clickType="none" />
</div>
<div class="bottom">
<!-- v-if="useGlobalStore().advanceStatus.privilegeLimit.remain.todayAiUndressCount || state.freeCount" -->
<!-- <template> -->
<p>
你当前免费体验为{{
useGlobalStore().advanceStatus.privilegeLimit.remain.todayAiUndressCount + state.freeCount
}}次数
<!-- 今日可用次数:
<span>
{{ useGlobalStore().advanceStatus.privilegeLimit.remain.todayAiUndressCount + state.todayFreeCount }}
</span> -->
</p>
<p>
当日免费{{
(useGlobalStore().advanceStatus.privilegeLimit.remain.todayAiUndressCount || 0) + state.todayFreeCount
}}次数
</p>
<p>
处理一张照片的费用是
<span>{{ state.aiUndressPrice || 0 }}金币</span>
</p>
<!-- <p><span class="green-text">使用抵扣券</span></p> -->
<!-- </template> -->
<!-- <template v-else>
<p>你当前没有免费体验次数</p>
<p>
处理一张照片的费用是
<span>{{ state.aiUndressPrice || 0 }}金币</span>
</p>
</template> -->
<div class="padding_box">
<!-- <ShareBox @checkedChange="checkedChange" @textChange="textChange" ref="shareBoxRef" />-->
<div class="submit flex-center-center" @click="onSubmit">立即提交</div>
</div>
<div class="sizeBox" />
</div>
</div>
<!-- <div class="submit-box">
<div class="submit flex-center-center" @click="onSubmit">立即提交</div>
</div> -->
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { showToast } from 'vant';
import { useUserStore } from '@/stores/user';
import { useConfigStore } from '@/stores/config';
import { useGlobalStore } from '@/stores/global';
import AppApi from '@/service/app';
import MineApi from '@/service/mine';
import UserApi from '@/service/user';
import ShareBox from './components/share_box.vue';
import TabPageScroll from '@/utils/resetTabPageScrollTop';
const tabPageScroll = new TabPageScroll();
const globalStore = useGlobalStore();
const configStore = useConfigStore();
const userStore = useUserStore();
const props = withDefaults(
defineProps<{
list: any;
}>(),
{
list: [],
},
);
setTimeout(() => {
tabPageScroll.add(`#undressId`);
}, 100);
const state = reactive({
uploadImages: <Array<string>>[],
fileList: [],
timer: <any>'',
userInfo: userStore.userInfo,
userWallet: <any>{},
aiUndressPrice: 0, // ai脱衣的售价
totalPrice: 0, // ai脱衣总售价
fileSize: 2 * 1024 * 1024,
freeCount: 0,
list: props.list,
loading: true,
todayFreeCount: 0,
shareStatus: 1, // 分享状态
shareTitle: '', // 分享标题
});
const init = () => {
state.aiUndressPrice = configStore.appConfig.aiUndressPrice;
UserApi.queryWallet().then((res) => {
state.userWallet = res;
state.freeCount = state.userWallet.aiUndressFreeTimes || 0;
state.todayFreeCount = state.userWallet.todayAiFreeTimes || 0;
});
};
init();
const shareBoxRef = ref();
const checkedChange = (status: number) => {
state.shareStatus = status;
};
const textChange = (text: string) => {
state.shareTitle = text;
};
const beforeRead = (file: any) => {
if (file.size > state.fileSize) {
showToast('文件不可超过2M');
return false;
}
return true;
};
// 图片上传
const afterRead = async (file: any) => {
if (file.length) {
file.map(async (item: any) => {
let newFile = await compress(item.file);
item.file = newFile;
await uploadImg(item);
});
} else {
let newFile = await compress(file.file);
file.file = newFile;
uploadImg(file);
}
};
//图片上传
const uploadImg = async (fileInfo: any) => {
let req = new FormData();
req.append('upload', fileInfo.file);
fileInfo.status = 'uploading';
fileInfo.message = '上传中...';
try {
let ret: any = await AppApi.uploadFile(req);
fileInfo.status = 'done';
fileInfo.message = '上传成功';
fileInfo.status = '';
fileInfo.message = '';
state.uploadImages.push(ret.coverImg);
// state.totalPrice = showPrice(state.uploadImages.length, state.aiUndressPrice);
if (state.uploadImages.length >= state.todayFreeCount) {
//freeCount
// 当选中的图片大于等于免费次数的时候才计算总价格
state.totalPrice = showPrice(state.uploadImages.length, state.aiUndressPrice);
}
} catch (e) {
fileInfo.status = 'failed';
fileInfo.message = '上传失败';
}
};
//将base64转换为文件对象
const convertBase64ToBlob = (base64: any) => {
var base64Arr = base64.split(',');
var imgtype = '';
var base64String = '';
if (base64Arr.length > 1) {
//如果是图片base64,去掉头信息
base64String = base64Arr[1];
imgtype = base64Arr[0].substring(base64Arr[0].indexOf(':') + 1, base64Arr[0].indexOf(';'));
}
// 将base64解码
var bytes = atob(base64String);
//var bytes = base64;
var bytesCode = new ArrayBuffer(bytes.length);
// 转换为类型化数组
var byteArray = new Uint8Array(bytesCode);
// 将base64转换为ascii码
for (var i = 0; i < bytes.length; i++) {
byteArray[i] = bytes.charCodeAt(i);
}
// 生成Blob对象(文件对象)
return new Blob([bytesCode], { type: imgtype });
};
// 压缩图片
const compress = (file: any) => {
if (!file || !window.FileReader) return;
if (/^image/.test(file.type)) {
let reader = new FileReader();
reader.readAsDataURL(file);
return new Promise((resolve) => {
reader.onloadend = function () {
let result: any = reader.result;
let img: any = new Image();
img.src = result;
if (result.length <= 200 * 1024) {
let blob = convertBase64ToBlob(result);
let files = new File([blob], file.name, { type: blob.type });
resolve(files);
return;
}
img.onload = function () {
let canvas = document.createElement('canvas');
let ctx: any = canvas.getContext('2d');
let width = img.width;
let height = img.height;
canvas.width = width;
canvas.height = height;
// 铺底色
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, width, height);
//进行最小压缩
let ndata = canvas.toDataURL('image/jpeg', 0.3);
let blob = convertBase64ToBlob(ndata);
let files = new File([blob], file.name, { type: blob.type });
resolve(files);
};
};
});
}
};
const beforeDelete = async (file: any, detail: any) => {
const newArr = await delElbyIndex(state.uploadImages, detail.index);
if (state.uploadImages.length >= state.todayFreeCount) {
//freeCount
// 当选中的图片大于等于免费次数的时候才计算总价格
state.totalPrice = showPrice(newArr.length, state.aiUndressPrice);
}
state.uploadImages = newArr;
return true;
};
// 根据索引删除数组
const delElbyIndex = (arr: any, index: number) => {
for (let i = index, len = arr.length - 1; i <= len; i++) {
arr[i] = arr[i + 1];
arr.length = len;
}
return arr;
};
// 价格优惠
const showPrice = (quantity: number, price: number) => {
let totalPrice: any = 0;
// totalPrice = quantity * price;
totalPrice = (quantity - state.todayFreeCount) * price; //freeCount
return Math.ceil(totalPrice);
};
const getGenerateImg = async () => {
// if (!userStore.isVip) return showToast('请先充值vip');
if (!state.uploadImages.length) return showToast('请上传图片');
// if (state.freeCount <= 0 && (state.totalPrice > userStore.walletInfo.amount)) return showToast('免费次数与账户余额不足');
let req = {
originPic: state.uploadImages,
coin: Number(state.totalPrice),
shareStatus: state.shareStatus,
shareTitle: state.shareTitle,
};
try {
await MineApi.generateImg(req);
showToast('上传成功,等待审核');
if (
useGlobalStore().presaleObj.status != 0 &&
globalStore.advanceStatus.privilegeLimit.hasLimit &&
useGlobalStore().advanceStatus.privilegeLimit.remain.todayAiUndressCount
) {
useGlobalStore().advanceStatus.privilegeLimit.remain.todayAiUndressCount -= 1;
} else {
if (state.todayFreeCount > state.fileList.length) {
state.todayFreeCount -= state.fileList.length;
state.freeCount -= state.fileList.length;
} else {
state.freeCount -= state.todayFreeCount;
state.todayFreeCount = 0;
}
}
state.totalPrice = 0;
state.fileList = [];
state.uploadImages = <Array<string>>[];
state.shareTitle = '';
shareBoxRef.value?.resetData();
} catch (e) {
console.log(e);
}
};
const onSubmit = () => {
debounce(getGenerateImg, 500);
};
const debounce = (fn: any, wait: any) => {
if (state.timer) {
clearTimeout(state.timer);
}
state.timer = setTimeout(() => {
fn();
}, wait);
};
</script>
<style lang="scss" scoped>
:deep(.van-uploader__preview) {
width: 111px;
height: 111px;
border-radius: 4px;
overflow: hidden;
margin: unset;
.van-uploader__preview-image {
width: unset;
height: unset;
}
}
.previewDelete {
margin-top: 4px;
margin-right: 4px;
color: #000;
}
.undress {
background: #0f0f0f;
.undress_box {
height: calc(100vh - 78px - 48px * 2);
.undress_top {
width: 100%;
display: flex;
padding: 12px 16px;
:deep(.van-uploader) {
width: 111px;
height: 111px;
.van-uploader__wrapper {
width: 100%;
height: 100%;
border: dashed 1px #656565;
border-radius: 8px;
overflow: hidden;
background: #1d1b1c;
}
.van-uploader__input-wrapper,
.uploadBox {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
img {
width: 26px;
height: 26px;
margin-bottom: 10px;
}
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
}
}
.precautionsBox {
h2 {
font-size: 14px;
color: #fff;
margin-bottom: 2px;
}
margin-left: 10px;
font-size: 10px;
line-height: 18px;
color: rgb(153, 153, 153);
}
}
.aiImg {
// width: 100%;
// height: 260px;
margin: 0 16px;
padding: 16px 0;
border-top: 1px solid rgba(255, 255, 255, 0.1);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
p {
font-size: 18px;
color: #fff;
font-weight: 600;
margin-bottom: 16px;
}
.advSwiper {
height: 280px;
transform: translateZ(0px);
position: relative;
padding-bottom: 30px;
:deep(.van-swipe-item) {
height: 260px !important;
}
:deep(.van-swipe) {
padding-bottom: 20px;
}
}
}
.padding_box {
padding: 0 16px;
}
.bottom {
text-align: center;
margin: 20px 0;
p {
margin-bottom: 10px;
font-size: 12px;
color: #989898;
span {
color: #ffc000;
}
.green-text {
color: rgba(53, 222, 188, 1);
text-decoration: underline;
}
}
}
.sizeBox {
height: 120px;
}
}
}
// .submit-box {
// position: absolute;
// bottom: 0;
// @include SafeArea('padding', 'bottom', 30px);
// background: #000000;
// width: 100%;
// .submit {
// margin: 0 auto;
// width: 343px;
// height: 44px;
// background: #ffc000;
// border-radius: 4px;
// color: #fff;
// font-size: 16px;
// }
// }
.submit {
margin: 20px auto 0;
width: 343px;
height: 44px;
background: #ffc000;
border-radius: 88px;
color: #1f2029;
font-size: 16px;
font-weight: 500;
}
:deep(.van-swipe__indicators) {
bottom: 0px;
left: 50%;
transform: translateX(-50%);
width: auto;
.van-swipe__indicator {
width: 6px;
height: 6px;
border-radius: 50%;
background: #ffc000;
opacity: 0.3;
}
.van-swipe__indicator--active {
width: 6px;
height: 6px;
border-radius: 50%;
background: #ffc000;
opacity: 1;
}
}
</style>
+123
View File
@@ -0,0 +1,123 @@
<template>
<div class="bindingCode">
<Header title="绑定邀请码" />
<div class="content overflow-y-auto">
<div class="tips">
<p>输入邀请码</p>
<p>邀请码只能绑定一次 且不能修改</p>
</div>
<div class="inputBox">
<span>邀请码</span>
<van-field v-model="state.value" :border="false" placeholder="请输入邀请码(字母大写)" />
</div>
<div class="share">邀请1人 获得1天会员</div>
<div class="btn"><span @click="bindingCode">立即绑定</span></div>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import { showToast } from 'vant';
import UserApi from '@/service/user';
import { useUserStore } from '@/stores/user';
import { useRouter } from 'vue-router';
const userStore = useUserStore();
const router = useRouter();
const state = reactive({
userInfo: userStore.userInfo,
value: <string>'',
});
/**
* 绑定邀请码
*/
const bindingCode = () => {
if (!state.value) {
showToast('请输入邀请码');
return;
}
let req = {
promotionCode: state.value,
};
let res: any = UserApi.inviteBind(req);
res.then(() => {
showToast('绑定邀请码成功!');
state.userInfo.inviterCode = state.value;
userStore.saveUserInfo(state.userInfo);
router.back();
});
};
</script>
<style lang="scss" scoped>
.bindingCode {
height: 100%;
.content {
height: calc(100vh - 44px);
.tips {
padding: 20px;
p {
margin-bottom: 15px;
&:nth-child(1) {
font-weight: 500;
font-size: 30px;
}
&:nth-child(2) {
font-size: 16px;
color: #525252;
}
}
}
.inputBox {
margin: 30px 20px 15px 20px;
display: flex;
align-items: center;
border-bottom: solid 1px rgba(0, 0, 0, 0.05);
span {
font-weight: 500;
}
:deep(.van-cell) {
border-radius: 5px;
font-size: 14px;
color: #333;
background: rgba(3, 15, 24, 1);
height: 42px;
display: flex;
align-items: center;
flex: 1;
overflow: hidden;
}
:deep(.van-field__control) {
color: #fff;
}
::placeholder {
color: rgba(82, 82, 82, 1);
}
}
.btn {
display: flex;
justify-content: center;
span {
width: 284px;
height: 44px;
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
color: #ffffff;
border-radius: 3px;
background: #f68804;
}
}
}
.share {
font-size: 12px;
color: rgba(82, 82, 82, 1);
margin-bottom: 30px;
padding: 0 20px;
}
}
</style>
+190
View File
@@ -0,0 +1,190 @@
<template>
<div class="bindingPhone">
<Header title="绑定手机" />
<div class="content overflow-y-auto">
<div class="form">
<div class="itembox">
<div class="value">
<van-field
label="手机号"
v-model="state.phoneValue"
center
clearable
maxlength="11"
type="tel"
placeholder="请输入手机号码"
/>
</div>
</div>
<div class="itembox">
<div class="value">
<van-field
label="验证码"
v-model="state.codeValue"
center
clearable
maxlength="6"
type="digit"
placeholder="输入验证码"
/>
<div class="timer" v-if="state.timer">{{ state.timer }}S</div>
<div class="getBtn" @click="countdown" v-else>获取验证码</div>
</div>
</div>
</div>
<div class="subBtnBox" @click="bindAccount">
<div class="subBtn">立即绑定</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import { showToast } from 'vant';
import UserApi from '@/service/user';
import { useUserStore } from '@/stores/user';
import { useRouter } from 'vue-router';
const userStore = useUserStore();
const router = useRouter();
const state = reactive({
phoneValue: <string>'',
codeValue: <string>'',
timer: <number>0,
userInfo: userStore.userInfo,
});
const countdown = () => {
if (!state.phoneValue) {
showToast('手机号不能为空');
return;
}
sendCode();
};
const sendCode = () => {
let res: any = UserApi.getCaptcha({
type: 2,
mobile: '+86' + state.phoneValue,
});
res.then(() => {
state.timer = 60;
let countdownTime = setInterval(() => {
state.timer--;
if (state.timer <= 0) {
clearInterval(countdownTime);
}
}, 1000);
});
};
const bindAccount = () => {
if (!state.phoneValue) return showToast('请输入手机号');
if (!state.codeValue) return showToast('请输入验证码');
let res: any = UserApi.bindAccount({
mobile: '+86' + state.phoneValue,
code: state.codeValue,
});
res.then(() => {
state.userInfo.mobile = '+86' + state.phoneValue;
userStore.saveUserInfo(state.userInfo);
showToast('绑定成功');
router.go(-1);
});
};
</script>
<style lang="scss" scoped>
.bindingPhone {
height: 100%;
width: 100%;
background: #000;
.content {
height: calc(100vh - 44px);
padding-top: 40px;
.form {
padding: 0 42px;
}
.itembox {
width: 100%;
color: #fff;
margin-bottom: 0;
.value {
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
margin-bottom: 24px;
}
}
:deep(.van-field__label) {
width: 60px;
color: #fff;
font-size: 16px;
margin-right: 12px;
}
:deep(.van-field) {
background-color: transparent;
padding: 0;
flex: 1;
}
:deep(.van-cell::after) {
display: none;
}
:deep(.van-field__control) {
color: #fff;
font-size: 12px;
}
::placeholder {
color: rgba(255, 255, 255, 0.35);
font-size: 12px;
}
.getBtn {
flex-shrink: 0;
font-size: 16px;
color: rgba(255, 255, 255, 0.5);
white-space: nowrap;
margin-left: 8px;
}
.timer {
flex-shrink: 0;
font-size: 16px;
color: #ffc000;
margin-left: 8px;
}
.subBtnBox {
display: flex;
justify-content: center;
margin-top: 80px;
.subBtn {
width: 264px;
height: 44px;
border-radius: 22px;
background: #ffc000;
color: #1f2029;
font-size: 16px;
font-weight: 500;
display: flex;
justify-content: center;
align-items: center;
}
}
}
}
</style>
+148
View File
@@ -0,0 +1,148 @@
<template>
<div class="officialCommunityPage">
<Header title="商务合作" />
<div class="content overflow-y-auto">
<div class="listBox">
<div class="th" v-for="(item, index) in state.cooperationList" :key="index" @click="toLink(item.url)">
<div class="left">
<img :src="item.icon" />
<!-- <pic :imgUrl="item.icon" :ratio="100" :key="item.icon"></pic>-->
<span>{{ item.description }}</span>
</div>
<div class="right">进入</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, reactive } from 'vue';
import { useConfigStore } from '@/stores/config';
import { compatibleAppStoreJump } from '@/utils/index';
import business1 from '@/assets/images/png/business1.png';
import business2 from '@/assets/images/png/business2.png';
import business3 from '@/assets/images/png/business3.png';
import business4 from '@/assets/images/png/business4.png';
import business5 from '@/assets/images/png/business5.png';
const configStore = useConfigStore();
const state = reactive({
cooperationList: <any>[],
});
/**
* 获取官方群聊
*/
const getList = () => {
let sourceList = configStore.appConfig.sourceList || [];
if (sourceList && sourceList.length > 0) {
const list = sourceList.filter((item: any) => {
if (item.type == 'GROUP') {
return item;
}
});
if (list.length > 0) {
let domain = list[0].domain || [];
let newList = domain.filter((item: any) => {
if (item.description.indexOf('商务') > -1) {
return true;
}
});
return newList;
}
}
return [];
};
/**
* 跳转链接
* @param link
*/
const toLink = (link: string) => {
compatibleAppStoreJump(link);
};
/**
* 初始化完成执行
*/
onMounted(() => {
let groupScr = getList();
groupScr.forEach((item, index) => {
if (item.url.indexOf('t.me') > -1) {
// item.name = "Telegram";
item['icon'] = business1;
}
if (item.url.indexOf('ptcc.in') > -1) {
// item.name = "potato";
item['icon'] = business2;
}
if (item.url.indexOf('t.me') > -1) {
// item.name = "potato";
item['icon'] = business3;
}
if (item.url.indexOf('page.dingtalk.com') > -1) {
// item.name = "钉钉";
item['icon'] = business4;
}
if (item.url.indexOf('weibo.com') > -1) {
// item.name = "微博";
item['icon'] = business5;
}
});
state.cooperationList = getList();
});
</script>
<style lang="scss" scoped>
.officialCommunityPage {
width: 100%;
height: 100%;
.content {
padding: 20px 12px;
height: calc(100vh - 44px);
.th {
height: 52px;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
// background: rgb( 27, 29, 51);
background: #2e2e2e;
border-radius: 20px;
padding-right: 15px;
margin-bottom: 20px;
.left {
display: flex;
align-items: center;
img {
width: 52px;
}
span {
font-size: 19px;
color: #ffffff;
padding-left: 10px;
}
}
.right {
width: 70px;
height: 28px;
// background: linear-gradient(to bottom, rgb(232, 95, 131), rgb(73, 58, 190));
background: linear-gradient(180deg, rgb(253, 143, 97), rgb(248, 75, 98), rgb(116, 57, 138));
font-size: 15px;
color: #ffffff;
display: flex;
justify-content: center;
align-items: center;
border-radius: 20px;
}
}
}
}
</style>
+322
View File
@@ -0,0 +1,322 @@
<template>
<div class="avatar_setting">
<Header title="选择头像" rightTitle="保存" @rightFuc="confirm" />
<div class="flex-vcenter justify-center" @click.stop>
<van-uploader :disabled="urrPortraitStatus" :after-read="afterRead">
<div class="avatar_area">
<div class="avatar_border">
<LazyImg
v-if="state.currentAvatar"
class="avatar"
round
:src="state.currentAvatar"
:key="state.currentAvatar"
/>
<div v-else class="avatar-placeholder" />
</div>
<div class="text">从相册中选择</div>
</div>
</van-uploader>
</div>
<div class="avatar_box overflow-y-auto">
<div class="avatarList" v-if="!state.loading">
<div class="avatar_item" v-for="item in state.userAvatars" :key="item">
<LazyImg class="avatar_img" round :src="item" @click="selectAvatar(item)" />
<div class="selected" v-if="state.currentAvatar == item"></div>
</div>
</div>
<Loading v-else />
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'changeAvatar',
};
</script>
<script lang="ts" setup>
import { computed, nextTick, provide, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user';
import UserApi from '@/service/user';
import { showToast, showLoadingToast } from 'vant';
import AppApi from '@/service/app';
const state = reactive({
userInfo: <Types.User.UserInfoType>{},
userAvatars: <Array<any>>[],
currentAvatar: '', //选中的头像
isRequest: false,
loading: true,
});
const router = useRouter();
const userStore = useUserStore();
provide('loading', state.loading);
nextTick(async () => {
console.log('dad');
/**
* 获取用户信息
*/
const userStore = useUserStore();
state.userInfo = userStore.userInfo;
state.currentAvatar = userStore.userInfo.portrait;
getAvatars();
});
// 头像审核
const urrPortraitStatus = computed(() => state.userInfo.urrPortraitStatus == 1);
// 获取头像列表
async function getAvatars() {
try {
let res: any = await UserApi.portraitList({
// type: 'avatar',
pageNumber: 1,
pageSize: 10,
});
console.log(res);
state.userAvatars = res.data || [];
state.loading = false;
} catch (e) {
console.log(e);
}
}
// 选择当前头像
function selectAvatar(item: any) {
if (urrPortraitStatus.value) {
return showToast('头像审核中');
}
state.currentAvatar = item;
}
// 确定修改头像
async function confirm(isDefaultSource = true) {
if (!state.currentAvatar) return router.go(-1);
let req = {
portrait: state.currentAvatar,
isDefaultSource,
};
state.loading = true;
try {
await UserApi.editPersonInfo(req);
state.loading = false;
showToast('修改成功');
state.userInfo.portrait = state.currentAvatar;
userStore.updateUserInfo();
router.go(-1);
} catch (e) {
state.loading = false;
console.log(e);
}
}
// 图片上传
const afterRead = async (file: any) => {
if (file.length) {
file.map(async (item: any) => {
let newFile = await compress(item.file);
item.file = newFile;
await uploadImg(item);
});
} else {
let newFile = await compress(file.file);
file.file = newFile;
uploadImg(file);
}
};
const coverRatio = ref(1); //像素比 默认为1
//图片上传
const uploadImg = async (fileInfo: any) => {
if (urrPortraitStatus.value) {
showToast('头像审核中~');
return false;
}
let params = new FormData();
params.append('upload', fileInfo.file);
showLoadingToast({
message: '上传中...',
forbidClick: true,
});
try {
let res: any = await AppApi.uploadFile(params);
state.currentAvatar = res.coverImg;
confirm(false);
} catch (e) {
showToast('上传失败');
}
};
//将base64转换为文件对象
const convertBase64ToBlob = (base64: any) => {
var base64Arr = base64.split(',');
var imgtype = '';
var base64String = '';
if (base64Arr.length > 1) {
//如果是图片base64,去掉头信息
base64String = base64Arr[1];
imgtype = base64Arr[0].substring(base64Arr[0].indexOf(':') + 1, base64Arr[0].indexOf(';'));
}
// 将base64解码
var bytes = atob(base64String);
//var bytes = base64;
var bytesCode = new ArrayBuffer(bytes.length);
// 转换为类型化数组
var byteArray = new Uint8Array(bytesCode);
// 将base64转换为ascii码
for (var i = 0; i < bytes.length; i++) {
byteArray[i] = bytes.charCodeAt(i);
}
// 生成Blob对象(文件对象)
return new Blob([bytesCode], { type: imgtype });
};
// 压缩图片
const compress = (file: any) => {
if (!file || !window.FileReader) return;
if (/^image/.test(file.type)) {
let reader = new FileReader();
// 获取图片的宽高
// reader.onload = function (e: any) {
// let txt = e.target.result;
// let img = document.createElement('img');
// img.src = txt;
// img.onload = function () {
// console.log('宽度:', img.width);
// console.log('高度:', img.height);
// };
// };
reader.readAsDataURL(file);
return new Promise((resolve) => {
reader.onloadend = function () {
let result: any = reader.result;
let img: any = new Image();
img.src = result;
if (result.length <= 200 * 1024) {
let blob = convertBase64ToBlob(result);
let files = new File([blob], file.name, { type: blob.type });
resolve(files);
return;
}
img.onload = function () {
let canvas = document.createElement('canvas');
let ctx: any = canvas.getContext('2d');
let width = img.width;
let height = img.height;
coverRatio.value = img.height / img.width;
canvas.width = width;
canvas.height = height;
// 铺底色
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, width, height);
//进行最小压缩
let ndata = canvas.toDataURL('image/jpeg', 0.3);
let blob = convertBase64ToBlob(ndata);
let files = new File([blob], file.name, { type: blob.type });
resolve(files);
};
};
});
}
};
</script>
<style lang="scss" scoped>
.avatar_setting {
height: 100%;
display: flex;
flex-direction: column;
background: #000;
.justify-center {
justify-content: center;
}
.avatar_area {
margin-top: 24px;
margin-bottom: 24px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.avatar_border {
height: 80px;
width: 80px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
position: relative;
overflow: hidden;
background: rgba(255, 255, 255, 0.08);
.avatar {
width: 100%;
height: 100%;
border-radius: 50%;
}
.avatar-placeholder {
width: 100%;
height: 100%;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
}
}
.text {
font-size: 14px;
font-weight: 400;
text-align: center;
margin-top: 12px;
color: #666;
line-height: 20px;
}
}
.avatar_box {
flex: 1 0;
height: 0;
padding: 0 38px 16px;
.avatarList {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-column-gap: 20px;
grid-row-gap: 30px;
}
.avatar_item {
height: 60px;
width: 60px;
position: relative;
.avatar_img {
height: 60px;
width: 60px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
}
.selected {
height: 22px;
width: 22px;
background: url('@/assets/images/png/mine/figma/avatar-selected.svg') center / contain no-repeat;
position: absolute;
right: 0;
bottom: 0;
}
}
}
:deep(.right-btn) {
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
}
:deep(.van-uploader__input-wrapper) {
display: flex;
justify-content: center;
}
}
</style>
@@ -0,0 +1,178 @@
<template>
<!-- 列表数据 -->
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="sixGrid">
<van-checkbox-group v-model="state.result" ref="checkboxGroup">
<HorizontalGameBox
class="gameBox"
:videoInfo="item"
v-for="item in state.list"
:key="item.id"
:isEdit="isEdit"
@select="handleSelect"
@del="handleDel"
/>
</van-checkbox-group>
</div>
</PullRefresh>
</template>
<script setup lang="ts">
import { watch, reactive, nextTick, computed } from 'vue';
import UserApi from '@/service/user';
import { useUserStore } from '@/stores/user';
import HomeApi from '@/service/home';
const props = defineProps({
isEdit: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['changeSelectList']);
const { userInfo } = useUserStore();
// 列表数据项
const state = reactive({
list: <Types.Video.VInfosItem[]>[],
pageNumber: 1,
pageSize: 12,
result: <string[]>[],
});
// PullRefresh 参数
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 列表获取
const getList = async (type = '') => {
try {
let res = await UserApi.queryCollectList({
uid: userInfo.uid,
type: 'SEED_LINK',
pageNumber: state.pageNumber,
pageSize: state.pageSize,
});
const list = res.list || [];
moreParams.loading = false;
moreParams.refreshing = false;
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
const handleDel = async (id: string) => {
await HomeApi.collect({
type: 'SEED_LINK',
objID: id,
isCollect: false,
});
state.list = state.list.filter((item: Types.Video.VInfosItem) => item.id !== id);
};
// 数据重置
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
// 触底加载
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
// 下拉刷新
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
const selectAll = computed(() => {
return state.result.length === state.list.length;
});
// 选择
const handleSelect = (id: string) => {
if (state.result.includes(id)) {
state.result = state.result.filter((item: string) => item !== id);
} else {
state.result.push(id);
}
emit('changeSelectList', state.result, selectAll.value);
};
// 全选点击
const toggleSelect = () => {
if (state.result.length === state.list.length) {
state.result = [];
} else {
state.result = state.list.map((item: Types.Video.VInfosItem) => item.id);
}
emit('changeSelectList', state.result, selectAll.value);
};
watch(
() => props.isEdit,
(val) => {
if (!val) {
state.result = [];
}
},
);
watch(
() => state.result,
(val) => {
nextTick(() => {
emit('changeSelectList', val, selectAll.value);
});
},
);
defineExpose({
reset,
onLoad,
onRefresh,
toggleSelect,
});
</script>
<style scoped lang="scss">
.sixGrid {
.van-checkbox-group {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 8px;
grid-row-gap: 10px;
padding: 0 16px;
:deep() {
.gameBox {
width: 168px;
}
.videoBox {
width: 100%;
overflow: hidden;
.coverBox {
border-radius: 6px !important;
}
}
}
}
}
</style>
@@ -0,0 +1,148 @@
<template>
<!-- 列表数据 -->
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="categoryContent">
<CategoryContent
ref="categoryContentRef"
@changeSelectList="changeSelectList"
:isEdit="isEdit"
:contentWrap="true"
:list="state.list"
:contentType="mediaType"
:coverDirection="coverDirection"
@del="handleDel"
/>
</div>
</PullRefresh>
</template>
<script setup lang="ts">
import { ref, reactive, nextTick, computed } from 'vue';
import CategoryContent from '@/views/acgModule/components/categoryContent.vue';
import MediaApi from '@/service/media';
const categoryContentRef = ref();
const emit = defineEmits(['changeSelectList']);
const props = defineProps({
isEdit: {
type: Boolean,
default: false,
},
mediaType: {
type: String,
default: 'image',
},
});
const coverDirection = computed(() => (props.mediaType === 'video' ? 'landscape' : 'portrait'));
// 列表数据项
const state = reactive({
list: <any>[],
pageNumber: 1,
pageSize: 12,
});
// PullRefresh 参数
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 列表获取
const getList = async (type = '') => {
const parmas: any = {
type: props.mediaType,
...state,
};
delete parmas.list;
try {
let res: any = await MediaApi.queryBookshelfs(parmas);
const list = res.list || [];
for (const item of list) {
item.title = item.name;
}
moreParams.loading = false;
moreParams.refreshing = false;
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
// 数据重置
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
const changeSelectList = (result: string[], selectAll: boolean) => {
emit('changeSelectList', result, selectAll);
};
const handleDel = async (id: string) => {
await MediaApi.postBookshelfDel({
id,
});
state.list = state.list.filter((item: Types.Video.VInfosItem) => item.id !== id);
};
reset();
// 触底加载
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
// 下拉刷新
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
defineExpose({
reset,
onLoad,
onRefresh,
toggleSelect: () => {
categoryContentRef.value?.toggleSelect();
},
});
</script>
<style scoped lang="scss">
.categoryContent {
padding: 12px 10px;
:deep() {
.category-content {
.list {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-column-gap: 5px;
grid-row-gap: 12px;
.item {
width: 100% !important;
overflow: hidden;
margin: 0 !important;
}
}
}
.category-content.landscape .list {
grid-template-columns: 1fr 1fr;
grid-column-gap: 7px;
}
}
}
</style>
@@ -0,0 +1,178 @@
<template>
<!-- 列表数据 -->
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="sixGrid">
<van-checkbox-group v-model="state.result" ref="checkboxGroup">
<HorizontalVideoBox
class="videoBox"
:videoInfo="item"
:videoList="state.list"
v-for="item in state.list"
:key="item.id"
:isEdit="isEdit"
@select="handleSelect"
@del="handleDel"
/>
</van-checkbox-group>
</div>
</PullRefresh>
</template>
<script setup lang="ts">
import UserApi from '@/service/user';
import { watch, reactive, nextTick, computed } from 'vue';
import { useUserStore } from '@/stores/user';
import HomeApi from '@/service/home';
const { userInfo } = useUserStore();
const props = defineProps({
isEdit: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['changeSelectList', 'load']);
// 列表数据项
const state = reactive({
list: <Types.Video.VInfosItem[]>[],
pageNumber: 1,
pageSize: 12,
result: <string[]>[],
});
// PullRefresh 参数
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 列表获取
const getList = async (type = '') => {
try {
let res = await UserApi.queryCollectList({
uid: userInfo.uid,
type: 'SP',
pageNumber: state.pageNumber,
pageSize: state.pageSize,
});
const list = res.list || [];
moreParams.loading = false;
moreParams.refreshing = false;
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
emit('load', state.list, 0);
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
const handleDel = async (id: string) => {
await HomeApi.collect({
type: 'SP',
objID: id,
isCollect: false,
});
state.list = state.list.filter((item: Types.Video.VInfosItem) => item.id !== id);
};
// 数据重置
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
// 触底加载
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
// 下拉刷新
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
const selectAll = computed(() => {
return state.result.length === state.list.length;
});
// 选择
const handleSelect = (id: string) => {
if (state.result.includes(id)) {
state.result = state.result.filter((item: string) => item !== id);
} else {
state.result.push(id);
}
emit('changeSelectList', state.result, selectAll.value);
};
// 全选点击
const toggleSelect = () => {
if (state.result.length === state.list.length) {
state.result = [];
} else {
state.result = state.list.map((item: Types.Video.VInfosItem) => item.id);
}
emit('changeSelectList', state.result, selectAll.value);
};
watch(
() => props.isEdit,
(val) => {
if (!val) {
state.result = [];
}
},
);
watch(
() => state.result,
(val) => {
nextTick(() => {
emit('changeSelectList', val, selectAll.value);
});
},
);
defineExpose({
reset,
onLoad,
onRefresh,
toggleSelect,
});
</script>
<style scoped lang="scss">
.sixGrid {
.van-checkbox-group {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 7px;
grid-row-gap: 12px;
padding: 12px 16px;
:deep() {
.videoBox {
width: 100%;
overflow: hidden;
.coverBox {
border-radius: 6px !important;
height: 95px !important;
}
}
}
}
}
</style>
@@ -0,0 +1,159 @@
<template>
<!-- 列表数据 -->
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<van-checkbox-group v-model="state.result" ref="checkboxGroup">
<div class="listBox" v-for="(item, index) in state.list" :key="item.id">
<Post
:list="state.list"
:postInfo="item"
:currentIndex="index"
:isEdit="isEdit"
@del="handleDel"
/>
</div>
</van-checkbox-group>
</PullRefresh>
</template>
<script setup lang="ts">
import UserApi from '@/service/user';
import { watch, reactive, nextTick, computed } from 'vue';
import { useUserStore } from '@/stores/user';
import HomeApi from '@/service/home';
const props = defineProps({
isEdit: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['changeSelectList', 'load']);
const { userInfo } = useUserStore();
// 列表数据项
const state = reactive({
list: <Types.Video.VInfosItem[]>[],
pageNumber: 1,
pageSize: 12,
result: <string[]>[],
});
// PullRefresh 参数
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 列表获取
const getList = async (type = '') => {
try {
let res = await UserApi.queryCollectList({
uid: userInfo.uid,
type: 'COVER',
pageNumber: state.pageNumber,
pageSize: state.pageSize,
});
const list = res.list || [];
moreParams.loading = false;
moreParams.refreshing = false;
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
emit('load', state.list, 2);
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
const handleDel = async (id: string) => {
await HomeApi.collect({
type: 'COVER',
objID: id,
isCollect: false,
});
state.list = state.list.filter((item: Types.Video.VInfosItem) => item.id !== id);
};
// 数据重置
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
// 触底加载
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
// 下拉刷新
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
const selectAll = computed(() => {
return state.result.length === state.list.length;
});
// 选择
const handleSelect = (id: string) => {
if (state.result.includes(id)) {
state.result = state.result.filter((item: string) => item !== id);
} else {
state.result.push(id);
}
emit('changeSelectList', state.result, selectAll.value);
};
// 全选点击
const toggleSelect = () => {
if (state.result.length === state.list.length) {
state.result = [];
} else {
state.result = state.list.map((item: Types.Video.VInfosItem) => item.id);
}
emit('changeSelectList', state.result, selectAll.value);
};
watch(
() => props.isEdit,
(val) => {
if (!val) {
state.result = [];
}
},
);
watch(
() => state.result,
(val) => {
nextTick(() => {
emit('changeSelectList', val, selectAll.value);
});
},
);
defineExpose({
reset,
onLoad,
onRefresh,
toggleSelect,
});
</script>
<style scoped lang="scss">
.listBox {
margin: 0 16px;
padding-top: 12px;
}
</style>
@@ -0,0 +1,188 @@
<template>
<!-- 列表数据 -->
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="sixGrid">
<van-checkbox-group v-model="state.result" ref="checkboxGroup">
<HorizontalVideoBox
class="videoBox"
:videoInfo="item"
:videoList="state.list"
v-for="item in state.list"
:isShort="true"
:key="item.id"
:isEdit="isEdit"
@select="handleSelect"
@del="handleDel"
/>
</van-checkbox-group>
</div>
</PullRefresh>
</template>
<script setup lang="ts">
import { watch, reactive, nextTick, computed } from 'vue';
import UserApi from '@/service/user';
import { useUserStore } from '@/stores/user';
import HomeApi from '@/service/home';
const props = defineProps({
isEdit: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['changeSelectList', 'load']);
const { userInfo } = useUserStore();
// 列表数据项
const state = reactive({
list: <Types.Video.VInfosItem[]>[],
pageNumber: 1,
pageSize: 12,
result: <string[]>[],
});
// PullRefresh 参数
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 列表获取
const getList = async (type = '') => {
try {
let res: any = await UserApi.queryCollectList({
uid: userInfo.uid,
type: 'SHORT',
pageNumber: state.pageNumber,
pageSize: state.pageSize,
});
const list = res.list || [];
moreParams.loading = false;
moreParams.refreshing = false;
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
emit('load', state.list, 1);
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
const handleDel = async (id: string) => {
await HomeApi.collect({
type: 'SHORT',
objID: id,
isCollect: false,
});
state.list = state.list.filter((item: Types.Video.VInfosItem) => item.id !== id);
};
// 数据重置
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
// 触底加载
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
// 下拉刷新
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
const selectAll = computed(() => {
return state.result.length === state.list.length;
});
// 选择
const handleSelect = (id: string) => {
if (state.result.includes(id)) {
state.result = state.result.filter((item: string) => item !== id);
} else {
state.result.push(id);
}
emit('changeSelectList', state.result, selectAll.value);
};
// 全选点击
const toggleSelect = () => {
if (state.result.length === state.list.length) {
state.result = [];
} else {
state.result = state.list.map((item: Types.Video.VInfosItem) => item.id);
}
emit('changeSelectList', state.result, selectAll.value);
};
watch(
() => props.isEdit,
(val) => {
if (!val) {
state.result = [];
}
},
);
watch(
() => state.result,
(val) => {
nextTick(() => {
emit('changeSelectList', val, selectAll.value);
});
},
);
defineExpose({
reset,
onLoad,
onRefresh,
toggleSelect,
});
</script>
<style scoped lang="scss">
.sixGrid {
.van-checkbox-group {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 7px;
grid-row-gap: 12px;
padding: 12px 16px;
:deep() {
.videoBox {
width: 100%;
overflow: hidden;
position: relative;
.coverBox {
border-radius: 6px !important;
height: 224px !important;
}
.videoTitle {
height: auto !important;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1 !important;
-webkit-box-orient: vertical;
}
}
}
}
}
</style>
@@ -0,0 +1,189 @@
<template>
<!-- 列表数据 -->
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="sixGrid">
<van-checkbox-group v-model="state.result" ref="checkboxGroup">
<HorizontalVideoBox
class="videoBox"
:videoInfo="item"
:videoList="state.list"
v-for="item in state.list"
:isShort="true"
:key="item.id"
:isEdit="isEdit"
@select="handleSelect"
:isPic="true"
@del="handleDel"
/>
</van-checkbox-group>
</div>
</PullRefresh>
</template>
<script setup lang="ts">
import { watch, reactive, nextTick, computed } from 'vue';
import UserApi from '@/service/user';
import { useUserStore } from '@/stores/user';
import HomeApi from '@/service/home';
const { userInfo } = useUserStore();
const props = defineProps({
isEdit: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['changeSelectList']);
// 列表数据项
const state = reactive({
list: <Types.Video.VInfosItem[]>[],
pageNumber: 1,
pageSize: 12,
result: <string[]>[],
});
// PullRefresh 参数
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 列表获取
const getList = async (type = '') => {
try {
let res: any = await UserApi.queryCollectList({
uid: userInfo.uid,
type: 'PIC',
pageNumber: state.pageNumber,
pageSize: state.pageSize,
});
const list = res.list || [];
moreParams.loading = false;
moreParams.refreshing = false;
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
const handleDel = async (id: string) => {
await HomeApi.collect({
type: 'PIC',
objID: id,
isCollect: false,
});
state.list = state.list.filter((item: Types.Video.VInfosItem) => item.id !== id);
};
// 数据重置
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
// 触底加载
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
// 下拉刷新
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
const selectAll = computed(() => {
return state.result.length === state.list.length;
});
// 选择
const handleSelect = (id: string) => {
if (state.result.includes(id)) {
state.result = state.result.filter((item: string) => item !== id);
} else {
state.result.push(id);
}
emit('changeSelectList', state.result, selectAll.value);
};
// 全选点击
const toggleSelect = () => {
if (state.result.length === state.list.length) {
state.result = [];
} else {
state.result = state.list.map((item: Types.Video.VInfosItem) => item.id);
}
emit('changeSelectList', state.result, selectAll.value);
};
watch(
() => props.isEdit,
(val) => {
if (!val) {
state.result = [];
}
},
);
watch(
() => state.result,
(val) => {
nextTick(() => {
emit('changeSelectList', val, selectAll.value);
});
},
);
defineExpose({
reset,
onLoad,
onRefresh,
toggleSelect,
});
</script>
<style scoped lang="scss">
.sixGrid {
.van-checkbox-group {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 7px;
grid-row-gap: 12px;
padding: 12px 16px;
:deep() {
.videoBox {
width: 100%;
overflow: hidden;
position: relative;
.coverBox {
border-radius: 6px !important;
height: 224px !important;
}
.videoTitle {
height: auto !important;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1 !important;
-webkit-box-orient: vertical;
}
}
}
}
}
</style>
+226
View File
@@ -0,0 +1,226 @@
<template>
<div class="collection-video" @touchstart.stop @touchmove.stop @touchend.stop>
<Header title="我的收藏">
<template #right>
<div class="edit" :class="{ active: state.isEdit }" @click="state.isEdit = !state.isEdit">
{{ state.isEdit ? '完成' : '编辑' }}
</div>
</template>
</Header>
<van-tabs
v-model:active="state.active"
background="#000"
title-active-color="#fff"
title-inactive-color="rgba(255, 255, 255, 0.5)"
line-height="3"
animated
swipeable
@change="tabChange"
>
<van-tab v-for="(item, index) in state.tabsList" :key="index" :title="item.title">
<div class="tab-content-auto overflow-y-auto" @scroll="onScroll" :id="`tabContent${index}`">
<component
:isEdit="state.isEdit"
v-if="item.comments"
:is="item.comments"
:key="item.name"
:mediaType="item.mediaType || ''"
:mode="item.mode"
@load="(list) => handleLoad(list, index)"
@changeSelectList="changeSelectList"
:ref="(el) => (childRefs[index] = el)"
/>
</div>
</van-tab>
</van-tabs>
</div>
</template>
<script lang="ts">
export default {
name: 'collectVideo',
};
</script>
<script setup lang="ts">
import { ref, watch, reactive, markRaw, onActivated, computed, provide } from 'vue';
import MovieList from './components/MovieList.vue';
import ShortnessVideoList from './components/ShortnessVideoList.vue';
import PostList from './components/PostList.vue';
import MangaList from './components/MangaList.vue';
import TheatresList from './components/TheatresList.vue';
import MineDramaList from '../components/MineDramaList.vue';
const childRefs = ref<any[]>([]);
const state = reactive({
loading: false,
selects: <string[]>[],
active: 0,
tabsList: [
{
title: '视频',
name: 0,
comments: markRaw(MovieList),
scrollTop: 0,
type: 'SP',
list: <Types.Video.VInfosItem[]>[],
},
{
title: '抖音',
name: 1,
comments: markRaw(ShortnessVideoList),
scrollTop: 0,
type: 'SHORT',
list: <Types.Video.VInfosItem[]>[],
},
{
title: '短剧',
name: 'drama',
comments: markRaw(MineDramaList),
scrollTop: 0,
mode: 'collect',
type: 'drama',
list: <Types.Video.VInfosItem[]>[],
},
{
title: '帖子',
name: 2,
comments: markRaw(PostList),
scrollTop: 0,
type: 'COVER',
list: <Types.Video.VInfosItem[]>[],
},
{
title: '动漫',
name: 6,
comments: markRaw(MangaList),
scrollTop: 0,
mediaType: 'video',
type: 'video',
},
{
title: '漫画',
name: 5,
comments: markRaw(MangaList),
scrollTop: 0,
mediaType: 'image',
type: 'image',
},
{
title: '图集',
name: 7,
comments: markRaw(TheatresList),
type: 'PIC',
scrollTop: 0,
},
{
title: '小说',
name: 8,
comments: markRaw(MangaList),
scrollTop: 0,
mediaType: 'text',
type: 'text',
},
],
isEdit: false,
isAll: false,
});
provide(
'isEdit',
computed(() => state.isEdit),
);
provide('editMode', 'delete');
const handleLoad = (list: Types.Video.VInfosItem[], index: number) => {
state.tabsList[index].list = list;
};
const changeSelectList = (ids: string[], selectAll: boolean) => {
state.selects = ids;
state.isAll = selectAll;
};
const tabChange = () => {
state.isEdit = false;
state.selects = [];
setScrollTop(state.tabsList[state.active].scrollTop);
};
const onScroll = (event: any) => {
state.tabsList[state.active].scrollTop = event.target.scrollTop;
};
onActivated(() => {
setScrollTop(state.tabsList[state.active].scrollTop);
});
const setScrollTop = (top: number) => {
const dom = document.getElementById(`tabContent${state.active}`);
if (dom) {
dom.scrollTop = top;
}
};
watch(
() => state.isEdit,
(val) => {
if (!val) {
state.isAll = false;
state.selects = [];
}
},
);
</script>
<style scoped lang="scss">
.collection-video {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
:deep() {
.van-tabs {
flex: 1 0;
height: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
}
.van-tabs__nav {
padding-bottom: 0;
height: 44px;
box-sizing: border-box;
}
.van-tab {
width: auto;
}
.van-tabs__line {
bottom: 7px;
width: 13px;
height: 3px;
border-radius: 12px;
background: linear-gradient(90deg, rgba(255, 192, 0, 0) 0%, #ffc000 100%);
}
.van-tabs__content {
flex: 1 0;
height: 0;
overflow: hidden;
}
.van-tab__panel {
height: 100%;
width: 100%;
overflow: hidden;
}
.tab-content-auto {
height: 100%;
width: 100%;
}
}
.edit {
font-size: 12px;
line-height: 18px;
color: rgba(255, 255, 255, 0.5);
&.active {
color: #ffc000;
}
}
}
</style>
@@ -0,0 +1,233 @@
<template>
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="mine-drama-list">
<DramaCoverCard
v-for="item in list"
:key="item.id"
:item="item"
:is-edit="editing"
:edit-mode="editMode"
:selected="result.includes(item.id)"
@click="goPlay(item)"
@select="handleSelect(item.id)"
@delete="handleDel(item.id)"
/>
</div>
</PullRefresh>
</template>
<script setup lang="ts">
import { computed, inject, nextTick, reactive, ref, watch, type ComputedRef } from 'vue';
import { useRouter } from 'vue-router';
import { showToast } from 'vant';
import { LikeType } from '@/enums/app';
import CommunityApi from '@/service/community';
import MediaApi from '@/service/media';
import ShortDramaApi from '@/service/short-drama';
import DramaCoverCard from '@/views/shortVideo/short-drama/components/DramaCoverCard.vue';
import {
getDramaHistoryList,
historyToSeries,
removeDramaHistory,
} from '@/views/shortVideo/short-drama/utils/dramaLocalWatch';
const props = withDefaults(
defineProps<{
/** like | collect | purchased | history */
mode: 'like' | 'collect' | 'purchased' | 'history';
isEdit?: boolean;
}>(),
{
isEdit: false,
},
);
const emit = defineEmits(['changeSelectList', 'load']);
const injectedEdit = inject<ComputedRef<boolean> | boolean>('isEdit', false);
const injectedEditMode = inject<'delete' | 'select' | ComputedRef<'delete' | 'select'>>('editMode', 'delete');
const editing = computed(() => {
if (props.isEdit) return true;
if (typeof injectedEdit === 'object' && injectedEdit && 'value' in injectedEdit) {
return Boolean(injectedEdit.value);
}
return Boolean(injectedEdit);
});
const editMode = computed(() => {
if (typeof injectedEditMode === 'object' && injectedEditMode && 'value' in injectedEditMode) {
return injectedEditMode.value || 'delete';
}
return injectedEditMode || 'delete';
});
const router = useRouter();
const list = ref<Types.ShortDrama.DramaSeries[]>([]);
const result = ref<string[]>([]);
const pageState = reactive({
pageNumber: 1,
pageSize: 12,
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
const fetchRemote = async () => {
if (props.mode === 'like') {
return ShortDramaApi.getMineLikeList(pageState.pageNumber, pageState.pageSize);
}
if (props.mode === 'collect') {
return ShortDramaApi.getMineCollectList(pageState.pageNumber, pageState.pageSize);
}
return ShortDramaApi.getMineBuyList(pageState.pageNumber, pageState.pageSize);
};
const getList = async (type = '') => {
try {
if (props.mode === 'history') {
const all = await getDramaHistoryList();
const mapped = all.map(historyToSeries);
list.value = mapped;
moreParams.isNoData = mapped.length === 0;
moreParams.finished = true;
moreParams.loading = false;
moreParams.refreshing = false;
moreParams.error = false;
return;
}
const res = await fetchRemote();
const next = res.list || [];
if (type === 'refresh' || pageState.pageNumber === 1) {
list.value = next;
} else {
list.value = list.value.concat(next);
}
moreParams.isNoData = pageState.pageNumber === 1 && next.length === 0;
moreParams.finished = !res.hasNext;
moreParams.error = false;
emit('load', list.value);
} catch {
moreParams.error = true;
if (pageState.pageNumber === 1) {
list.value = [];
moreParams.isNoData = true;
}
} finally {
moreParams.loading = false;
moreParams.refreshing = false;
}
};
const reset = (type = '') => {
nextTick(() => {
pageState.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
moreParams.loading = true;
getList(type);
});
};
reset();
const onLoad = () => {
if (props.mode === 'history' || moreParams.finished) {
moreParams.loading = false;
return;
}
moreParams.loading = true;
pageState.pageNumber += 1;
getList();
};
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
const goPlay = (item: Types.ShortDrama.DramaSeries) => {
void import('@/views/shortVideo/short-drama/DramaPlayPage.vue');
router.push({
path: '/shortDramaPlay',
query: { id: item.id },
});
};
const selectAll = computed(() => result.value.length === list.value.length && list.value.length > 0);
const handleSelect = (id: string) => {
if (result.value.includes(id)) {
result.value = result.value.filter((item) => item !== id);
} else {
result.value = [...result.value, id];
}
emit('changeSelectList', result.value, selectAll.value);
};
const toggleSelect = () => {
if (result.value.length === list.value.length) {
result.value = [];
} else {
result.value = list.value.map((item) => item.id);
}
emit('changeSelectList', result.value, selectAll.value);
};
const handleDel = async (id: string) => {
try {
if (props.mode === 'like') {
await CommunityApi.cancleZan({ type: LikeType.DRAMA, objIDs: [id] });
} else if (props.mode === 'collect') {
await MediaApi.postBookshelfDel({ id });
} else if (props.mode === 'history') {
await removeDramaHistory(id);
} else {
return;
}
list.value = list.value.filter((item) => item.id !== id);
result.value = result.value.filter((item) => item !== id);
moreParams.isNoData = list.value.length === 0;
showToast('删除成功');
} catch (e) {
console.log(e);
}
};
watch(
() => editing.value,
(val) => {
if (!val) {
result.value = [];
emit('changeSelectList', [], false);
}
},
);
defineExpose({
reset,
onLoad,
onRefresh,
toggleSelect,
});
</script>
<script lang="ts">
export default {
name: 'MineDramaList',
};
</script>
<style scoped lang="scss">
.mine-drama-list {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px 7px;
padding: 12px 16px;
justify-items: center;
}
</style>
+120
View File
@@ -0,0 +1,120 @@
<template>
<div class="editNick">
<Header title="修改昵称" rightTitle="保存" @rightFuc="editNick" />
<div class="content overflow-y-auto">
<div class="inputBox">
<div class="subTitle">你的昵称</div>
<van-field
class="nameInput"
v-model="state.value"
:border="false"
placeholder="输入新的昵称"
@click-right-icon="() => (state.value = '')"
:right-icon="state.value ? closeIcon : ''"
/>
<div class="tip">昵称30天内可修改4次</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { nextTick, reactive } from 'vue';
import { showToast } from 'vant';
import UserApi from '@/service/user';
import { useUserStore } from '@/stores/user';
import { useRouter } from 'vue-router';
import closeIcon from '@/assets/images/png/mine/close.png';
const state = reactive({
userInfo: <Types.User.UserInfoType>{},
value: <string>'',
});
const userStore = useUserStore();
const router = useRouter();
nextTick(async () => {
state.userInfo = userStore.userInfo;
});
const editNick = () => {
if (!state.value.trim()) {
showToast('名字不能为空!');
return;
}
let req = {
name: state.value,
};
let res: any = UserApi.editPersonInfo(req);
res.then(() => {
showToast('修改成功');
state.userInfo.name = state.value;
userStore.saveUserInfo(state.userInfo);
router.back();
});
};
</script>
<style lang="scss" scoped>
.editNick {
height: 100%;
background: #000;
.content {
height: calc(100vh - 44px);
.inputBox {
padding: 24px 16px 0;
.subTitle {
font-size: 24px;
font-weight: 500;
color: #fff;
line-height: 34px;
margin-bottom: 20px;
}
.nameInput {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 0;
}
.tip {
font-size: 12px;
color: rgba(255, 255, 255, 0.35);
line-height: 18px;
margin-top: 12px;
}
:deep(.van-cell) {
border-radius: 0;
font-size: 14px;
background: transparent;
height: 42px;
display: flex;
align-items: center;
padding-left: 0;
padding-right: 0;
}
:deep(.van-field__control) {
color: #fff;
font-size: 20px;
}
::placeholder {
color: rgba(255, 255, 255, 0.35);
font-size: 20px;
}
}
}
:deep(.right-btn) {
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
}
}
</style>
+109
View File
@@ -0,0 +1,109 @@
<template>
<div class="editNick">
<Header title="名字"/>
<div class="content overflow-y-auto">
<div class="inputBox">
<p>昵称</p>
<van-field v-model="state.value" :border="false" placeholder="请输入" />
<div class="btn"><span @click="editNick">确定</span></div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { nextTick, reactive } from 'vue';
import { showToast } from 'vant';
import UserApi from '@/service/user';
import { useUserStore } from '@/stores/user';
import { useRouter } from 'vue-router';
const state = reactive({
userInfo: <Types.User.UserInfoType>{},
value: <string>'',
});
const userStore = useUserStore();
const router = useRouter();
nextTick(async () => {
/**
* 获取用户信息
*/
state.userInfo = userStore.userInfo;
});
/**
* 修改昵称
*/
const editNick = () => {
if (!state.value.trim()) {
showToast('名字不能为空!');
return;
}
let req = {
name: state.value,
};
let res: any = UserApi.editPersonInfo(req);
res.then((resData: any) => {
showToast('修改成功');
state.userInfo.name = state.value;
userStore.saveUserInfo(state.userInfo);
router.back();
});
};
</script>
<style lang="scss" scoped>
.editNick {
height:100%;
.content {
height:calc(100vh - 44px);
.inputBox {
padding: 0 10px;
margin-top: 30px;
p{
font-size: 12px;
margin-bottom: 15px;
}
:deep(.van-cell) {
border-radius: 5px;
font-size: 14px;
background: rgb(32 ,39 ,51);
height: 42px;
display: flex;
align-items: center;
}
:deep(.van-field__control) {
color: #fff;
}
::placeholder {
color: rgb( 67, 76, 85);
}
}
.btn {
margin-top: 38px;
display: flex;
justify-content: center;
span {
width: 100%;
height: 47px;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
color: #ffffff;
border-radius: 5px;
background: linear-gradient(to right, rgb(132,164,249), rgb(43,93,222));
}
}
}
}
</style>
+124
View File
@@ -0,0 +1,124 @@
<template>
<div class="editNick">
<Header title="个性签名"/>
<div class="content overflow-y-auto">
<div class="inputBox">
<van-field
v-model="state.value"
rows="2"
autosize
type="textarea"
maxlength="20"
placeholder="请输入签名内容"
show-word-limit
/>
<p>请勿带入任何违规信息,会被永久封禁</p>
</div>
<div class="btn"><span @click="editSign">确定</span></div>
</div>
</div>
</template>
<script lang="ts" setup>
import { nextTick, reactive } from 'vue';
import { showToast } from 'vant';
import UserApi from '@/service/user';
import { useUserStore } from '@/stores/user';
import { useRouter } from 'vue-router';
const state = reactive({
userInfo: <Types.User.UserInfoType>{},
value: <string>'',
});
const userStore = useUserStore();
const router = useRouter();
nextTick(async () => {
/**
* 获取用户信息
*/
state.userInfo = userStore.userInfo;
});
/**
* 修改昵称
*/
const editSign = () => {
if (!state.value.trim()) {
showToast('签名不能为空!');
return;
}
let req = {
summary: state.value,
};
let res: any = UserApi.editPersonInfo(req);
res.then((resData: any) => {
showToast('签名修改成功');
state.userInfo.summary = state.value;
userStore.saveUserInfo(state.userInfo);
router.back();
});
};
</script>
<style lang="scss" scoped>
.editNick {
height:100%;
.content {
height:calc(100vh - 44px);
.inputBox {
margin-top: 20px;
padding: 0 12px;
input {
border: none;
background: none;
color: #ffffff;
width: 100%;
font-size: 14px;
}
p {
margin-top: 20px;
font-size: 12px;
color: rgb(251, 65, 92);
}
}
.btn {
margin-top: 74px;
display: flex;
justify-content: center;
span {
width: 50%;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
font-size: 15px;
color: #ffffff;
border-radius: 20px;
// background: linear-gradient(to bottom, rgb( 247, 138, 171), rgb(116, 57, 138));
background: linear-gradient(
180deg,
rgb(253, 143, 97),
rgb(248, 75, 98),
rgb(115, 57, 138)
);
}
}
:deep(.van-cell) {
background: none;
}
:deep(.van-field__control) {
color: #ffffff;
font-size:14px;
}
.van-cell::after {
border-bottom: 0;
}
}
}
</style>
+202
View File
@@ -0,0 +1,202 @@
<template>
<div class="exchange">
<Header title="兑换码" />
<div class="content overflow-y-auto">
<div class="inputBox">
<!-- <p>兑换码</p> -->
<van-field v-model="state.inputValue" :border="false" placeholder="填写兑换码" />
<div class="btn" @click="exchange"><span>立即兑换</span></div>
</div>
<div class="record" v-if="state.isSHow">
<div class="th">
<div class="left">兑换码</div>
<div class="center">类型</div>
<div class="right">兑换时间</div>
</div>
<div>
<div class="td" v-for="(item, index) in state.exchangeRecordList" :key="index">
<div class="left">{{ item.code }}</div>
<div class="center">{{ authorityType(item.authority, item) }}</div>
<div class="right">{{ dateFormat(item.createdAt) }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import UserApi from '@/service/user';
import { nextTick, reactive } from 'vue';
import { dateFormat } from '@/utils';
import { showToast } from 'vant';
const state = reactive({
exchangeRecordList: <any>[],
inputValue: <string>'',
isSHow: false,
});
/**
* 兑换
*/
const exchange = async () => {
if (!state.inputValue) {
showToast('请输入兑换码');
return;
}
let res: any = await UserApi.useExchange({
code: state.inputValue,
});
showToast('兑换成功');
state.inputValue = '';
await getExchangeRecord();
};
/**
* 获取我的推广用户列表
* @param uid
*/
const getExchangeRecord = async () => {
let res: any = await UserApi.queryExchangeRecord({
pageNumber: 1,
pageSize: 50,
});
state.exchangeRecordList = res.data || [];
nextTick(() => {
state.exchangeRecordList.length > 0 ? (state.isSHow = true) : (state.isSHow = false);
});
};
/**
* 格式化兑换奖励
* @param type
* @param item
*/
const authorityType = (type: any, item: any) => {
switch (type) {
case 'shortVideoVip':
return `${item.reward}天体验卡`;
case 'filmVip':
return '影视vip';
case 'superVip':
return '超级vip';
case 'gold':
return '兑换金币';
case '3dPermanentVip':
return '兑换3天vip';
default:
return '';
}
};
nextTick(async () => {
await getExchangeRecord();
});
</script>
<style lang="scss" scoped>
.exchange {
width: 100%;
height: 100%;
.content {
height: calc(100vh - 44px);
.inputBox {
padding: 0 16px;
margin-top: 18px;
p {
font-size: 12px;
margin-bottom: 15px;
}
:deep(.van-cell) {
border-radius: 8px;
font-size: 14px;
background: #202133;
height: 42px;
display: flex;
align-items: center;
}
:deep(.van-field__control) {
color: #fff;
}
::placeholder {
color: #666;
}
}
.btn {
margin-top: 12px;
display: flex;
justify-content: center;
span {
width: 100%;
height: 47px;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
color: #ffffff;
border-radius: 4px;
// background: linear-gradient(90deg, #439fff 0%, #0865fd 94.27%);
background: rgba(52, 118, 255, 1);
}
}
.record {
margin: 24px 16px 0;
.th {
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255, 255, 255, 0.05);
// background: #202133;
border-radius: 8px 8px 0px 0px;
font-size: 12px;
color: #ffffff;
width: 100%;
height: 38px;
& > div {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.left {
width: 28%;
}
.center {
width: 34%;
}
.right {
width: 38%;
}
}
.td {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
width: 100%;
height: 38px;
background-color: #1f2732;
& > div {
width: 33.33%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
color: #939393;
}
.left {
width: 28%;
}
.center {
width: 34%;
}
.right {
width: 38%;
}
}
}
}
}
</style>
+251
View File
@@ -0,0 +1,251 @@
<template>
<div class="exchange">
<Header title="领取兑换" />
<div class="content overflow-y-auto">
<div class="tips">
<p>输入兑换码</p>
<p>每个兑换码只能输入一次</p>
</div>
<div class="inputBox">
<div class="title">兑换码</div>
<van-field v-model="state.inputValue" :border="false" placeholder="请输入兑换码(字母大写)" />
</div>
<div class="share" @click="router.push('/officialCommunity')">官方社群领取更多福利</div>
<div class="recordTitle">兑换记录</div>
<div class="record">
<div class="th">
<div class="left">兑换</div>
<div class="center">兑换类型</div>
<div class="right">兑换时间</div>
</div>
<div class="record-list" v-if="state.exchangeRecordList.length > 0">
<div class="td" v-for="(item, index) in state.exchangeRecordList" :key="index">
<div class="left">{{ item.code }}</div>
<div class="center">{{ item.desc }}</div>
<div class="right">{{ moment(item.createdAt).format('YYYY.MM.DD') }}</div>
</div>
</div>
</div>
</div>
<div class="footer">
<div class="btn" @click="exchange">立即兑换</div>
</div>
</div>
</template>
<script lang="ts" setup>
import UserApi from '@/service/user';
import { nextTick, reactive } from 'vue';
import { showToast } from 'vant';
import moment from 'moment';
import router from '@/router';
const state = reactive({
exchangeRecordList: <any>[],
inputValue: <string>'',
isShow: false,
});
/**
* 兑换
*/
const exchange = async () => {
if (!state.inputValue) {
showToast('请输入兑换码');
return;
}
await UserApi.useExchange({
code: state.inputValue,
});
showToast('兑换成功');
state.inputValue = '';
await getExchangeRecord();
};
/**
* 获取我的兑换记录列表
*/
const getExchangeRecord = async () => {
let res: any = await UserApi.queryExchangeRecord({
pageNumber: 1,
pageSize: 50,
});
let list = res.data || [];
state.exchangeRecordList = list || [];
nextTick(() => {
state.exchangeRecordList.length > 0 ? (state.isShow = true) : (state.isShow = false);
});
};
nextTick(async () => {
await getExchangeRecord();
});
</script>
<style lang="scss" scoped>
.exchange {
width: 100%;
height: 100%;
background: #000;
.content {
height: calc(100% - 44px);
padding: 12px 28px calc(80px + env(safe-area-inset-bottom));
box-sizing: border-box;
.tips {
p {
&:nth-child(1) {
font-weight: 500;
font-size: 30px;
line-height: normal;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 12px;
}
&:nth-child(2) {
font-size: 16px;
line-height: normal;
color: rgba(255, 255, 255, 0.35);
margin-bottom: 0;
}
}
}
.inputBox {
margin-top: 36px;
display: flex;
align-items: center;
border-bottom: 0.5px solid rgba(255, 255, 255, 0.1);
padding-bottom: 11px;
.title {
font-size: 14px;
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
flex-shrink: 0;
white-space: nowrap;
line-height: normal;
}
:deep(.van-cell) {
padding: 0 0 0 12px;
font-size: 14px;
background: transparent;
height: auto;
display: flex;
align-items: center;
flex: 1;
overflow: hidden;
}
:deep(.van-field__control) {
color: rgba(255, 255, 255, 0.9);
text-align: left;
}
::placeholder {
color: rgba(255, 255, 255, 0.35);
}
}
.share {
font-size: 12px;
line-height: normal;
color: rgba(255, 255, 255, 0.35);
margin-top: 18px;
margin-bottom: 24px;
}
.recordTitle {
font-size: 18px;
line-height: 24px;
color: rgba(255, 255, 255, 0.9);
font-weight: 600;
margin-bottom: 12px;
}
.record {
.th {
display: flex;
align-items: center;
gap: 5px;
width: 100%;
height: 42px;
margin-bottom: 12px;
& > div {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background: rgba(255, 255, 255, 0.05);
flex: 1;
font-size: 14px;
line-height: 20px;
color: rgba(255, 255, 255, 0.9);
}
}
.record-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.td {
display: flex;
align-items: center;
gap: 5px;
width: 100%;
font-size: 12px;
line-height: 18px;
color: rgba(255, 255, 255, 0.55);
padding-bottom: 12px;
border-bottom: 0.5px solid rgba(255, 255, 255, 0.1);
box-sizing: border-box;
& > div {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
min-width: 0;
word-break: break-all;
}
&:last-child {
border-bottom: none;
padding-bottom: 0;
}
}
}
}
.footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, #000 40%);
box-sizing: border-box;
}
.btn {
width: 100%;
height: 44px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 44px;
background: #ffc000;
color: #1f2029;
font-size: 16px;
font-weight: 500;
line-height: 22px;
}
}
</style>
+60
View File
@@ -0,0 +1,60 @@
<template>
<div class="feedback">
<Header title="帮助反馈" />
<div class="titles">常见问题与反馈</div>
<div class="content overflow-y-auto">
<div class="question" style="margin-top: 8px">为什么充值不了?</div>
<div class="answer">由于网络问题,请尝试其他的充值渠道或重启app再次尝试获取充值链接。</div>
<div class="question">充值后没到账</div>
<div class="answer">充值成功后通常会在数分钟内到账,请刷新APP或重启后查看。</div>
<div class="question">如何分享给好友?</div>
<div class="answer">
点击【我的】—【分享好友】即可进入分享界面。在分享页面点击【保存图片分享】即可保存图片到相册,要分享时选择相册图片即可;点击【复制分享链接】即可复制文字分享,分享时长按粘贴即可
</div>
<div class="question">分享给好友后,没有送VIP</div>
<div class="answer">邀请好友注册后,需查看好友推广码是否有填写为你的分享码,若没有则需手动填写</div>
<div class="question">购买会员/金币支付失败</div>
<div class="answer">使用支付宝支付失败时,请间隔2分钟重试,如果还是不行,可以换种支付方式</div>
<div class="question">如何填写邀请码</div>
<div class="answer">
在「我的」页面点击个人头像或设置即可跳转页面,然后点击「输入推广码」打开新页面,输入对方的推广码即可(邀请码字母大写)
</div>
<div class="question">忘记密码,如何找回账号</div>
<div class="answer">请在「我的」页面-账号找回,通过账号凭证或绑定的手机号找回账号</div>
</div>
</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped>
.feedback {
height: 100%;
width: 100%;
box-sizing: border-box;
.titles {
font-size: 16px;
font-weight: 500;
margin: 10px 0 10px 15px;
}
.content {
height: calc(100vh - 73px);
padding: 0 15px 50px 15px;
box-sizing: border-box;
font-size: 12px;
.question {
margin: 18px 0 10px 0;
color: #cdcdcd;
}
.answer {
margin: 15px 0 0 0;
padding-bottom: 12px;
color: #999;
line-height: normal;
border-bottom: 1px solid #212121;
}
}
}
</style>
@@ -0,0 +1,180 @@
<template>
<div class="postboxs overflow-y-auto">
<PullRefresh v-if="!state.loading" :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="post">
<div class="boxs" v-for="(item, index) in state.list" :key="index">
<div class="bgbox" @click="$router.push({ path: '/communityTagDeatil', query: { id: item.id } })">
<lazyImg :src="item.coverImg" :key="item.coverImg" class="avatar" />
<div class="dialog">
<p>{{ item.name }}</p>
{{ item.videoCount || 0 }}个帖子
</div>
</div>
<div class="btn flex-center-center" @click="followUp(item)">
{{ item.hasCollected ? '取消关注' : '关注' }}
</div>
</div>
</div>
</PullRefresh>
<Loading v-else />
</div>
</template>
<script lang="ts" setup>
import { reactive, nextTick } from 'vue';
import UserApi from '@/service/user';
import { useUserStore } from '@/stores/user';
import CommunityApi from '@/service/community';
import { showToast } from 'vant';
const userStore = useUserStore();
const state = reactive({
loading: true,
pageNumber: 1,
pageSize: 20,
list: [],
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 获取话题
const getList = async (type: any) => {
try {
let req = {
pageNumber: state.pageNumber,
pageSize: state.pageSize,
type: 'tag',
uid: userStore.userInfo.uid,
};
let res: any = await UserApi.queryCollectList(req);
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (type === 'refresh') {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1 && state.list.length == 0) {
moreParams.isNoData = true;
}
if (!res.hasNext) {
moreParams.finished = true;
}
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
nextTick(() => {
getList();
});
// 关注
const followUp = async (data: any) => {
try {
let isCollect = data.hasCollected;
let req = {
isCollect: !isCollect,
objID: data.id,
type: 'tag',
};
await CommunityApi.collect(req);
data.hasCollected = !isCollect;
showToast(`${data.hasCollected ? '关注' : '取消'}成功`);
} catch (e) {
console.log(e);
}
};
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList(null);
};
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
getList('refresh');
};
</script>
<style lang="scss" scoped>
.postboxs {
width: 100%;
height: calc(100vh - 90px);
padding: 10px;
box-sizing: border-box;
.post {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
grid-column-gap: 20px;
grid-auto-flow: row dense;
}
.boxs {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
margin-bottom: 15px;
.bgbox {
width: 100%;
height: 99px;
.avatar {
width: 100%;
height: 100%;
}
.dialog {
position: absolute;
width: 100%;
height: 100%;
background: rgb(0, 0, 0, 0.5);
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
font-size: 12px;
p {
font-size: 14px;
font-weight: bold;
// margin-bottom: 25px;
margin-bottom: 10px;
}
}
}
// .btn{
// margin-top: 10px;
// width: 82px;
// height: 26px;
// font-size: 14px;
// border-radius: 20px;
// background: linear-gradient(to right, rgb(132,164,249), rgb(43,93,222));
// }
.btn {
margin-top: 10px;
width: 108px;
height: 27px;
font-size: 12px;
font-weight: 600;
border-radius: 15px;
background: rgba(52, 118, 255, 1);
// background: linear-gradient(to right, rgb(132,164,249), rgb(43,93,222));
}
}
}
</style>
@@ -0,0 +1,161 @@
<template>
<div class="userboxs overflow-y-auto">
<PullRefresh v-if="!state.loading" :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div
class="userList"
v-for="(item, index) in state.list"
:key="index"
@click="$router.push({ path: '/userInfo', query: { uid: item.uid } })"
>
<div class="left">
<lazyImg class="avatar" :src="item.portrait" round :key="item.portrait" />
<div>
<p>{{ item.name }}</p>
<span>{{ item.summary || '-' }}</span>
</div>
</div>
<div class="right flex-center-center">
<div class="btn flex-center-center" @click.stop="followUp(item)">
{{ item.hasFollow ? '取消关注' : '关注' }}
</div>
</div>
</div>
</PullRefresh>
<Loading v-else />
</div>
</template>
<script lang="ts" setup>
import { reactive, nextTick } from 'vue';
import UserApi from '@/service/user';
import CommunityApi from '@/service/community';
import { showToast } from 'vant';
const state = reactive({
loading: true,
pageNumber: 1,
pageSize: 10,
list: [],
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 获取话题
const getList = async (type: any) => {
try {
let req = {
pageNumber: state.pageNumber,
pageSize: state.pageSize,
};
let res: any = await UserApi.queryFollowList(req);
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (type === 'refresh') {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1 && state.list.length == 0) {
moreParams.isNoData = true;
}
if (!res.hasNext) {
moreParams.finished = true;
}
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
nextTick(() => {
getList();
});
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList(null);
};
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
getList('refresh');
};
// 关注
const followUp = async (data: any) => {
try {
let req = {
isFollow: !data.hasFollow,
followUID: data?.uid,
};
await CommunityApi.careOrcancle(req);
data.hasFollow = !data.hasFollow;
showToast(`${data.hasFollow ? '关注' : '取消'}成功`);
} catch (e) {
console.log(e);
}
};
</script>
<style lang="scss" scoped>
.userboxs {
width: 100%;
height: calc(100vh - 80px);
padding: 10px;
box-sizing: border-box;
.userList {
width: 100%;
margin-bottom: 30px;
height: 50px;
display: flex;
justify-content: space-between;
padding-top: 20px;
.left {
display: flex;
align-items: center;
.avatar {
width: 60px;
height: 60px;
margin-right: 15px;
}
p {
font-size: 16px;
margin: 10px 0;
width: 250px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
span {
font-size: 14px;
color: rgb(172, 186, 191);
display: inline-block;
width: 250px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.btn {
width: 82px;
height: 26px;
font-size: 14px;
border-radius: 20px;
background: linear-gradient(to right, rgb(132, 164, 249), rgb(43, 93, 222));
}
}
}
</style>
@@ -0,0 +1,190 @@
<template>
<div class="userboxs overflow-y-auto">
<PullRefresh v-if="!state.loading" :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div
class="userList"
v-for="(item, index) in state.list"
:key="index"
@click="$router.push({ path: '/userInfo', query: { uid: item.uid } })"
>
<div class="left">
<lazyImg class="avatar" round :src="item.portrait" :key="item.portrait" />
<div class="box">
<p>{{ item.name }}</p>
<div class="follow-info">
<span>{{ watchCount(item.totalWorks) }}作品</span>
<span>{{ watchCount(item.fans) }}粉丝</span>
</div>
</div>
</div>
<div class="right flex-center-center">
<div class="btn flex-center-center" @click.stop="followUp(item)">已关注</div>
</div>
</div>
</PullRefresh>
<Loading v-else />
</div>
</template>
<script lang="ts" setup>
import { reactive, nextTick } from 'vue';
import UserApi from '@/service/user';
import CommunityApi from '@/service/community';
import { showToast } from 'vant';
import { watchCount } from '@/utils/index';
const state = reactive({
loading: true,
pageNumber: 1,
pageSize: 10,
list: <any>[],
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 获取话题
const getList = async (type: any) => {
try {
let req = {
pageNumber: state.pageNumber,
pageSize: state.pageSize,
};
let res: any = await UserApi.queryFollowList(req);
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (type === 'refresh') {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1 && state.list.length == 0) {
moreParams.isNoData = true;
}
if (!res.hasNext) {
moreParams.finished = true;
}
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
nextTick(() => {
getList('');
});
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList(null);
};
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
getList('refresh');
};
// 关注
const followUp = async (data: any) => {
try {
let req = {
isFollow: !data.hasFollow,
followUID: data?.uid,
};
await CommunityApi.careOrcancle(req);
data.hasFollow = !data.hasFollow;
showToast(`${data.hasFollow ? '关注' : '取消'}成功`);
} catch (e) {
console.log(e);
}
};
</script>
<style lang="scss" scoped>
.userboxs {
width: 100%;
height: calc(100vh - 90px);
padding: 14px 0;
box-sizing: border-box;
.userList {
width: calc(100% - 32px);
height: 66px;
margin: 0 16px 12px;
padding: 12px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
.left {
display: flex;
align-items: center;
overflow: hidden;
flex: 1;
min-width: 0;
.avatar {
width: 40px;
height: 40px;
margin-right: 12px;
flex-shrink: 0;
}
.box {
overflow: hidden;
min-width: 0;
}
p {
font-size: 16px;
font-weight: 500;
color: #fff;
margin: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: left;
}
.follow-info {
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
margin-top: 4px;
span {
margin-right: 8px;
}
}
}
.right {
flex-shrink: 0;
margin-left: 8px;
.btn {
width: 60px;
height: 24px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.5);
font-size: 12px;
}
}
}
}
</style>
+63
View File
@@ -0,0 +1,63 @@
<template>
<div class="follow">
<Header title="我的关注" />
<userBox />
<!-- <van-tabs v-model:active="state.tabActive" type="card">
<van-tab title="用户">
<userBox v-if="state.tabActive == 0" />
</van-tab>
<van-tab title="圈子">
<postBox v-if="state.tabActive == 1" />
</van-tab>
</van-tabs> -->
</div>
</template>
<script lang="ts">
export default {
name: 'follow',
};
</script>
<script lang="ts" setup>
import { nextTick, reactive, computed, onActivated, onDeactivated } from 'vue';
import postBox from './components/postBox.vue';
import userBox from './components/userBox.vue';
const state = reactive({
tabActive: 0,
});
</script>
<style lang="scss" scoped>
.follow {
width: 100%;
height: 100%;
background: #000;
:deep(.van-tabs__wrap) {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
}
:deep(.van-tabs__nav--card) {
border-radius: 30px;
width: 188px !important;
background: rgb(32, 37, 47);
// background: rgba(255, 255, 255, 0.05);
border: none;
}
:deep(.van-tab--card) {
background: rgb(32, 37, 47);
// background: rgba(255, 255, 255, 0.05);
border-radius: 30px;
border: none;
color: rgb(172, 186, 191);
}
:deep(.van-tab--card.van-tab--active) {
// background: linear-gradient(to right, rgb(132,164,249), rgb(43,93,222));
background: rgba(52, 118, 255, 1);
color: #fff;
}
}
</style>
@@ -0,0 +1,111 @@
<template>
<!-- 列表数据 -->
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="sixGrid">
<HorizontalGameBox class="gameBox" :videoInfo="item" v-for="item in state.list" :key="item.id" @del="handleDel" />
</div>
</PullRefresh>
</template>
<script setup lang="ts">
import { reactive, nextTick } from 'vue';
import { queryHistoryVideo } from '@/utils';
import { HistoryVideoType } from '@/enums/app';
import { closeDB, deleteDB, openDB, dbName, dbVersion } from '@/utils/indexDB';
// 列表数据项
const state = reactive({
list: <Types.Video.VInfosItem[]>[],
pageNumber: 1,
pageSize: 12,
});
// PullRefresh 参数
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 列表获取
const getList = async (type = '') => {
try {
let res: any = await queryHistoryVideo(state.pageNumber, state.pageSize, HistoryVideoType.SEEKBRANCHSTORY);
const list = res || [];
moreParams.loading = false;
moreParams.refreshing = false;
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
const handleDel = async (id: string) => {
let db = await openDB(dbName, dbVersion);
const res = await deleteDB(db, HistoryVideoType.SEEKBRANCHSTORY, id);
console.log(res);
await closeDB(db);
if (res) {
state.list = state.list.filter((item: Types.Video.VInfosItem) => item.id !== id);
}
};
// 数据重置
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
// 触底加载
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
// 下拉刷新
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
defineExpose({
reset,
onLoad,
onRefresh,
});
</script>
<style scoped lang="scss">
.sixGrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 8px;
grid-row-gap: 10px;
padding: 0 16px;
:deep() {
.gameBox {
width: 168px;
}
.videoBox {
width: 100%;
overflow: hidden;
.coverBox {
border-radius: 6px !important;
}
}
}
}
</style>
@@ -0,0 +1,133 @@
<template>
<!-- 列表数据 -->
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="categoryContent">
<CategoryContent
:contentWrap="true"
:list="state.list"
:contentType="mediaType"
:coverDirection="coverDirection"
@del="handleDel"
/>
</div>
</PullRefresh>
</template>
<script setup lang="ts">
import { reactive, nextTick, computed } from 'vue';
import CategoryContent from '@/views/acgModule/components/categoryContent.vue';
import { queryHistoryVideo } from '@/utils';
import { HistoryVideoType } from '@/enums/app';
import { closeDB, deleteDB, openDB, dbName, dbVersion } from '@/utils/indexDB';
const props = defineProps({
mediaType: {
type: String,
default: 'image',
},
dbType: {
type: String,
default: 'acgImage',
},
});
const coverDirection = computed(() => (props.mediaType === 'video' ? 'landscape' : 'portrait'));
// 列表数据项
const state = reactive({
list: <any>[],
pageNumber: 1,
pageSize: 12,
});
// PullRefresh 参数
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 列表获取
const getList = async (type = '') => {
try {
let res: any = await queryHistoryVideo(state.pageNumber, state.pageSize, props.dbType as HistoryVideoType);
const list = res || [];
moreParams.loading = false;
moreParams.refreshing = false;
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
const handleDel = async (id: string) => {
let db = await openDB(dbName, dbVersion);
const res = await deleteDB(db, props.dbType as HistoryVideoType, id);
console.log(res);
await closeDB(db);
if (res) {
state.list = state.list.filter((item: Types.Video.VInfosItem) => item.id !== id);
}
};
// 数据重置
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
// 触底加载
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
// 下拉刷新
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
defineExpose({
reset,
onLoad,
onRefresh,
});
</script>
<style scoped lang="scss">
.categoryContent {
padding: 12px 10px;
:deep() {
.category-content {
.list {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-column-gap: 5px;
grid-row-gap: 12px;
.item {
width: 100% !important;
overflow: hidden;
margin: 0 !important;
}
}
}
.category-content.landscape .list {
grid-template-columns: 1fr 1fr;
grid-column-gap: 7px;
}
}
}
</style>
@@ -0,0 +1,125 @@
<template>
<!-- 列表数据 -->
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="sixGrid">
<HorizontalVideoBox
class="videoBox"
:videoInfo="item"
:videoList="state.list"
v-for="item in state.list"
:key="item.id"
:is-edit="isEdit"
edit-mode="delete"
@del="handleDel"
/>
</div>
</PullRefresh>
</template>
<script setup lang="ts">
import { reactive, nextTick } from 'vue';
import { queryHistoryVideo } from '@/utils';
import { HistoryVideoType } from '@/enums/app';
import { closeDB, deleteDB, openDB, dbName, dbVersion } from '@/utils/indexDB';
// 列表数据项
const props = defineProps({
isEdit: {
type: Boolean,
default: false,
},
});
const state = reactive({
list: <Types.Video.VInfosItem[]>[],
pageNumber: 1,
pageSize: 12,
});
// PullRefresh 参数
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 列表获取
const getList = async (type = '') => {
try {
let res: any = await queryHistoryVideo(state.pageNumber, state.pageSize, HistoryVideoType.VIDEO);
const list = res || [];
moreParams.loading = false;
moreParams.refreshing = false;
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
const handleDel = async (id: string) => {
let db = await openDB(dbName, dbVersion);
const res = await deleteDB(db, HistoryVideoType.VIDEO, id);
console.log(res);
await closeDB(db);
if (res) {
state.list = state.list.filter((item: Types.Video.VInfosItem) => item.id !== id);
}
};
// 数据重置
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
// 触底加载
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
// 下拉刷新
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
defineExpose({
reset,
onLoad,
onRefresh,
});
</script>
<style scoped lang="scss">
.sixGrid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-column-gap: 7px;
grid-row-gap: 12px;
padding: 12px 10px;
:deep() {
.videoBox {
width: 100%;
overflow: hidden;
.coverBox {
border-radius: 6px !important;
height: 96px !important;
}
}
}
}
</style>
@@ -0,0 +1,108 @@
<template>
<!-- 列表数据 -->
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="listBox" v-for="(item, index) in state.list" :key="item.id">
<Post
:list="state.list"
:postInfo="item"
:currentIndex="index"
:is-edit="isEdit"
edit-mode="delete"
@del="handleDel"
/>
</div>
</PullRefresh>
</template>
<script setup lang="ts">
import { reactive, nextTick } from 'vue';
import { queryHistoryVideo } from '@/utils';
import { HistoryVideoType } from '@/enums/app';
import { closeDB, deleteDB, openDB, dbName, dbVersion } from '@/utils/indexDB';
defineProps({
isEdit: {
type: Boolean,
default: false,
},
});
// 列表数据项
const state = reactive({
list: <any>[],
pageNumber: 1,
pageSize: 12,
});
// PullRefresh 参数
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 列表获取
const getList = async (type = '') => {
try {
let res: any = await queryHistoryVideo(state.pageNumber, state.pageSize, HistoryVideoType.POST);
const list = res || [];
moreParams.loading = false;
moreParams.refreshing = false;
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
const handleDel = async (id: string) => {
let db = await openDB(dbName, dbVersion);
const res = await deleteDB(db, HistoryVideoType.POST, id);
console.log(res);
await closeDB(db);
if (res) {
state.list = state.list.filter((item: Types.Video.VInfosItem) => item.id !== id);
}
};
// 数据重置
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
// 触底加载
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
// 下拉刷新
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
defineExpose({
reset,
onLoad,
onRefresh,
});
</script>
<style scoped lang="scss">
.listBox {
margin: 0 10px;
}
</style>
@@ -0,0 +1,139 @@
<template>
<!-- 列表数据 -->
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="sixGrid">
<HorizontalVideoBox
class="videoBox"
:videoInfo="item"
:videoList="state.list"
v-for="item in state.list"
:key="item.id"
:isShort="true"
:is-edit="isEdit"
edit-mode="delete"
@del="handleDel"
/>
</div>
</PullRefresh>
</template>
<script setup lang="ts">
import { reactive, nextTick } from 'vue';
import { queryHistoryVideo } from '@/utils';
import { HistoryVideoType } from '@/enums/app';
import { closeDB, deleteDB, openDB, dbName, dbVersion } from '@/utils/indexDB';
/** 短剧复用 VerticalVideo 时曾误写入抖音历史:映射数据没有 publisher */
const isDouyinHistoryItem = (item: Types.Video.VInfosItem) => Boolean(item.publisher);
defineProps({
isEdit: {
type: Boolean,
default: false,
},
});
// 列表数据项
const state = reactive({
list: <Types.Video.VInfosItem[]>[],
});
/** IndexedDB 抖音历史最多 50 条,一次取出再过滤,避免短剧占满首页导致空列表 */
const HISTORY_QUERY_SIZE = 50;
// PullRefresh 参数
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 列表获取
const getList = async (type = '') => {
try {
const res = (await queryHistoryVideo(1, HISTORY_QUERY_SIZE, HistoryVideoType.LACKVIDEO)) || [];
const list = (Array.isArray(res) ? res : []).filter(isDouyinHistoryItem);
moreParams.loading = false;
moreParams.refreshing = false;
moreParams.isNoData = list.length === 0;
moreParams.finished = true;
moreParams.error = false;
state.list = list;
} catch (e) {
moreParams.loading = false;
moreParams.refreshing = false;
moreParams.error = true;
if (type === 'refresh' || state.list.length === 0) {
state.list = [];
moreParams.isNoData = true;
}
}
};
const handleDel = async (id: string) => {
let db = await openDB(dbName, dbVersion);
const res = await deleteDB(db, HistoryVideoType.LACKVIDEO, id);
console.log(res);
await closeDB(db);
if (res) {
state.list = state.list.filter((item: Types.Video.VInfosItem) => item.id !== id);
}
};
// 数据重置
const reset = (type = '') => {
nextTick(() => {
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
const onLoad = () => {
moreParams.loading = false;
};
// 下拉刷新
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
defineExpose({
reset,
onLoad,
onRefresh,
});
</script>
<style scoped lang="scss">
:deep(.horizontalVideoBox .coverBox) {
height: 234px !important;
}
.sixGrid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-column-gap: 5px;
grid-row-gap: 12px;
padding: 12px 10px;
:deep() {
.videoBox {
width: 100%;
overflow: hidden;
position: relative;
.coverBox {
border-radius: 6px !important;
height: 148px !important;
}
.videoTitle {
height: auto !important;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1 !important;
-webkit-box-orient: vertical;
}
}
}
}
</style>
@@ -0,0 +1,137 @@
<template>
<!-- 列表数据 -->
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="sixGrid">
<HorizontalVideoBox
class="videoBox"
:videoInfo="item"
:videoList="state.list"
v-for="item in state.list"
:isShort="true"
:key="item.id"
:isPic="true"
:is-edit="isEdit"
edit-mode="delete"
@del="handleDel"
/>
</div>
</PullRefresh>
</template>
<script setup lang="ts">
import { reactive, nextTick } from 'vue';
import { queryHistoryVideo } from '@/utils';
import { HistoryVideoType } from '@/enums/app';
import { closeDB, deleteDB, openDB, dbName, dbVersion } from '@/utils/indexDB';
defineProps({
isEdit: {
type: Boolean,
default: false,
},
});
// 列表数据项
const state = reactive({
list: <Types.Video.VInfosItem[]>[],
pageNumber: 1,
pageSize: 12,
});
// PullRefresh 参数
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 列表获取
const getList = async (type = '') => {
try {
let res: any = await queryHistoryVideo(state.pageNumber, state.pageSize, HistoryVideoType.PIC);
const list = res || [];
moreParams.loading = false;
moreParams.refreshing = false;
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
const handleDel = async (id: string) => {
let db = await openDB(dbName, dbVersion);
const res = await deleteDB(db, HistoryVideoType.PIC, id);
console.log(res);
await closeDB(db);
if (res) {
state.list = state.list.filter((item: Types.Video.VInfosItem) => item.id !== id);
}
};
// 数据重置
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
// 触底加载
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
// 下拉刷新
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
defineExpose({
reset,
onLoad,
onRefresh,
});
</script>
<style scoped lang="scss">
:deep(.horizontalVideoBox .coverBox) {
height: 234px !important;
}
.sixGrid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-column-gap: 5px;
grid-row-gap: 12px;
padding: 12px 10px;
:deep() {
.videoBox {
width: 100%;
position: relative;
.coverBox {
height: 148px !important;
border-radius: 6px !important;
}
.videoTitle {
height: auto !important;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1 !important;
-webkit-box-orient: vertical;
}
}
}
}
</style>
+264
View File
@@ -0,0 +1,264 @@
<template>
<div class="history-record overflow-y-auto" @touchstart.stop @touchmove.stop @touchend.stop>
<Header title="我的历史">
<template #right>
<div class="edit" :class="{ active: state.isEdit }" @click="handleEdit">
{{ state.isEdit ? '完成' : '编辑' }}
</div>
</template>
</Header>
<van-tabs
v-model:active="state.active"
background="#000"
title-active-color="#fff"
title-inactive-color="rgba(255, 255, 255, 0.5)"
line-height="3"
animated
swipeable
@change="tabChange"
>
<van-tab v-for="(item, index) in state.tabsList" :key="index" :title="item.title">
<div class="tab-content-auto overflow-y-auto" @scroll="onScroll" :id="`tabContent${index}`">
<component
v-if="item.comments"
:is="item.comments"
:key="item.name"
:mediaType="item.mediaType || null"
:dbType="item.dbType || null"
:mode="item.mode"
:ref="(el) => (childRefs[index] = el)"
v-model:isAllChecked="state.isAllChecked"
v-model:deleteCount="state.deleteIds"
:isEdit="state.isEdit"
/>
</div>
</van-tab>
</van-tabs>
<!-- <div class="delete-box" v-if="state.isEdit">
<div class="delete-box-item" @click="handleAllChecked">{{ state.isAllChecked ? '取消全选' : '全选' }}</div>
<div class="delete-box-item" @click="handleDelete">删除({{ state.deleteIds.length }})</div>
</div> -->
</div>
</template>
<script lang="ts">
export default {
name: 'historyRecord',
};
</script>
<script setup lang="ts">
import { ref, reactive, markRaw, onActivated, provide, computed } from 'vue';
import MovieList from './components/MovieList.vue';
import ShortnessVideoList from './components/ShortnessVideoList.vue';
import PostList from './components/PostList.vue';
import MangaList from './components/MangaList.vue';
import GameList from './components/GameList.vue';
import TheatresList from './components/TheatresList.vue';
import MineDramaList from '../components/MineDramaList.vue';
import { HistoryVideoType } from '@/enums/app';
import { showSuccessToast, showToast } from 'vant';
import { closeDB, batchDeleteDB, openDB } from '@/utils/indexDB';
const childRefs = ref<any[]>([]);
// 列表数据项
const state = reactive({
active: 0,
tabsList: [
{
title: '视频',
name: 0,
comments: markRaw(MovieList),
scrollTop: 0,
storeName: HistoryVideoType.VIDEO,
},
{
title: '抖音',
name: 1,
comments: markRaw(ShortnessVideoList),
scrollTop: 0,
storeName: HistoryVideoType.LACKVIDEO,
},
{
title: '短剧',
name: 'drama',
comments: markRaw(MineDramaList),
scrollTop: 0,
mode: 'history',
},
{
title: '帖子',
name: 2,
comments: markRaw(PostList),
scrollTop: 0,
storeName: HistoryVideoType.POST,
},
{
title: '动漫',
name: 4,
comments: markRaw(MangaList),
scrollTop: 0,
mediaType: 'video',
dbType: 'acgVideo',
storeName: HistoryVideoType.AcgVideo,
},
{
title: '漫画',
name: 3,
comments: markRaw(MangaList),
scrollTop: 0,
mediaType: 'image',
dbType: 'acgImage',
storeName: HistoryVideoType.AcgImage,
},
{
title: '图集',
name: 5,
comments: markRaw(TheatresList),
scrollTop: 0,
storeName: HistoryVideoType.PIC,
},
{
title: '小说',
name: 6,
comments: markRaw(MangaList),
scrollTop: 0,
mediaType: 'text',
dbType: 'acgText',
storeName: HistoryVideoType.AcgText,
},
],
isEdit: false,
isAllChecked: false,
deleteIds: [],
});
provide(
'isEdit',
computed(() => state.isEdit),
);
provide('editMode', 'delete');
// 编辑
const handleEdit = () => {
state.isEdit = !state.isEdit;
};
const tabChange = () => {
state.isEdit = false;
setScrollTop(state.tabsList[state.active].scrollTop);
};
const onScroll = (event: any) => {
state.tabsList[state.active].scrollTop = event.target.scrollTop;
};
onActivated(() => {
setScrollTop(state.tabsList[state.active].scrollTop);
// keep-alive 再次进入时重拉当前 tab,避免刚看过的记录仍是旧列表
const child = childRefs.value[state.active];
child?.reset?.();
});
// 设置top值
const setScrollTop = (top: number) => {
const dom = document.getElementById(`tabContent${state.active}`);
if (dom) {
dom.scrollTop = top;
}
};
const handleAllChecked = () => {
state.isAllChecked = !state.isAllChecked;
};
// 删除
const handleDelete = async () => {
console.log('删除');
let db = await openDB('lld_db');
let isOk = await batchDeleteDB(db, 'img', state.deleteIds);
closeDB(db);
if (isOk) {
showToast('删除成功');
return;
}
showToast('删除失败');
};
</script>
<style scoped lang="scss">
.history-record {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
:deep() {
.van-tabs {
flex: 1 0;
height: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
}
.van-tabs__line {
bottom: 21px;
width: 13px;
height: 3px;
border-radius: 12px;
background: linear-gradient(90deg, rgba(255, 192, 0, 0) 0%, #ffc000 100%);
}
.van-tabs__content {
flex: 1 0;
height: 0;
overflow: hidden;
}
.van-tab__panel {
height: 100%;
width: 100%;
overflow: hidden;
}
.tab-content-auto {
height: 100%;
width: 100%;
}
}
.edit {
font-size: 12px;
line-height: 18px;
color: rgba(255, 255, 255, 0.5);
&.active {
color: #ffc000;
}
}
.delete-box {
position: fixed;
bottom: calc(14px + env(safe-area-inset-bottom));
left: 50%;
transform: translateX(-50%);
width: 343px;
height: 62px;
background: rgba(48, 48, 48, 1);
display: flex;
justify-content: space-between;
align-items: center;
align-items: center;
padding: 0 24px;
border-radius: 3px;
.delete-box-item {
font-size: 12px;
color: #757575;
flex: 1;
height: 20px;
line-height: 20px;
text-align: center;
width: 110px;
font-size: 16px;
&:nth-of-type(1) {
border-right: 1px solid rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.6);
}
&:nth-of-type(2) {
color: #fff;
background: rgba(245, 44, 86, 1);
line-height: 38px;
height: 38px;
border-radius: 3px;
}
}
}
}
</style>
+708
View File
@@ -0,0 +1,708 @@
<template>
<div class="mainboxs">
<div class="minePage overflow-y-auto">
<lazyImg
v-if="configStore.appConfig.personalCenterBackground"
:src="configStore.appConfig.personalCenterBackground"
class="mine_page_bg"
/>
<img v-else class="mine_page_bg" src="@/assets/theme/default/mine_bg.png" />
<img class="task_sign" src="@/assets/images/png/mine/task_sign.png" @click="jumpPage('/task_sign_day')" />
<van-pull-refresh v-model="state.loading" @refresh="onRefresh" success-text="刷新成功">
<div class="user">
<div class="avatar" @click="jumpPage('/setting')">
<lazyImg :src="state.userInfo.portrait" round :key="state.userInfo.portrait" />
</div>
<div class="info">
<div class="row">
<div class="name">
<span>{{ state.userInfo.name }}</span>
<VipImg :isVip="state.userInfo.vipLevel" />
</div>
</div>
<div class="id">
<span>ID:{{ state.userInfo.uid }}</span>
<SvgIcon class="id-copy" name="id-copy" size="14px" color="#D0D0D0" @click.stop="copyUid" />
</div>
</div>
<div class="options">
<div class="message-entry" @click="jumpPage('/messageCenter')">
<img src="@/assets/images/png/mine/msg.png" alt="" />
<div v-if="showMessageRedDot" class="newsTip" />
</div>
<div class="right">
<img @click="jumpPage('/setting')" src="@/assets/images/png/mine/setting.png" alt="" />
</div>
</div>
</div>
<div class="num-nav">
<div class="num-nav-item">
<div class="num">0</div>
<div class="text">剩余缓存次数</div>
</div>
<div class="num-nav-divider" />
<div class="num-nav-item">
<div class="num">
{{ (globalStore?.advanceStatus?.privilegeLimit?.remain?.todayAiUndressCount || 0) + state.freeCount }}
</div>
<div class="text">剩余AI脱衣次数</div>
</div>
</div>
<div class="padding">
<MeTabPaymentBanner v-if="isMeTabPaymentActive" />
<div v-else class="vip_box" @click="jumpPage('/member')">
<img src="@/assets/images/png/mine/vip_icon.png" alt="" class="vip_icon" />
<div class="vip_type">
<template v-if="state.userInfo.isVip">
<div class="type_title">{{ state.userInfo.vipName }}会员</div>
<div class="type_info">到期时间:{{ moment(state.userInfo.vipExpireDate).format('YYYY.MM.DD') }}</div>
</template>
<template v-else>
<div class="type_title">{{ newUserTimeStr ? '新用升级会员特惠' : '开通会员 享专属特权' }}</div>
<div class="type_time" v-if="newUserTimeStr">
<img class="clock" src="@/assets/images/png/mine/figma/vip-clock.svg" alt="" />
{{ newUserTimeStr }}
</div>
<div class="type_info" v-else>解锁专属 畅享无限</div>
</template>
</div>
<div class="vip_btn">{{ state.userInfo.isVip ? '会员中心' : '开通会员' }}</div>
</div>
<div class="recharge">
<div class="item" @click="jumpPage('/member')" />
<div class="item" @click="jumpPage('/wallet')">
<p>余额:{{ Math.floor(((userStore.walletInfo.amount || 0) * 100 + (userStore.walletInfo.income || 0) * 100) / 100) }}</p>
</div>
<div class="item" @click.stop="onAiGirlFriendClick" />
</div>
<div class="features">
<div class="item" @click="jumpPage('/historyRecord')">
<img src="@/assets/images/png/mine/figma/mine-feat-history.svg" alt="" />
<span>历史记录</span>
</div>
<div class="item" @click="jumpPage('/purchasedVideo')">
<img src="@/assets/images/png/mine/figma/mine-feat-purchase.svg" alt="" />
<span>我的购买</span>
</div>
<div class="item" @click="jumpPage('/collectVideo')">
<img src="@/assets/images/png/mine/figma/mine-feat-collect.svg" alt="" />
<span>我的收藏</span>
</div>
<div class="item" @click="jumpPage('/myLike')">
<img src="@/assets/images/png/mine/figma/mine-feat-like.svg" alt="" />
<span>我的喜欢</span>
</div>
</div>
</div>
<HomeHeader :advType="AdType.MINE_BLOCK_ADV" class="padding" />
<div class="padding">
<div class="cell_box">
<div class="cell_title">常用功能</div>
<div class="cell">
<div class="cell_item" v-for="(item, index) in cellList1" :key="index" @click="jumpPage(item.path)">
<img :src="item.image" alt="" class="item_iamge" />
<div class="item_name">{{ item.title }}</div>
<div v-if="showMessageRedDot && item.title == '我的消息'" class="newsTip" />
</div>
</div>
</div>
</div>
<div class="share-code">我的邀请码 {{ state.userInfo.promotionCode }}</div>
<div class="official-address">永久官方地址 {{ configStore.sourceInfo.GUIDE }}</div>
</van-pull-refresh>
<!-- <div class="lottery" v-if="configStore?.appConfig?.luckyDrawH5" @click="$router.push('lotteryPage')">
<lazyImg
class="lotteryIcon"
v-if="configStore?.appConfig?.luckyDrawIcon"
:src="configStore?.appConfig?.luckyDrawIcon"
:key="configStore?.appConfig?.luckyDrawIcon"
/>
<div class="lotteryImg" v-else></div>
</div> -->
<!-- <AiFloat :isShowClose="false" routeName="mine"></AiFloat> -->
<!-- <AccountCredentialsPopup :is-open="isShowAccountCredentialsPopup" @close="isShowAccountCredentialsPopup = false" /> -->
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'mine',
};
</script>
<script lang="ts" setup>
import { toRefs, reactive, onActivated, computed, ref } from 'vue';
// import storeMine from '@/components/originStroe/storeMine.vue';
import { useUserStore } from '@/stores/user';
import UserApi from '@/service/user';
import { showToast } from 'vant';
import { useRouter } from 'vue-router';
import { getAdItem } from '@/utils/getConfig';
import { AdType } from '@/enums/app';
import { newUserTimeStr, queryTiroCountdown } from '@/hooks/newUserCountdown';
import { useConfigStore } from '@/stores/config';
import { useGlobalStore } from '@/stores/global';
// import AiFloat from '@/components/AiFloat/index.vue';
import MineApi from '@/service/mine';
import moment from 'moment';
import mineCell from './mineCell';
import { isActivePaymentStatus } from '@/utils/paymentStatusPopup';
import MeTabPaymentBanner from '@/components/MeTabPaymentBanner/index.vue';
import toClipboard from '@/utils/clipboard';
const router = useRouter();
const globalStore = useGlobalStore();
const configStore = useConfigStore();
const userStore = useUserStore();
const isMeTabPaymentActive = computed(() => {
if (!isActivePaymentStatus(configStore.appConfig?.paymentStatusPopup)) return false;
return !!configStore.appConfig?.paymentStatusPopupConfig?.meTab;
});
const { cellList1 } = mineCell();
queryTiroCountdown();
const state = reactive({
userInfo: <any>{},
isVip: false,
code: '',
exchangeCodeShow: false,
loading: false,
advsList: <Array<Types.App.AdsInfoListItem>>[],
percentage: 0, // 打卡进度
totalDays: 0, // 需要打卡天数
currentSignDays: 0, // 连续打卡天数
forgetSignDays: 0, // 忘记打卡天数
freeCount: 0, // 免费次数
totalAiUndressFreeTimes: 0, // 总免费次数
});
/** keep-alive 首进只走 onActivated,避免与 setup 内 init 双请求 */
onActivated(() => {
userStore.getCheckMessageTip();
// 如果有预售活动则调用
if (configStore.appConfig.advanceStatus && configStore.appConfig.advanceStatus.activityStatus) {
getAdvanceStatus();
globalStore.setPresaleActivityDetail();
}
getInfo();
state.userInfo = userStore.userInfo;
userStore.updateWallet();
state.advsList = getAdItem(AdType.MINE);
queryFreeCount();
});
/**
* 获取用户信息
* @param uid
*/
const getInfo = async () => {
try {
let res: any = await UserApi.queryUserInfo(null);
const merged = { ...userStore.userInfo, ...res };
state.userInfo = merged;
await userStore.saveUserInfo(merged);
state.totalDays = state.userInfo?.whoringCard?.detailInfo[0]?.totalDays || 0;
state.currentSignDays = state.userInfo?.whoringCard?.detailInfo[0]?.currentSignDays || 0;
state.forgetSignDays = state.userInfo?.whoringCard?.detailInfo[0]?.forgetSignDays || 0;
state.percentage = (state.currentSignDays / state.totalDays) * 100;
state.loading = false;
} catch (e) {
console.log(e);
}
};
// 获取预售状态
const getAdvanceStatus = async () => {
try {
let res: any = await MineApi.advanceStatus();
globalStore.setAdvanceStatus(res);
} catch (e) {
console.log(e);
}
};
const jumpPage = (routePath: string) => {
if (routePath === '__lock__') {
showToast('功能升级中');
return;
}
if (routePath === '/member' && state.userInfo.isUpgrade) {
router.push({
path: routePath,
query: {
autoSelectUpgrade: '1',
},
});
return;
}
router.push(routePath);
};
const copyUid = async () => {
try {
if (!state.userInfo?.uid) {
showToast('复制失败');
return;
}
await toClipboard(String(state.userInfo.uid));
showToast('复制成功');
} catch (e) {
console.log(e);
}
};
const isAiGirlFriendLoading = ref(false);
const onAiGirlFriendClick = async () => {
if (isAiGirlFriendLoading.value) return;
isAiGirlFriendLoading.value = true;
try {
const res = await MineApi.getAimatev2Url();
if (res?.url) {
router.push({ path: '/aiGirlfriend/web', query: { authUrl: res.url } });
} else {
showToast('功能升级中');
}
} catch {
showToast('功能升级中');
} finally {
isAiGirlFriendLoading.value = false;
}
};
/**
* 下拉刷新
*/
const onRefresh = () => {
state.loading = true;
getInfo();
userStore.updateWallet();
userStore.getCheckMessageTip();
// 如果有预售活动则调用
if (configStore.appConfig.advanceStatus && configStore.appConfig.advanceStatus.activityStatus) {
getAdvanceStatus();
globalStore.setPresaleActivityDetail();
}
};
const { isNewsTip } = toRefs(useUserStore());
/** 我的消息红点:业务未读 */
const showMessageRedDot = computed(() => Boolean(isNewsTip.value));
const queryFreeCount = () => {
UserApi.queryWallet().then((res: any) => {
state.freeCount = res.aiUndressFreeTimes || 0;
state.totalAiUndressFreeTimes = res.totalAiUndressFreeTimes || 0;
});
};
</script>
<style lang="scss" scoped>
.mainboxs {
width: 100%;
height: 100%;
background: #000;
}
.default_bg {
background: url('@/assets/theme/default/mine_bg.png') no-repeat;
background-size: 100% 812px;
}
.minePage {
padding-bottom: 50px;
box-sizing: border-box;
width: 100%;
height: 100%;
background: #000;
.mine_page_bg {
width: 100%;
height: 255px;
position: absolute;
top: 0;
left: 0;
}
.task_sign {
width: 64px;
height: 35px;
position: absolute;
top: 70px;
right: 0;
z-index: 10;
}
.user {
padding: 56px 16px 12px 16px;
box-sizing: border-box;
width: 100%;
display: flex;
align-items: center;
position: relative;
.avatar {
width: 60px;
height: 60px;
border-radius: 50%;
flex-shrink: 0;
}
.info {
display: flex;
flex-direction: column;
justify-content: center;
flex: 1;
padding-left: 17px;
.row {
display: flex;
justify-content: space-between;
.name {
display: flex;
align-items: center;
margin-bottom: 6px;
span {
font-size: 16px;
font-weight: 500;
line-height: 22px;
margin-right: 10px;
}
img {
width: 24px;
height: 24px;
margin-left: 6px;
}
}
}
.id {
display: flex;
align-items: center;
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
line-height: 18px;
.id-copy {
width: 14px;
height: 14px;
margin-left: 8px;
flex-shrink: 0;
}
}
}
.options {
position: absolute;
right: 13px;
top: 12px;
left: 12px;
display: flex;
justify-content: space-between;
align-items: center;
height: 44px;
.message-entry {
position: relative;
display: inline-flex;
width: 24px;
height: 24px;
img {
width: 100%;
height: 100%;
}
.newsTip {
top: -2px;
right: -2px;
}
}
.right {
img {
width: 24px;
display: inline-block;
margin-left: 12px;
}
}
}
}
.padding {
padding: 0 16px;
box-sizing: border-box;
}
.vip_box {
display: flex;
align-items: center;
justify-content: space-between;
height: 68px;
background: url('@/assets/images/png/mine/vip_bg.png') no-repeat;
background-size: 100% 100%;
padding: 0 12px 0 9px;
position: relative;
overflow: hidden;
border-radius: 5px;
.vip_icon {
width: 44px;
height: 44px;
margin-right: 8px;
flex-shrink: 0;
}
.vip_type {
flex: 1;
min-width: 0;
.type_title {
background: linear-gradient(90deg, #fae6d2 0%, #ffffff 66.79%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-size: 16px;
font-weight: 700;
line-height: 22px;
margin-bottom: 4px;
}
.type_time {
font-size: 12px;
color: #e9e8e7;
line-height: 18px;
display: flex;
align-items: center;
.clock {
width: 9px;
height: 9px;
margin-right: 3px;
}
}
.type_info {
font-size: 10px;
color: #e9e8e7;
line-height: 16px;
}
}
.vip_btn {
flex-shrink: 0;
height: 26px;
padding: 0 10px;
border-radius: 999px;
background: linear-gradient(89.13deg, #fbe9bc 0.26%, #ffffff 50%, #f4e1bd 93.97%);
color: #000;
font-size: 10px;
font-weight: 600;
line-height: 26px;
text-align: center;
white-space: nowrap;
}
}
.num-nav {
display: flex;
justify-content: center;
align-items: center;
gap: 50px;
margin: 9px 16px 21px;
.num-nav-item {
display: flex;
flex-direction: column;
align-items: center;
width: 95px;
.num {
font-size: 16px;
font-weight: 500;
line-height: 22px;
margin-bottom: 4px;
color: #fff;
}
.text {
font-size: 12px;
line-height: 18px;
color: rgba(255, 255, 255, 0.7);
}
}
.num-nav-divider {
width: 1px;
height: 12px;
background: rgba(255, 255, 255, 0.2);
}
}
.recharge {
box-sizing: border-box;
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
.item {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
font-size: 14px;
border-radius: 10px;
p {
width: 100%;
font-size: 10px;
color: #ffffff8c;
padding-left: 15px;
margin-top: 20px;
box-sizing: border-box;
}
&:nth-of-type(1) {
p {
padding-left: 10px;
}
}
span {
font-size: 12px;
padding: 8px 0 0 0;
box-sizing: border-box;
}
}
.item:nth-child(1) {
width: 116px;
height: 60px;
background: url('@/assets/images/png/mine/tuig_bg.png') no-repeat;
background-size: 100% 100%;
}
.item:nth-child(2) {
width: 116px;
height: 60px;
background: url('@/assets/images/png/mine/coin_bg.png') no-repeat;
background-size: 100% 100%;
}
.item:nth-child(3) {
width: 116px;
height: 60px;
background: url('@/assets/images/png/mine/ai_nvyou_bg.png') no-repeat;
background-size: 100% 100%;
}
}
.features {
width: 100%;
background: rgba(255, 255, 255, 0.04);
height: auto;
border-radius: 6px;
padding: 18px 20px;
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
box-sizing: border-box;
.item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
img {
width: 24px;
height: 24px;
}
span {
font-size: 12px;
color: #ffffff;
line-height: 18px;
padding-top: 0;
}
}
}
.swiperBox {
margin-top: 10px;
border-radius: 10px;
overflow: hidden;
}
.cell_box {
margin-top: 12px;
padding: 14px 0 16px;
background: rgba(255, 255, 255, 0.05);
border-radius: 5px;
.cell_title {
font-size: 16px;
font-weight: 500;
line-height: 22px;
padding: 0 16px;
color: #efefef;
}
.cell {
margin-top: 10px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-row-gap: 16px;
grid-column-gap: 8px;
.cell_item {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
.item_iamge {
width: 24px;
height: 24px;
margin-bottom: 3px;
}
.item_name {
font-size: 12px;
line-height: 18px;
color: #fff;
}
.newsTip {
top: 0;
right: 18px;
}
}
}
}
.signIn {
margin: 16px 0;
width: 100%;
height: 100%;
font-size: 13.7px;
font-weight: 500;
.signIn_top {
padding-bottom: 8px;
.signIn_left {
> span {
color: rgba(255, 255, 255, 0.5);
}
}
.signIn_right {
color: rgba(255, 255, 255, 0.5);
}
.ckeckBtn {
color: #fff;
opacity: 0.6;
}
}
.signIn_bottom {
width: 100%;
height: 15px;
.progress {
width: 100%;
height: 10px;
background: rgba(255, 255, 255, 0.3) !important;
}
}
}
}
.lottery {
height: 50px;
width: 50px;
position: fixed;
bottom: 172px;
right: 10px;
z-index: 999;
.lotteryIcon {
height: 100%;
width: 100%;
}
.lotteryImg {
height: 100%;
width: 100%;
background: url('@/assets/gif/lottery.gif') no-repeat;
background-size: 100% 100%;
}
}
.newsTip {
position: absolute;
top: 0;
right: 14px;
width: 8px;
height: 8px;
border-radius: 50%;
background: #fd0563;
}
.share-code {
font-size: 16px;
color: rgba(189, 189, 189, 1);
font-weight: 400;
text-align: center;
margin-top: 16px;
}
.official-address {
font-size: 12px;
color: rgba(82, 82, 82, 1);
font-weight: 400;
text-align: center;
margin-top: 12px;
}
</style>
+98
View File
@@ -0,0 +1,98 @@
<template>
<div class="login">
<Header title="" />
<div class="content">
<div class="title">
欢迎来到萝莉岛社区
<img src="../../../assets/images/png/business1.png" alt="" />
</div>
<van-field v-model="state.value" :border="false" placeholder="请输入账号(最少6位)或手机号" />
<van-field v-model="state.value" :border="false" placeholder="请输入登录密码" />
<div class="btnbox">
<div class="flex-center-center">注册</div>
<div class="flex-center-center">登录</div>
</div>
<h2>提示</h2>
<p>1.</p>
<p>2.</p>
<p>3.</p>
</div>
</div>
</template>
<script lang="ts" setup>
import { nextTick, reactive } from 'vue';
import { showToast } from 'vant';
import UserApi from '@/service/user';
import { useUserStore } from '@/stores/user';
import { useRouter } from 'vue-router';
const state = reactive({
userInfo: <Types.User.UserInfoType>{},
value: <string>'',
});
</script>
<style lang="scss" scoped>
.login {
.content {
padding: 50px 30px 0 30px;
box-sizing: border-box;
}
.title {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
font-weight: bold;
font-size: 16px;
img {
width: 50px;
height: 50px;
margin: 20px 0 0 0;
}
}
.btnbox {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
margin: 50px 0 30px 0;
div {
width: 140px;
height: 40px;
border-radius: 10px;
background: #727272;
}
}
h2 {
margin-bottom: 20px;
}
p {
font-size: 14px;
margin-bottom: 5px;
}
:deep(.van-cell) {
margin: 30px 0 0 0;
}
:deep(.van-cell) {
border-radius: 5px;
font-size: 16px;
background: #727272;
height: 42px;
display: flex;
align-items: center;
}
:deep(.van-field__control) {
color: #fff;
}
::placeholder {
// color: ;
}
}
</style>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+95
View File
@@ -0,0 +1,95 @@
<template>
<Loading v-if="isLoading" class="member-ab-loading" />
<MemberNewPanel v-else-if="isVariantB" :vip-product="vipProduct" />
<MemberLegacyPanel v-else :vip-product="vipProduct" />
<PaymentGuidePopup
:show="paymentGuideVisible"
:guide="paymentGuide"
:scene="paymentGuideScene"
@close="closePaymentGuide"
/>
</template>
<script lang="ts">
export default {
name: 'member',
};
</script>
<script lang="ts" setup>
import { computed, defineAsyncComponent, onActivated, onDeactivated, onMounted, onUnmounted, ref } from 'vue';
import PaymentGuidePopup from '@/components/PaymentGuidePopup/index.vue';
import { PaymentGuideScene } from '@/enums/home';
import { usePaymentGuide } from '@/hooks/usePaymentGuide';
import { turnOnPaymentGuideLocalSwitch } from '@/utils/paymentGuideLocalSwitch';
import { emptyVipProduct, fetchVipProductCached, hasWarmVipProductCache } from '@/utils/vipProductCache';
const MemberLegacyPanel = defineAsyncComponent(() => import('./components/MemberLegacyPanel.vue'));
const MemberNewPanel = defineAsyncComponent(() => import('./components/MemberNewPanel.vue'));
const isLoading = ref(true);
const vipProduct = ref<Types.Recharge.VipProductResponse>(emptyVipProduct());
/** VIP 套餐 A/B:A=原版面板,B=新版面板;未下发时按 A */
const isVariantB = computed(() => String(vipProduct.value.variant || '').toUpperCase() === 'B');
const {
visible: paymentGuideVisible,
guide: paymentGuide,
scene: paymentGuideScene,
closeGuide: closePaymentGuide,
tryShowGuide,
} = usePaymentGuide();
let memberPageActive = false;
/** 防止 keep-alive 首进 onMounted + onActivated 连打两次 */
let enterSeq = 0;
const tryShowVipCenterGuide = async () => {
const seq = ++enterSeq;
turnOnPaymentGuideLocalSwitch(PaymentGuideScene.VIP_CENTER);
// 等面板开始渲染后再弹,避免遮罩盖住空白 loading
if (isLoading.value) return;
if (seq !== enterSeq || !memberPageActive) return;
await tryShowGuide(PaymentGuideScene.VIP_CENTER, undefined, () => memberPageActive);
};
const loadVipProduct = async () => {
if (!hasWarmVipProductCache()) isLoading.value = true;
try {
vipProduct.value = await fetchVipProductCached();
} catch (e) {
console.log(e);
vipProduct.value = emptyVipProduct();
} finally {
isLoading.value = false;
if (memberPageActive) {
void tryShowVipCenterGuide();
}
}
};
const leaveMemberPage = () => {
memberPageActive = false;
closePaymentGuide();
};
onMounted(() => {
memberPageActive = true;
void loadVipProduct();
});
onActivated(() => {
memberPageActive = true;
// keep-alive 重进:数据可能已就绪,直接尝试弹窗
if (!isLoading.value) {
void tryShowVipCenterGuide();
}
});
onDeactivated(leaveMemberPage);
onUnmounted(leaveMemberPage);
</script>
<style lang="scss" scoped>
.member-ab-loading {
min-height: 60vh;
}
</style>
+390
View File
@@ -0,0 +1,390 @@
<template>
<div class="memberExchange">
<Header title="会员兑换" />
<div class="main overflow-y-auto" v-if="!state.loading">
<div class="currentIntegralBox flex-center-center">
<div class="title">当前积分</div>
<div class="number">{{ state.userWallet.integral || 0 }}</div>
</div>
<div class="integralCardArea overflow-x-auto" @touchstart.stop @touchmove.stop @touchend.stop>
<div class="integralCardBox" v-for="item in state.integralListVip" :key="item.id">
<div class="integralCard">
<div class="vipIcon"></div>
<div class="integralDesc">{{ item.price }}积分</div>
<div class="integralTitle">{{ item.name }}</div>
</div>
<div class="exchangeBtn flex-center-center" @click="exchange(item)">兑换</div>
</div>
</div>
<div class="integralList">
<div class="itemlist" v-for="item in state.integralList" :key="item.id">
<div class="top">
<!-- <p>{{ item.name }}</p>
<span>{{ item.desc }}</span> -->
<LazyImg :src="item.img" class="itemlistimg" :key="item.img" />
</div>
<div class="bot flex-center-between">
花费{{ item.price }}积分
<div class="btn flex-center-center" @click="exchange(item)">立即兑换</div>
</div>
</div>
</div>
<Empty v-if="state.integralList.length == 0" />
<!-- <div class="exchangeRecordMain">
<div class="titleBox">兑换记录</div>
<div class="exchangeRecordBox overflow-y-auto">
<div class="exchangeRecord flex-center-between" v-for="item in state.integralRecordList" :key="item.id">
<div class="exchangeInfo">
{{ item.desc }}
</div>
<div class="exchangeTime">{{ dateFormat(item.createdAt) }}</div>
</div>
<div class="nodata" v-if="!state.integralRecordList.length">您还没有兑换过会员喔~</div>
</div>
</div> -->
<div class="recordTitle">兑换记录</div>
<div class="record">
<div class="th">
<div class="left">兑换方式</div>
<div class="center">兑换类型</div>
<div class="right">兑换时间</div>
</div>
<div v-if="state.integralRecordList.length">
<div class="td" v-for="(item, index) in state.integralRecordList" :key="index">
<div class="left">{{ item.name }}</div>
<div class="center">{{ item.desc }}</div>
<div class="right">{{ moment(item.createdAt).format('YYYY.MM.DD') }}</div>
</div>
</div>
</div>
</div>
<div class="main" v-else>
<Loading />
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import { useUserStore } from '@/stores/user';
import { showToast } from 'vant';
import MineApi from '@/service/mine';
import UserApi from '@/service/user';
import HomeApi from '@/service/home';
import moment from 'moment';
const userStore = useUserStore();
const state = reactive({
integralListVip: <Array<Types.Recharge.IntegralList>>[], // 积分列表
integralList: <Array<Types.Recharge.IntegralList>>[], // 积分列表
loading: true,
userWallet: <Types.User.WalletInfo>{},
integralRecordList: <any>[],
});
// 查询会员卡列表
const getVipList = async () => {
const res: any = await MineApi.queryVipProduct();
const list = res.integralList || [];
state.loading = false;
state.integralListVip = list.filter((i: any) => i.type === 1) || [];
state.integralList = list;
};
// 查询积分兑换记录
const getIntegralRecordList = async () => {
try {
let req: any = await HomeApi.getIntegralRecord();
state.integralRecordList = req || [];
} catch (e) {
console.log(e);
}
};
const exchange = async (item: Types.Recharge.IntegralList) => {
if (item.price > state.userWallet.integral) return showToast('您当前积分不足');
try {
let req = {
id: item.id,
};
await UserApi.exchangeIntegral(req);
state.userWallet.integral -= item.price;
getIntegralRecordList();
showToast('兑换成功');
} catch (e) {
console.log(e);
}
};
const init = () => {
state.userWallet = userStore.walletInfo;
getVipList();
getIntegralRecordList();
};
init();
</script>
<style lang="scss" scoped>
.memberExchange {
height: 100%;
:deep(.header) {
background: transparent;
}
.main {
height: calc(100% - 44px);
padding: 24px 16px 60px;
.currentIntegralBox {
height: 101px;
width: 343px;
background: #ffffff0d;
border: 1px solid #f68804;
border-radius: 8px;
flex-direction: column;
// &::before {
// content: '';
// position: absolute;
// top: 10px;
// left: 0;
// width: 9px;
// height: 25px;
// background: rgba(219, 54, 31, 1);
// border-radius: 0 6px 6px 0;
// }
.title {
color: #fff;
font-size: 16px;
// text-align: left;
// position: absolute;
// left: 16px;
// top: 10px;
font-weight: 500;
margin-bottom: 15px;
}
.number {
color: #f68804;
font-size: 32px;
font-weight: 600;
// margin-top: 9px;
}
}
.integralCardArea {
margin: 20px 0;
display: flex;
.integralCardBox {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
margin-left: 15px;
&:last-child {
// margin-right: 0;
}
.integralCard {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 120px;
width: 95px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
color: #421a11;
text-align: center;
.vipIcon {
height: 28px;
width: 28px;
background: url('@/assets/images/png/mine/vip1.png') no-repeat;
background-size: 100% 100%;
margin-top: 5px;
}
.integralTitle {
// color: rgba(153, 153, 153, 1);
background: rgba(255, 213, 168, 1);
border-radius: 2px;
font-size: 12px;
font-weight: 400;
margin-bottom: 10px;
height: 20px;
line-height: 20px;
min-width: 61px;
padding: 0 4px;
box-sizing: border-box;
color: rgba(105, 42, 31, 1);
}
.integralDesc {
// color: rgba(249, 193, 66, 1);
color: rgba(255, 255, 255, 0.9);
font-size: 16px;
font-weight: 400;
margin: 15px 0;
// padding: 3px 6px;
// border-radius: 2px;
// background-color: #f3e2d6;
}
}
.exchangeBtn {
height: 20px;
line-height: 20px;
width: 60px;
color: white;
background: #f68804;
border-radius: 2px;
margin-top: 8px;
font-size: 12px;
}
}
}
.integralList {
box-sizing: border-box;
display: grid;
grid-template-columns: 1fr 1fr;
grid-row-gap: 7px;
grid-column-gap: 9px;
.itemlist {
height: 134px;
background: #303030;
border-radius: 12px;
overflow: hidden;
padding: 10px;
box-sizing: border-box;
.top {
height: 76px;
padding: 15px 12px;
box-sizing: border-box;
position: relative;
p {
font-size: 16px;
margin-bottom: 5px;
position: relative;
z-index: 10;
color: #fff;
}
span {
font-size: 10px;
color: #cccccc;
position: relative;
z-index: 10;
}
.itemlistimg {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
:deep(.animationImg) {
height: auto;
}
}
}
}
.bot {
height: 36px;
box-sizing: border-box;
font-size: 12px;
margin-top: 4px;
color: #989898;
.btn {
font-size: 10px;
width: 56px;
height: 24px;
line-height: 24px;
background: #f68804;
color: #000;
border-radius: 20px;
}
}
}
.exchangeRecordMain {
height: 253px;
// width: 311px;
// background: rgba(255, 200.76, 170.24, 0.28);
// border-radius: 6px;
// border: 1px #ffc9aa solid;
margin-top: 32px;
padding: 0 22px 0 16px;
.titleBox {
// 兑换记录
color: rgba(255, 255, 255, 1);
font-size: 18px;
border-radius: 2px;
}
.exchangeRecordBox {
height: 220px;
.exchangeRecord {
border-bottom: 0.5px rgba(139, 50, 0, 0.3) solid;
padding: 10px 0;
.exchangeInfo {
color: $themeColor;
font-size: 13px;
}
.exchangeTime {
font-size: 12px;
color: #999999;
}
}
.nodata {
color: #8b3200;
font-size: 13px;
margin-top: 100px;
text-align: center;
}
}
}
.recordTitle {
font-size: 18px;
color: #fff;
margin: 10px 0;
font-weight: 600;
margin-top: 30px;
}
.record {
// margin-top: 40px;
// padding: 0 10px;
.th {
display: flex;
justify-content: space-between;
align-items: center;
// background: #f5f5f5;
font-size: 14px;
width: 100%;
height: 42px;
gap: 4px;
& > div {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background: rgba(255, 255, 255, 0.05);
flex: 1;
color: #fff;
font-weight: 400;
}
// .left {
// width: 28%;
// }
// .center {
// width: 34%;
// }
// .right {
// width: 38%;
// }
}
.td {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
width: 100%;
height: 42px;
color: rgba(153, 153, 153, 1);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
& > div {
width: 33.33%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
border-bottom: solid 1px rgba(0, 0, 0, 0.05);
&:first-child {
color: #fff;
}
}
}
}
}
}
</style>
@@ -0,0 +1,64 @@
<template>
<div class="announDetails">
<Header title="消息详情" />
<div class="content overflow-y-auto">
<div class="announTitle">{{state.announData.title}}</div>
<div class="cover">
<lazyImg
:src="state.announData.cover"
:key="state.announData.cover"
/>
</div>
<div class="content_text">
{{ state.announData.content }}
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive,nextTick } from 'vue';
import { useRoute } from "vue-router"
const route = useRoute()
import { getSessionItem } from '@/utils/longStorage';
const state = reactive({
announData:<any>{},
});
state.announData = JSON.parse(getSessionItem('announData'))
</script>
<style lang="scss" scoped>
.announDetails {
height: 100%;
.content{
width: 100%;
height: calc(100vh - 44px);
padding: 16px;
.announTitle {
font-size: 18px;
font-weight: 600;
margin-bottom: 10px;
}
.cover {
height: 208px;
border-radius: 8px;
overflow: hidden;
}
.content_text {
padding-top: 12px;
font-size: 16px;
line-height: 20px;
padding-bottom: 20px;
white-space: pre-wrap;
}
> h2 {
font-size: 18px;
}
}
}
</style>
@@ -0,0 +1,91 @@
<template>
<div class="comment-wrap overflow-y-auto" id="messageCommentId">
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<UserCommentItem
v-for="item in state.list"
:key="item.id"
:item="item"
routePath="postDetail"
variant="list"
></UserCommentItem>
</PullRefresh>
</div>
</template>
<script lang="ts">
export default {
name: 'comment',
};
</script>
<script lang="ts" setup>
import { onMounted, reactive } from 'vue';
import MessageApi from '@/service/message';
import TabPageScroll from '@/utils/resetTabPageScrollTop';
const tabPageScroll = new TabPageScroll();
const state = reactive({
loading: true,
list: <Array<any>>[],
});
const page = {
pageNumber: 1,
pageSize: 20,
};
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
const getList = async () => {
const { pageNumber } = page;
let req = {
pageNumber,
pageSize: page.pageSize,
msgType: 2,
};
try {
let res: any = await MessageApi.messageDyList(req);
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (pageNumber === 1) {
state.list = list;
} else {
state.list = state.list.concat(list);
}
moreParams.isNoData = state.list.length === 0;
if (!res.hasNext) {
moreParams.finished = true;
}
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
onMounted(() => {
getList();
tabPageScroll.add('#messageCommentId');
});
const onLoad = () => {
moreParams.loading = true;
page.pageNumber++;
getList();
};
const onRefresh = () => {
page.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
getList();
};
</script>
<style lang="scss" scoped>
.comment-wrap {
height: 100%;
}
</style>
@@ -0,0 +1,139 @@
<template>
<div class="contents overflow-y-auto">
<PullRefresh v-if="!state.loading" :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="listBox" v-for="(item, index) in state.list" :key="index">
<div class="title flex-center-between">
<p class="ellipsis">
#动态点赞
<!-- {{item.content}} -->
</p>
<span>{{ dateFormatYyyyMmDd(item.createdAt) }}</span>
</div>
<div class="mess ellipsis2">{{ item.content }}</div>
<div class="count">
<span>{{ item.likeCount || 0 }}个人为你点赞</span>
<!-- <LazyImg v-for="url in item.userGroup" :key="url.avatar" :src="url.avatar" round class="img" /> -->
</div>
</div>
</PullRefresh>
<Loading v-else />
</div>
</template>
<script lang="ts" setup>
import { reactive, nextTick } from 'vue';
import MessageApi from '@/service/message';
import { dateFormatYyyyMmDd } from '@/utils/index';
import { useRouter } from 'vue-router';
const router = useRouter();
const state = reactive({
loading: true,
pageNumber: 1,
pageSize: 10,
list: <any>[],
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: true,
});
const getList = async (type: any) => {
try {
let req = {
pageNumber: state.pageNumber,
pageSize: state.pageSize,
msgType: 1,
};
let res: any = await MessageApi.messageDyList(req);
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (type === 'refresh') {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1 && state.list.length == 0) {
moreParams.isNoData = true;
}
if (!res.hasNext) {
moreParams.finished = true;
}
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
nextTick(() => {
getList('');
});
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList(null);
};
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
getList('refresh');
};
</script>
<style lang="scss" scoped>
.contents {
height: 100%;
:deep(.my-refresh) {
padding: 0;
}
.listBox {
margin-bottom: 12px;
width: 100%;
padding: 12px;
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
.title {
width: 100%;
font-size: 16px;
p {
font-weight: 500;
}
span {
color: #ffffff;
font-size: 14px;
}
}
.mess {
font-size: 14px;
color: rgba(255, 255, 255, 0.45);
margin: 12px 0;
line-height: 20px;
}
.count {
width: 100%;
display: flex;
align-items: center;
font-size: 12px;
color: rgba(255, 255, 255, 0.35);
span {
padding-right: 10px;
}
.img {
width: 26px;
height: 26px;
margin-left: -5px;
}
}
}
}
</style>
@@ -0,0 +1,131 @@
<template>
<div class="contents">
<!-- <PullRefresh v-if="!state.loading" :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh"> -->
<!-- </PullRefresh> -->
<div v-if="!state.loading">
<div class="listBox" v-for="(item, index) in state.list" :key="index" @click="jumpResource(item)">
<div class="title">{{ item.title }}</div>
<div class="mess ellipsis3">{{ item.content }}</div>
<div class="time">{{ dateFormatYyyyMmDd(item.time) }}</div>
</div>
</div>
<Loading v-else />
</div>
</template>
<script lang="ts" setup>
import { reactive, nextTick } from 'vue';
import { useRouter } from 'vue-router';
import CommunityApi from '@/service/community';
import { dateFormatYyyyMmDd } from '@/utils/index';
import { useConfigStore } from '@/stores/config';
const configStore = useConfigStore();
const router = useRouter();
const state = reactive({
loading: true,
pageNumber: 1,
pageSize: 10,
list: <any>[],
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: true,
});
const getData = () => {
state.list = configStore.appConfig.announList;
state.loading = false;
};
const getList = async (type: any) => {
try {
let req = {
pageNumber: state.pageNumber,
pageSize: state.pageSize,
};
let res: any = await CommunityApi.getAnnouList(req);
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (type === 'refresh') {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1 && state.list.length == 0) {
moreParams.isNoData = true;
}
if (!res.hasNext) {
moreParams.finished = true;
}
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
nextTick(() => {
getData();
// getList("");
});
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList(null);
};
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
getList('refresh');
};
const jumpResource = (data: any) => {
sessionStorage.setItem('announData', JSON.stringify(data));
router.push('/announDetails');
};
</script>
<style lang="scss" scoped>
.contents {
width: 100%;
height: 100%;
.listBox {
background: rgba(255, 255, 255, 0.05);
padding: 11px 12px;
border-radius: 6px;
margin-bottom: 12px;
display: flex;
flex-direction: column;
gap: 10px;
.title {
font-size: 14px;
line-height: 20px;
font-weight: 500;
color: rgba(255, 255, 255, 0.5);
margin-bottom: 0;
}
.mess {
width: 100%;
font-size: 12px;
line-height: 18px;
color: rgba(255, 255, 255, 0.9);
text-align: justify;
}
.time {
font-size: 12px;
line-height: 18px;
color: rgba(255, 255, 255, 0.5);
}
}
}
</style>
+96
View File
@@ -0,0 +1,96 @@
<template>
<div class="messCenter">
<Header title="消息中心" />
<van-tabs
v-model:active="state.tabActive"
class="oepnX"
title-active-color="#FFFFFF"
title-inactive-color="rgba(255, 255, 255, 0.5)"
animated
:scrollspy="false"
:lazy-render="true"
>
<van-tab :title="item.label" v-for="(item, index) in state.tabList" :key="index">
<component :is="compArr[index]"></component>
</van-tab>
</van-tabs>
</div>
</template>
<script lang="ts">
export default {
name: 'messageCenter',
};
</script>
<script lang="ts" setup>
import { reactive, nextTick, onActivated } from 'vue';
import { useRoute } from 'vue-router';
import NoticeTab from './components/noticeTab.vue';
import ComComment from './components/comment.vue';
import { useUserStore } from '@/stores/user';
const route = useRoute();
const userStore = useUserStore();
const state = reactive({
tabList: [
{
label: '评论',
},
{
label: '公告',
},
],
tabActive: 0,
});
const compArr = [ComComment, NoticeTab];
onActivated(() => {
userStore.getCheckMessageTip();
});
nextTick(() => {
state.tabActive = Number(route.query.tabActive) || 0;
});
</script>
<style lang="scss" scoped>
@import '@/assets/styles/mixins.scss';
.messCenter {
overflow: hidden;
background: #000;
:deep(.van-tabs) {
.van-tabs__wrap {
height: 42px;
background: #000;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
.van-tabs__nav {
background: #000;
display: flex;
justify-content: center;
gap: 36px;
.van-tab {
font-size: 14px;
line-height: 20px;
width: auto;
flex: unset;
padding: 0;
&--active {
font-weight: 500;
}
}
.van-tabs__line {
height: 2px;
width: 9px;
background: #ffc000;
border-radius: 1px;
bottom: 4px;
}
}
}
.van-tab__panel {
padding: 12px 16px;
height: calc(100vh - 86px);
@include SafeArea('padding', 'bottom', 12px);
background: #000;
}
}
}
</style>
+39
View File
@@ -0,0 +1,39 @@
import { ref } from 'vue';
export default function mineCell() {
const cellList1 = ref([
{
title: '意见反馈',
image: new URL('@/assets/images/png/mine/my_cell2.png', import.meta.url).href,
path: '/problemFeedback',
},
{
title: '我的关注',
image: new URL('@/assets/images/png/mine/my_features4.png', import.meta.url).href,
path: '/follow',
},
{
title: '分享邀请',
image: new URL('@/assets/images/png/mine/my_cell5.png', import.meta.url).href,
path: '/share',
},
{
title: '应用推荐',
image: new URL('@/assets/images/png/mine/my_cell8.png', import.meta.url).href,
path: '/freeBenefits',
},
{
title: '账号凭证',
image: new URL('@/assets/images/png/mine/my_cell9.png', import.meta.url).href,
path: '/accountCertificate',
},
{
title: '我的消息',
image: new URL('@/assets/images/png/mine/my_cell10.png', import.meta.url).href,
path: '/messageCenter',
},
]);
return {
cellList1,
};
}
+590
View File
@@ -0,0 +1,590 @@
<template>
<div class="minepostpage">
<Header title="创作中心">
<template v-slot:right>
<!-- v-if="state.active === 2" -->
<div
v-if="state.active === 2"
class="right-btn"
:class="{ 'right-btn--done': state.isEdit }"
@click="editPost"
>
{{ !state.isEdit ? '编辑' : '完成' }}
</div>
</template>
</Header>
<div class="info">
<div class="info-item">
<div class="info-item-title">收益余额</div>
<div class="info-item-value">{{ (state.userWallet.income * 100) / 100 || 0 }}</div>
<div class="btnbox">
<div class="btn flex-center-center" @click="$router.push({ path: '/withdraw' })">立即提现</div>
<!-- <div class="btn flex-center-center" @click="$router.push('/revenueDetails')">收益明细</div> -->
</div>
</div>
<div class="info-item">
<div class="info-item-title">累计收益</div>
<div class="info-item-value">{{ (state.userWallet.vidIncome * 100) / 100 || 0 }}</div>
<div class="btnbox">
<!-- <div class="btn flex-center-center" @click="$router.push({ path: '/withdraw', query: { title: '收益提现' } })">
立即提现
</div> -->
<div class="btn flex-center-center" @click="$router.push('/revenueDetails')">业绩明细</div>
</div>
</div>
</div>
<van-tabs
v-model:active="state.active"
title-active-color="#FFFFFF"
title-inactive-color="rgba(255, 255, 255, 0.55)"
animated
@change="onChange"
>
<van-tab :title="item.name" :name="item.id" v-for="(item, index) in state.tabList" :key="index">
<div class="content overflow-y-auto" :id="`minepostPage${item.id}`" v-if="state.active === item.id">
<PullRefresh v-if="!state.loading" :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="listBox" v-for="(item, index) in state.list" :key="index">
<div class="list_status">
<p>{{ state.active === 1 ? '发布于' : '上传于' }}{{ fromNowTime(item.createdAt) }}</p>
<div
v-if="!state.isEdit"
class="status_btn"
:class="{ status_err: item.status == 2 }"
@click.stop="item.status == 2 ? showDialog(item.reason) : ''"
>
{{ setText(item.status) }}
</div>
</div>
<UserPost class="post" :postInfo="item" :currentIndex="index" :list="state.list" :key="item" />
<div v-if="state.isEdit" class="delete-box" @click="handleCheck(item)">
<div class="delete-btn">
<div v-if="!item.checked" class="unselect"></div>
<div v-if="item.checked" class="select"></div>
</div>
</div>
</div>
</PullRefresh>
<Loading v-else />
</div>
</van-tab>
</van-tabs>
<van-popup v-model:show="state.isShow">
<div class="warp">
<div class="content_warp app-popup-surface">
<div class="content">
<p class="title app-popup-title">未过审原因</p>
<div class="mess app-popup-body" v-html="state.reason"></div>
<div class="btn_box">
<div class="ISee flex-center-center app-popup-primary" @click="state.isShow = false">我知道了</div>
</div>
</div>
</div>
</div>
</van-popup>
<div class="bottom-box" v-if="state.isEdit">
<div class="btn flex-center-center" @click="handleCheckAll">
{{ isAllChecked ? '取消全选' : '全选' }}
</div>
<div class="line"></div>
<div class="btn flex-center-center delete-btn" @click="handleDelete">删除({{ checkedLength }})</div>
</div>
<div class="add_info" v-if="!state.isEdit" @click="state.showAddBox = true"></div>
<van-popup v-model:show="state.showAddBox" round position="bottom">
<div class="addPopup">
<div class="title">
<div class="add_line" />
选择发布类型
<!-- <SvgIcon name="popup_close" @click="state.showAddBox = false"></SvgIcon> -->
</div>
<div class="pop_mess">
<div @click="$router.push('/pubilshImg'), (state.showAddBox = false)">
<img src="@/assets/images/png/add_img.png" alt="" />
图片
</div>
<div @click="$router.push('/publishVideo'), (state.showAddBox = false)">
<img src="@/assets/images/png/add_video.png" alt="" />
视频
</div>
<div @click="$router.push('/publishGraphics'), (state.showAddBox = false)">
<img src="@/assets/images/png/add_text.png" alt="" />
图文
</div>
</div>
</div>
</van-popup>
</div>
</template>
<script lang="ts">
export default {
name: 'minepostpage',
};
</script>
<script lang="ts" setup>
import { reactive, nextTick, onActivated, computed } from 'vue';
import UserApi from '@/service/user';
import { useUserStore } from '@/stores/user';
import { fromNowTime } from '@/utils/index';
import TabPageScroll from '@/utils/resetTabPageScrollTop';
const tabPageScroll = new TabPageScroll();
const userStore = useUserStore();
const state = reactive({
userInfo: <Types.User.UserInfoType>{},
userWallet: <Types.User.WalletInfo>{},
loading: true,
pageNumber: 1,
pageSize: 10,
list: <Array<any>>[],
isShow: false,
reason: '',
tabList: [
{ name: '已发布', id: 1 },
{ name: '待审核', id: 0 },
{ name: '未通过', id: 2 },
],
active: 1,
isEdit: false,
showAddBox: false,
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: true,
});
const isAllChecked = computed(() => {
return state.list.length > 0 && state.list.every((v: any) => v.checked);
});
const checkedLength = computed(() => {
return state.list.filter((v: any) => v.checked).length;
});
// 获取帖子
const getList = async (type: any) => {
try {
let req = {
// uid:state.userInfo.uid,
pageNumber: state.pageNumber,
pageSize: state.pageSize,
status: state.active,
};
let res: any = await UserApi.minePublish(req);
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
console.log(res);
let list = res.list || [];
if (type === 'refresh') {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1 && state.list.length == 0) {
moreParams.isNoData = true;
}
if (!res.hasNext) {
moreParams.finished = true;
}
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
onActivated(() => {
let datas = localStorage.getItem('operateData') ? JSON.parse(localStorage.getItem('operateData') as string) : '';
if (datas) {
console.log(datas);
state.list.forEach((v: any, index: any) => {
if (v.id == datas.id) {
state.list.splice(index, 1, datas);
}
});
}
});
nextTick(() => {
state.userInfo = userStore.userInfo;
/**
* 获取金币钱包信息
*/
userStore.updateWallet();
state.userWallet = userStore.walletInfo;
getList(null);
tabPageScroll.add(`#minepostPage${state.active}`);
});
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList(null);
};
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
getList('refresh');
};
const showDialog = (data: any) => {
state.isShow = true;
state.reason = data;
};
const setText = (data: any) => {
if (data == 0) {
return '待审核';
} else if (data == 1) {
return '已通过';
} else if (data == 2) {
return '未通过原因';
// return '查看原因';
}
};
const onChange = (data: any) => {
state.pageNumber = 1;
state.list = [];
state.isEdit = false;
moreParams.loading = true;
moreParams.finished = false;
moreParams.refreshing = false;
moreParams.isNoData = false;
moreParams.error = false;
getList(null);
setTimeout(() => {
tabPageScroll.add(`#minepostPage${state.active}`);
}, 100);
};
const editPost = () => {
console.log('编辑');
state.isEdit = !state.isEdit;
};
const handleCheck = (item: any) => {
item.checked = !item.checked;
console.log(item);
};
const handleCheckAll = () => {
const allChecked = state.list.every((item) => item.checked);
state.list.forEach((v: any) => {
v.checked = !allChecked;
});
};
const handleDelete = async () => {
console.log(state.list);
let ids: Array<string> = [];
state.list.forEach((v: any) => {
if (v.checked) {
ids.push(v.id);
}
});
await UserApi.minePublishDelete({
ids,
});
state.list = state.list.filter((i) => !ids.includes(i.id));
console.log(state.list);
};
</script>
<style lang="scss" scoped>
.minepostpage {
height: 100%;
box-sizing: border-box;
background: #000;
:deep(.header) {
.right-btn {
color: rgba(152, 152, 152, 1);
font-size: 12px;
&.right-btn--done {
color: #ffc000;
}
}
}
.info {
height: 125px;
background: rgba(255, 255, 255, 0.05);
font-size: 15px;
display: flex;
align-items: center;
justify-content: center;
margin: 12px 16px;
border-radius: 8px;
padding: 0 30px;
.info-item {
display: flex;
align-items: center;
flex-direction: column;
padding: 0 10px;
justify-content: center;
flex: 1;
.info-item-title {
font-size: 14px;
color: rgba(255, 255, 255, 0.7);
margin-bottom: 10px;
}
.info-item-value {
font-size: 24px;
color: #fff;
}
}
.btnbox {
margin: 10px 0 0 0;
display: flex;
align-items: center;
justify-content: center;
div {
font-size: 12px;
background: #ffc000;
color: #1f2029;
width: 72px;
height: 25px;
border-radius: 13.5px;
}
}
}
.content {
height: calc(100vh - 200px);
padding: 0 10px;
padding-bottom: 62px;
.listBox {
position: relative;
.list_status {
padding: 0 10px;
margin-top: 12px;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
p {
color: rgba(255, 255, 255, 0.55);
}
.status_btn {
color: #03fceb;
}
.status_err {
color: #f52c56;
}
}
.delete-box {
position: absolute;
right: 0;
top: 0;
width: 100%;
height: 100%;
.delete-btn {
position: absolute;
top: 0;
right: 20px;
.unselect {
width: 22px;
height: 22px;
border-radius: 50%;
border: 1.5px solid rgba(255, 255, 255, 0.45);
box-sizing: border-box;
background: transparent;
}
.select {
width: 22px;
height: 22px;
border-radius: 50%;
background: #ffc000;
position: relative;
&::after {
content: '';
position: absolute;
left: 7px;
top: 4px;
width: 5px;
height: 9px;
border: solid #1f2029;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
}
}
}
}
:deep(.my-refresh) {
padding: 0;
}
}
}
.warp {
color: #fff;
.content_warp {
width: 311px;
border-radius: 10px;
background: #000;
.content {
height: auto !important;
font-size: 12px;
.title {
font-size: 20px;
text-align: center;
padding: 30px 0;
box-sizing: border-box;
}
.mess {
width: 100%;
font-size: 15px;
padding: 0 30px;
box-sizing: border-box;
line-height: 20px;
color: #a2a4ac;
word-break: break-all;
}
}
.btn_box {
width: 100%;
display: flex;
align-items: center;
justify-content: space-evenly;
margin: 30px 0 0 0;
padding-bottom: 20px;
box-sizing: border-box;
.ISee {
width: 205px;
height: 44px;
background: #ffc000;
color: #1f2029;
margin: 0 10px 0 0;
border-radius: 22px;
font-size: 16px;
}
.ISee:nth-child(2) {
margin: 0;
}
}
}
}
:deep(.van-tabs) {
.van-tabs__wrap {
background: #000;
position: sticky;
top: 0;
z-index: 9;
}
.van-tabs__nav {
background: #000;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
.van-tab {
flex: 1;
padding: 0 18px 5px 18px;
box-sizing: border-box;
}
.van-tabs__line {
width: 13px;
height: 3px;
bottom: 22px;
background: #ffc000;
border-radius: 2px;
}
}
.bottom-box {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 20;
width: 100%;
height: calc(62px + env(safe-area-inset-bottom));
padding: 0 24px env(safe-area-inset-bottom);
display: flex;
align-items: center;
justify-content: space-between;
background: #1a1a1a;
box-sizing: border-box;
.line {
display: none;
}
.btn {
min-width: auto;
height: 38px;
border-radius: 19px;
font-size: 15px;
color: rgba(255, 255, 255, 0.55);
padding: 0 8px;
}
.delete-btn {
min-width: 110px;
background: #f52c56;
color: #fff;
border-radius: 19px;
}
}
.add_info {
position: fixed;
bottom: 40px;
right: 16px;
z-index: 30;
width: 40px;
height: 40px;
background: url('@/assets/images/png/add_post.png') no-repeat;
background-size: 100% 100%;
}
.addPopup {
width: 100%;
padding: 20px 20px 60px 20px;
box-sizing: border-box;
background: #000;
.title {
text-align: center;
margin-bottom: 20px;
font-size: 16px;
color: #fff;
position: relative;
svg {
width: 13px;
height: 13px;
position: absolute;
right: 10px;
}
.add_line {
margin: 0 auto 16px;
width: 36px;
height: 5px;
background: #4d4d5c;
border-radius: 99px;
}
}
.pop_mess {
display: flex;
align-items: center;
justify-content: space-around;
color: #a3a2a2;
padding: 0 30px;
> div {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
font-size: 14px;
}
img {
display: block;
width: 40px;
height: 40px;
margin-bottom: 10px;
}
}
}
</style>
+213
View File
@@ -0,0 +1,213 @@
<template>
<div class="bindingPhone">
<Header title="手机号找回" />
<div class="content overflow-y-auto">
<div class="form">
<div class="itembox">
<div class="value">
<van-field
label="手机号"
v-model="state.phoneValue"
center
clearable
maxlength="11"
type="tel"
placeholder="请输入手机号码"
/>
</div>
</div>
<div class="itembox">
<div class="value">
<van-field
label="验证码"
v-model="state.codeValue"
center
clearable
maxlength="6"
type="digit"
placeholder="输入验证码"
/>
<div class="timer" v-if="state.timer">{{ state.timer }}S</div>
<div class="getBtn" @click="countdown" v-else>获取验证码</div>
</div>
</div>
</div>
<div class="subBtnBox" @click="bindAccount">
<div class="subBtn">立即找回</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import { showToast } from 'vant';
import UserApi from '@/service/user';
import { useUserStore } from '@/stores/user';
import { useRouter } from 'vue-router';
import { useConfigStore } from '@/stores/config';
import { refreshPaymentGuideConfig } from '@/utils/paymentGuide';
import { refreshPaymentPopupConfig } from '@/utils/paymentStatusPopup';
const userStore = useUserStore();
const router = useRouter();
const configStore = useConfigStore();
const state = reactive({
phoneValue: <string>'',
codeValue: <string>'',
timer: <number>0,
userInfo: userStore.userInfo,
});
const countdown = () => {
if (!state.phoneValue) {
showToast('手机号不能为空');
return;
}
sendCode();
};
const sendCode = () => {
let res: any = UserApi.getCaptcha({
type: 2,
mobile: '+86' + state.phoneValue,
});
res.then(() => {
state.timer = 60;
let countdownTime = setInterval(() => {
state.timer--;
if (state.timer <= 0) {
clearInterval(countdownTime);
}
}, 1000);
});
};
const bindAccount = async () => {
try {
if (!state.phoneValue) {
showToast('手机号不能为空');
return;
}
if (!state.codeValue) {
showToast('验证码不能为空');
return;
}
let json = {
mobile: '+86' + state.phoneValue,
code: state.codeValue,
};
let data: any = await UserApi.findAccount(json);
await userStore.saveLoginResult({ userInfo: data.userInfo, token: data.token });
await userStore.updateWallet();
await Promise.all([refreshPaymentPopupConfig(), refreshPaymentGuideConfig()]);
showToast('手机号码找回成功');
router.push('/mine');
let createAt = new Date(userStore.userInfo.createdAt).getTime();
let now = Date.now();
if (now - createAt > configStore.appConfig?.newbieSaleTime * 60 * 60 * 1000 && now - createAt > 3600000 * 24) {
const key = `first_login_time_${userStore.userInfo.uid}`;
let firstLoginTime: any = localStorage.getItem(key);
if (!firstLoginTime) {
firstLoginTime = Date.now().toString();
localStorage.setItem(key, firstLoginTime);
}
}
} catch (e) {
console.log(e);
}
};
</script>
<style lang="scss" scoped>
.bindingPhone {
height: 100%;
background: #000;
.content {
height: calc(100vh - 44px);
padding-top: 40px;
.form {
padding: 0 42px;
}
.itembox {
width: 100%;
color: #fff;
.value {
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
margin-bottom: 24px;
}
}
:deep(.van-field__label) {
width: 60px;
color: #fff;
font-size: 16px;
margin-right: 12px;
}
:deep(.van-field) {
background-color: transparent;
padding: 0;
flex: 1;
}
:deep(.van-cell::after) {
display: none;
}
:deep(.van-field__control) {
color: #fff;
font-size: 12px;
}
::placeholder {
color: rgba(255, 255, 255, 0.35);
font-size: 12px;
}
.getBtn {
flex-shrink: 0;
font-size: 16px;
color: rgba(255, 255, 255, 0.5);
white-space: nowrap;
margin-left: 8px;
}
.timer {
flex-shrink: 0;
font-size: 16px;
color: #ffc000;
margin-left: 8px;
}
.subBtnBox {
display: flex;
justify-content: center;
margin-top: 80px;
.subBtn {
width: 264px;
height: 44px;
border-radius: 22px;
background: #ffc000;
color: #1f2029;
font-size: 16px;
font-weight: 500;
display: flex;
justify-content: center;
align-items: center;
}
}
}
}
</style>
+376
View File
@@ -0,0 +1,376 @@
<template>
<div class="myAccount">
<Header title="账号找回" />
<div class="menu overflow-y-auto">
<div class="code row" @click="toBindingPhone">
<div class="left">手机号找回</div>
<div class="right">
<van-icon name="arrow" color="rgba(255,255,255,0.35)" size="14px" />
</div>
</div>
<div class="code row" @click.stop>
<div class="left">凭证找回</div>
<div class="right">
<van-icon name="arrow" color="rgba(255,255,255,0.35)" size="14px" />
</div>
<input class="file" type="file" accept="image/*" @change="uploadQrcode($event)" />
</div>
</div>
<van-popup v-model:show="scanVisible" position="bottom" :style="{ height: '70%' }" @closed="stopScanner">
<div class="scan-popup">
<div class="scan-title">请将二维码放入框内</div>
<div id="reader"></div>
<div class="scan-tip">若摄像头无法使用,可返回使用“图片识别找回”</div>
<div class="scan-actions">
<van-button block type="default" @click="closeScan">关闭</van-button>
</div>
</div>
</van-popup>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router';
import { showLoadingToast, showToast, closeToast } from 'vant';
import UserApi from '@/service/user';
import { useUserStore } from '@/stores/user';
import { nextTick, reactive, ref } from 'vue';
import qrcode from '@/assets/js/llqrcode.js';
import { useConfigStore } from '@/stores/config';
import { refreshPaymentGuideConfig } from '@/utils/paymentGuide';
import { refreshPaymentPopupConfig } from '@/utils/paymentStatusPopup';
import { Html5Qrcode } from 'html5-qrcode';
const router = useRouter();
const userStore = useUserStore();
const configStore = useConfigStore();
const state = reactive({
userInfo: userStore.userInfo,
});
const scanVisible = ref(false);
let html5QrCode: Html5Qrcode | null = null;
let scanning = false;
nextTick(() => {
state.userInfo = userStore.userInfo;
});
/**
* 压缩图片
*/
const compress = (file: File) => {
if (!file || !window.FileReader) return Promise.reject('文件读取失败');
if (!/^image/.test(file.type)) {
return Promise.reject('请选择图片文件');
}
const reader = new FileReader();
reader.readAsDataURL(file);
return new Promise<string>((resolve, reject) => {
reader.onloadend = function () {
const result = reader.result as string;
const img = new Image();
img.src = result;
img.onload = function () {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
reject('图片处理失败');
return;
}
const width = img.width;
const height = img.height;
canvas.width = width;
canvas.height = height;
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, width, height);
const ndata = canvas.toDataURL('image/jpeg', 0.5);
resolve(ndata);
};
img.onerror = function () {
reject('图片加载失败');
};
};
reader.onerror = function () {
reject('文件读取失败');
};
});
};
/**
* 图片识别登录
*/
const uploadQrcode = async (e: Event) => {
const fileInput = e.target as HTMLInputElement;
if (!fileInput.files || !fileInput.files.length) {
showToast('未能读取到图片,请重试!');
return;
}
showLoadingToast({
message: '识别中...',
duration: 0,
forbidClick: true,
});
try {
const file = fileInput.files[0];
const base64 = await compress(file);
qrcode.callback = async (imgMsg: string) => {
try {
if (!imgMsg) {
showToast('未识别到二维码内容');
return;
}
await login(imgMsg);
} catch (error: any) {
showToast(error?.message || '登录失败');
} finally {
fileInput.value = '';
closeToast();
}
};
qrcode.decode(base64);
} catch (error: any) {
fileInput.value = '';
closeToast();
showToast(error || '二维码识别失败,请重试');
}
};
/**
* 打开摄像头扫码
*/
const openScan = async () => {
scanVisible.value = true;
await nextTick();
try {
if (!html5QrCode) {
html5QrCode = new Html5Qrcode('reader');
}
if (scanning) return;
await html5QrCode.start(
{ facingMode: 'environment' },
{
fps: 10,
qrbox: { width: 220, height: 220 },
},
async (decodedText: string) => {
if (!decodedText) return;
showLoadingToast({
message: '登录中...',
duration: 0,
forbidClick: true,
});
try {
await stopScanner();
await login(decodedText);
scanVisible.value = false;
} catch (error: any) {
showToast(error?.message || '扫码登录失败');
} finally {
closeToast();
}
},
() => {
// 忽略单帧识别失败
},
);
scanning = true;
} catch (error) {
scanVisible.value = false;
showToast('当前环境无法打开摄像头,请改用图片识别找回');
}
};
/**
* 关闭扫码
*/
const closeScan = async () => {
await stopScanner();
scanVisible.value = false;
};
/**
* 停止扫码器
*/
const stopScanner = async () => {
try {
if (html5QrCode && scanning) {
await html5QrCode.stop();
await html5QrCode.clear();
}
} catch (error) {
// 忽略 stop 异常
} finally {
scanning = false;
}
};
/**
* 规范化扫码登录回包(兼容扁平 user / 嵌套 { userInfo, token })
*/
const normalizeQrLoginResult = (raw: unknown): { userInfo: Types.User.UserInfoType; token: string } => {
const data = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>;
const nested = data.userInfo && typeof data.userInfo === 'object' ? (data.userInfo as Record<string, unknown>) : null;
const userInfo = (nested?.uid != null ? nested : data) as unknown as Types.User.UserInfoType;
const token = String(data.token || nested?.token || userInfo?.token || '');
if (!token) {
throw new Error('登录失败:未返回 token');
}
if (userInfo?.uid == null || String(userInfo.uid) === '') {
throw new Error('登录失败:未返回用户信息');
}
return { userInfo, token };
};
/**
* 登录(切号后由 saveLoginResult 统一销毁旧 IM 并按新业务 token 重建)
*/
const login = async (qrUrl: string) => {
const raw = await UserApi.loginGuest({
content: qrUrl,
});
const { userInfo, token } = normalizeQrLoginResult(raw);
await userStore.saveLoginResult({ userInfo, token });
await userStore.updateWallet();
await Promise.all([refreshPaymentPopupConfig(), refreshPaymentGuideConfig()]);
const createAt = new Date(userStore.userInfo.createdAt).getTime();
const now = Date.now();
if (
now - createAt > (configStore.appConfig?.newbieSaleTime || 0) * 60 * 60 * 1000 &&
now - createAt > 24 * 60 * 60 * 1000
) {
const key = `first_login_time_${userStore.userInfo.uid}`;
let firstLoginTime = localStorage.getItem(key);
if (!firstLoginTime) {
firstLoginTime = Date.now().toString();
localStorage.setItem(key, firstLoginTime);
}
}
showToast('登录成功');
router.back();
};
/**
* 跳转手机号绑定
*/
const toBindingPhone = () => {
router.push('/findPhone');
};
</script>
<style lang="scss" scoped>
.myAccount {
height: 100%;
background: #000;
.menu {
margin: 12px 16px 0;
border-radius: 9px;
overflow: hidden;
background: rgba(255, 255, 255, 0.05);
.file {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
opacity: 0;
z-index: 2;
}
.row {
height: 44px;
display: flex;
padding-left: 12px;
padding-right: 12px;
justify-content: space-between;
align-items: center;
position: relative;
&:not(:last-child)::before {
content: '';
bottom: 0;
left: 12px;
right: 12px;
position: absolute;
height: 1px;
background: rgba(255, 255, 255, 0.06);
}
.left {
font-size: 14px;
color: #fff;
line-height: 20px;
}
.right {
font-size: 12px;
color: rgba(255, 255, 255, 0.35);
display: flex;
align-items: center;
}
}
}
.scan-popup {
padding: 16px;
.scan-title {
text-align: center;
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
}
.scan-tip {
margin-top: 12px;
text-align: center;
font-size: 12px;
color: #999;
}
#reader {
width: 100%;
min-height: 260px;
overflow: hidden;
border-radius: 12px;
background: #000;
}
.scan-actions {
margin-top: 16px;
}
}
}
</style>
@@ -0,0 +1,181 @@
<template>
<!-- 列表数据 -->
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="sixGrid">
<van-checkbox-group v-model="state.result" ref="checkboxGroup">
<HorizontalGameBox
class="gameBox"
:videoInfo="item"
v-for="item in state.list"
:key="item.id"
:isEdit="isEdit"
@select="handleSelect"
@del="handleDel"
/>
</van-checkbox-group>
</div>
</PullRefresh>
</template>
<script setup lang="ts">
import { watch, reactive, nextTick, computed } from 'vue';
import UserApi from '@/service/user';
import { useUserStore } from '@/stores/user';
import CommunityApi from '@/service/community';
const props = defineProps({
isEdit: {
type: Boolean,
default: false,
},
type: {
type: String,
default: '',
},
});
const emit = defineEmits(['changeSelectList']);
const { userInfo } = useUserStore();
// 列表数据项
const state = reactive({
list: <Types.Video.VInfosItem[]>[],
pageNumber: 1,
pageSize: 12,
result: <string[]>[],
});
// PullRefresh 参数
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 列表获取
const getList = async (type = '') => {
try {
let res = await UserApi.queryLikeList({
uid: userInfo.uid,
likeType: props.type,
pageNumber: state.pageNumber,
pageSize: state.pageSize,
});
const list = res.list || [];
moreParams.loading = false;
moreParams.refreshing = false;
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
// 数据重置
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
// 触底加载
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
// 下拉刷新
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
const selectAll = computed(() => {
return state.result.length === state.list.length;
});
// 选择
const handleSelect = (id: string) => {
if (state.result.includes(id)) {
state.result = state.result.filter((item: string) => item !== id);
} else {
state.result.push(id);
}
emit('changeSelectList', state.result, selectAll.value);
};
// 全选点击
const toggleSelect = () => {
if (state.result.length === state.list.length) {
state.result = [];
} else {
state.result = state.list.map((item: Types.Video.VInfosItem) => item.id);
}
emit('changeSelectList', state.result, selectAll.value);
};
const handleDel = async (id: string) => {
await CommunityApi.cancleZan({
objIDs: [id],
type: props.type,
});
state.list = state.list.filter((item: Types.Video.VInfosItem) => item.id !== id);
};
watch(
() => props.isEdit,
(val) => {
if (!val) {
state.result = [];
}
},
);
watch(
() => state.result,
(val) => {
nextTick(() => {
emit('changeSelectList', val, selectAll.value);
});
},
);
defineExpose({
reset,
onLoad,
onRefresh,
toggleSelect,
});
</script>
<style scoped lang="scss">
.sixGrid {
.van-checkbox-group {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 8px;
grid-row-gap: 10px;
padding: 0 16px;
:deep() {
.gameBox {
width: 168px;
}
.videoBox {
width: 100%;
overflow: hidden;
.coverBox {
border-radius: 6px !important;
}
}
}
}
}
</style>
@@ -0,0 +1,143 @@
<template>
<!-- 列表数据 -->
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="categoryContent">
<CategoryContent
ref="categoryContentRef"
@changeSelectList="changeSelectList"
:isEdit="isEdit"
:contentWrap="true"
:list="state.list"
:contentType="type"
:coverDirection="coverDirection"
@del="handleDel"
/>
</div>
</PullRefresh>
</template>
<script setup lang="ts">
import { ref, reactive, nextTick, computed } from 'vue';
import CategoryContent from '@/views/acgModule/components/categoryContent.vue';
import UserApi from '@/service/user';
import CommunityApi from '@/service/community';
const categoryContentRef = ref();
const emit = defineEmits(['changeSelectList']);
const props = defineProps({
isEdit: {
type: Boolean,
default: false,
},
type: {
type: String,
default: 'image',
},
});
const coverDirection = computed(() => (props.type === 'video' ? 'landscape' : 'portrait'));
// 列表数据项
const state = reactive({
list: <any>[],
pageNumber: 1,
pageSize: 12,
});
// PullRefresh 参数
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
const getList = async (type = '') => {
const parmas: any = {
likeType: props.type,
...state,
};
delete parmas.list;
try {
let res: any = await UserApi.queryLikeList(parmas);
const list = res.list || [];
moreParams.loading = false;
moreParams.refreshing = false;
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
const changeSelectList = (result: string[], selectAll: boolean) => {
emit('changeSelectList', result, selectAll);
};
const handleDel = async (id: string) => {
await CommunityApi.cancleZan({
objIDs: [id],
type: props.type,
});
state.list = state.list.filter((item: Types.Video.VInfosItem) => item.id !== id);
};
reset();
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
defineExpose({
reset,
onLoad,
onRefresh,
toggleSelect: () => {
categoryContentRef.value?.toggleSelect();
},
});
</script>
<style scoped lang="scss">
.categoryContent {
padding: 12px 10px;
:deep() {
.category-content {
.list {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-column-gap: 5px;
grid-row-gap: 12px;
.item {
width: 100% !important;
overflow: hidden;
margin: 0 !important;
}
}
}
.category-content.landscape .list {
grid-template-columns: 1fr 1fr;
grid-column-gap: 7px;
}
}
}
</style>
@@ -0,0 +1,180 @@
<template>
<!-- 列表数据 -->
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="sixGrid">
<van-checkbox-group v-model="state.result" ref="checkboxGroup">
<HorizontalVideoBox
class="videoBox"
:videoInfo="item"
:videoList="state.list"
v-for="item in state.list"
:key="item.id"
:isEdit="isEdit"
@select="handleSelect"
@del="handleDel"
/>
</van-checkbox-group>
</div>
</PullRefresh>
</template>
<script setup lang="ts">
import UserApi from '@/service/user';
import { watch, reactive, nextTick, computed } from 'vue';
import { useUserStore } from '@/stores/user';
import CommunityApi from '@/service/community';
const { userInfo } = useUserStore();
const props = defineProps({
isEdit: {
type: Boolean,
default: false,
},
type: {
type: String,
default: 'SP',
},
});
const emit = defineEmits(['changeSelectList', 'load']);
// 列表数据项
const state = reactive({
list: <Types.Video.VInfosItem[]>[],
pageNumber: 1,
pageSize: 12,
result: <string[]>[],
});
// PullRefresh 参数
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 列表获取
const getList = async (type = '') => {
try {
let res: any = await UserApi.queryLikeList({
uid: userInfo.uid,
likeType: props.type,
pageNumber: state.pageNumber,
pageSize: state.pageSize,
});
const list = res.list || [];
moreParams.loading = false;
moreParams.refreshing = false;
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
emit('load', state.list, 0);
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
// 数据重置
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
// 触底加载
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
// 下拉刷新
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
const selectAll = computed(() => {
return state.result.length === state.list.length;
});
// 选择
const handleSelect = (id: string) => {
if (state.result.includes(id)) {
state.result = state.result.filter((item: string) => item !== id);
} else {
state.result.push(id);
}
emit('changeSelectList', state.result, selectAll.value);
};
// 全选点击
const toggleSelect = () => {
if (state.result.length === state.list.length) {
state.result = [];
} else {
state.result = state.list.map((item: Types.Video.VInfosItem) => item.id);
}
emit('changeSelectList', state.result, selectAll.value);
};
const handleDel = async (id: string) => {
await CommunityApi.cancleZan({
objIDs: [id],
type: props.type,
});
state.list = state.list.filter((item: Types.Video.VInfosItem) => item.id !== id);
};
watch(
() => props.isEdit,
(val) => {
if (!val) {
state.result = [];
}
},
);
watch(
() => state.result,
(val) => {
nextTick(() => {
emit('changeSelectList', val, selectAll.value);
});
},
);
defineExpose({
reset,
onLoad,
onRefresh,
toggleSelect,
});
</script>
<style scoped lang="scss">
.sixGrid {
.van-checkbox-group {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 7px;
grid-row-gap: 12px;
padding: 12px 10px;
:deep() {
.videoBox {
width: 100%;
overflow: hidden;
.coverBox {
border-radius: 6px !important;
height: 95px !important;
}
}
}
}
}
</style>
@@ -0,0 +1,157 @@
<template>
<!-- 列表数据 -->
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<van-checkbox-group v-model="state.result" ref="checkboxGroup">
<div class="listBox" v-for="(item, index) in state.list" :key="item.id">
<Post :list="state.list" :postInfo="item" :currentIndex="index" :isEdit="isEdit" @del="handleDel" />
</div>
</van-checkbox-group>
</PullRefresh>
</template>
<script setup lang="ts">
import UserApi from '@/service/user';
import { watch, reactive, nextTick, computed } from 'vue';
import { useUserStore } from '@/stores/user';
import CommunityApi from '@/service/community';
const props = defineProps({
isEdit: {
type: Boolean,
default: false,
},
type: {
type: String,
default: 'COVER',
},
});
const emit = defineEmits(['changeSelectList', 'load']);
const { userInfo } = useUserStore();
// 列表数据项
const state = reactive({
list: <Types.Video.VInfosItem[]>[],
pageNumber: 1,
pageSize: 12,
result: <string[]>[],
});
// PullRefresh 参数
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 列表获取
const getList = async (type = '') => {
try {
let res: any = await UserApi.queryLikeList({
uid: userInfo.uid,
likeType: props.type,
pageNumber: state.pageNumber,
pageSize: state.pageSize,
});
const list = res.list || [];
moreParams.loading = false;
moreParams.refreshing = false;
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
emit('load', state.list, 2);
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
// 数据重置
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
// 触底加载
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
// 下拉刷新
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
const selectAll = computed(() => {
return state.result.length === state.list.length;
});
// 选择
const handleSelect = (id: string) => {
if (state.result.includes(id)) {
state.result = state.result.filter((item: string) => item !== id);
} else {
state.result.push(id);
}
emit('changeSelectList', state.result, selectAll.value);
};
// 全选点击
const toggleSelect = () => {
if (state.result.length === state.list.length) {
state.result = [];
} else {
state.result = state.list.map((item: Types.Video.VInfosItem) => item.id);
}
emit('changeSelectList', state.result, selectAll.value);
};
const handleDel = async (id: string) => {
console.log(id, 'id');
console.log(state.list, 'state.list1');
await CommunityApi.cancleZan({
objIDs: [id],
type: props.type,
});
state.list = state.list.filter((item: Types.Video.VInfosItem) => item.id !== id);
console.log(state.list, 'state.list2');
};
watch(
() => props.isEdit,
(val) => {
if (!val) {
state.result = [];
}
},
);
watch(
() => state.result,
(val) => {
nextTick(() => {
emit('changeSelectList', val, selectAll.value);
});
},
);
defineExpose({
reset,
onLoad,
onRefresh,
toggleSelect,
});
</script>
<style scoped lang="scss">
.listBox {
margin: 0 10px;
padding-top: 12px;
}
</style>
@@ -0,0 +1,191 @@
<template>
<!-- 列表数据 -->
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="sixGrid">
<van-checkbox-group v-model="state.result" ref="checkboxGroup">
<HorizontalVideoBox
class="videoBox"
:videoInfo="item"
:videoList="state.list"
v-for="item in state.list"
:isShort="true"
:key="item.id"
:isEdit="isEdit"
@select="handleSelect"
@del="handleDel"
/>
</van-checkbox-group>
</div>
</PullRefresh>
</template>
<script setup lang="ts">
import { watch, reactive, nextTick, computed } from 'vue';
import UserApi from '@/service/user';
import { useUserStore } from '@/stores/user';
import CommunityApi from '@/service/community';
const props = defineProps({
isEdit: {
type: Boolean,
default: false,
},
type: {
type: String,
default: 'SHORT',
},
});
const emit = defineEmits(['changeSelectList', 'load']);
const { userInfo } = useUserStore();
// 列表数据项
const state = reactive({
list: <Types.Video.VInfosItem[]>[],
pageNumber: 1,
pageSize: 12,
result: <string[]>[],
});
// PullRefresh 参数
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 列表获取
const getList = async (type = '') => {
try {
let res: any = await UserApi.queryLikeList({
uid: userInfo.uid,
likeType: props.type,
pageNumber: state.pageNumber,
pageSize: state.pageSize,
});
const list = res.list || [];
moreParams.loading = false;
moreParams.refreshing = false;
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
emit('load', state.list, 1);
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
// 数据重置
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
// 触底加载
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
// 下拉刷新
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
const selectAll = computed(() => {
return state.result.length === state.list.length;
});
// 选择
const handleSelect = (id: string) => {
if (state.result.includes(id)) {
state.result = state.result.filter((item: string) => item !== id);
} else {
state.result.push(id);
}
emit('changeSelectList', state.result, selectAll.value);
};
// 全选点击
const toggleSelect = () => {
if (state.result.length === state.list.length) {
state.result = [];
} else {
state.result = state.list.map((item: Types.Video.VInfosItem) => item.id);
}
emit('changeSelectList', state.result, selectAll.value);
};
const handleDel = async (id: string) => {
await CommunityApi.cancleZan({
objIDs: [id],
type: props.type,
});
state.list = state.list.filter((item: Types.Video.VInfosItem) => item.id !== id);
};
watch(
() => props.isEdit,
(val) => {
if (!val) {
state.result = [];
}
},
);
watch(
() => state.result,
(val) => {
nextTick(() => {
emit('changeSelectList', val, selectAll.value);
});
},
);
defineExpose({
reset,
onLoad,
onRefresh,
toggleSelect,
});
</script>
<style scoped lang="scss">
.sixGrid {
.van-checkbox-group {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-column-gap: 5px;
grid-row-gap: 12px;
padding: 12px 10px;
:deep() {
.videoBox {
width: 100%;
overflow: hidden;
position: relative;
.coverBox {
border-radius: 6px !important;
height: 148px !important;
}
.videoTitle {
height: auto !important;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1 !important;
-webkit-box-orient: vertical;
}
}
}
}
}
</style>
@@ -0,0 +1,192 @@
<template>
<!-- 列表数据 -->
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="sixGrid">
<van-checkbox-group v-model="state.result" ref="checkboxGroup">
<HorizontalVideoBox
class="videoBox"
:videoInfo="item"
:videoList="state.list"
v-for="item in state.list"
:isShort="true"
:key="item.id"
:isEdit="isEdit"
@select="handleSelect"
@del="handleDel"
:isPic="true"
/>
</van-checkbox-group>
</div>
</PullRefresh>
</template>
<script setup lang="ts">
import { watch, reactive, nextTick, computed } from 'vue';
import UserApi from '@/service/user';
import { useUserStore } from '@/stores/user';
import CommunityApi from '@/service/community';
const { userInfo } = useUserStore();
const props = defineProps({
isEdit: {
type: Boolean,
default: false,
},
type: {
type: String,
default: 'PIC',
},
});
const emit = defineEmits(['changeSelectList']);
// 列表数据项
const state = reactive({
list: <Types.Video.VInfosItem[]>[],
pageNumber: 1,
pageSize: 12,
result: <string[]>[],
});
// PullRefresh 参数
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 列表获取
const getList = async (type = '') => {
try {
let res: any = await UserApi.queryLikeList({
uid: userInfo.uid,
likeType: props.type,
pageNumber: state.pageNumber,
pageSize: state.pageSize,
});
const list = res.list || [];
moreParams.loading = false;
moreParams.refreshing = false;
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
// 数据重置
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
// 触底加载
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
// 下拉刷新
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
const selectAll = computed(() => {
return state.result.length === state.list.length;
});
// 选择
const handleSelect = (id: string) => {
if (state.result.includes(id)) {
state.result = state.result.filter((item: string) => item !== id);
} else {
state.result.push(id);
}
emit('changeSelectList', state.result, selectAll.value);
};
// 全选点击
const toggleSelect = () => {
if (state.result.length === state.list.length) {
state.result = [];
} else {
state.result = state.list.map((item: Types.Video.VInfosItem) => item.id);
}
emit('changeSelectList', state.result, selectAll.value);
};
const handleDel = async (id: string) => {
await CommunityApi.cancleZan({
objIDs: [id],
type: props.type,
});
state.list = state.list.filter((item: Types.Video.VInfosItem) => item.id !== id);
};
watch(
() => props.isEdit,
(val) => {
if (!val) {
state.result = [];
}
},
);
watch(
() => state.result,
(val) => {
nextTick(() => {
emit('changeSelectList', val, selectAll.value);
});
},
);
defineExpose({
reset,
onLoad,
onRefresh,
toggleSelect,
});
</script>
<style scoped lang="scss">
.sixGrid {
.van-checkbox-group {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-column-gap: 5px;
grid-row-gap: 12px;
padding: 12px 10px;
:deep() {
.videoBox {
width: 100%;
overflow: hidden;
position: relative;
.coverBox {
height: 148px !important;
border-radius: 6px !important;
}
.videoTitle {
height: auto !important;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1 !important;
-webkit-box-orient: vertical;
}
}
}
}
}
</style>
+216
View File
@@ -0,0 +1,216 @@
<template>
<div class="my-like" @touchstart.stop @touchmove.stop @touchend.stop>
<Header title="我的喜欢">
<template #right>
<div class="edit" :class="{ active: state.isEdit }" @click="state.isEdit = !state.isEdit">
{{ state.isEdit ? '完成' : '编辑' }}
</div>
</template>
</Header>
<van-tabs
v-model:active="state.active"
background="#000"
title-active-color="#fff"
title-inactive-color="rgba(255, 255, 255, 0.5)"
line-height="3"
animated
swipeable
@change="tabChange"
>
<van-tab v-for="(item, index) in state.tabsList" :key="index" :title="item.title">
<div class="tab-content-auto overflow-y-auto" @scroll="onScroll" :id="`tabContent${index}`">
<component
:isEdit="state.isEdit"
v-if="item.comments"
:is="item.comments"
:key="item.name"
:type="item.type || ''"
:mode="item.mode"
@load="(list) => handleLoad(list, index)"
@changeSelectList="changeSelectList"
:ref="(el) => (childRefs[index] = el)"
/>
</div>
</van-tab>
</van-tabs>
</div>
</template>
<script lang="ts">
export default {
name: 'myLike',
};
</script>
<script setup lang="ts">
import { ref, watch, reactive, markRaw, onActivated, computed, provide } from 'vue';
import MovieList from './components/MovieList.vue';
import ShortnessVideoList from './components/ShortnessVideoList.vue';
import PostList from './components/PostList.vue';
import MangaList from './components/MangaList.vue';
import TheatresList from './components/TheatresList.vue';
import MineDramaList from '../components/MineDramaList.vue';
const childRefs = ref<any[]>([]);
const state = reactive({
loading: false,
selects: <string[]>[],
active: 0,
tabsList: [
{
title: '视频',
name: 0,
comments: markRaw(MovieList),
scrollTop: 0,
type: 'SP',
list: <Types.Video.VInfosItem[]>[],
},
{
title: '抖音',
name: 1,
comments: markRaw(ShortnessVideoList),
scrollTop: 0,
type: 'SHORT',
list: <Types.Video.VInfosItem[]>[],
},
{
title: '短剧',
name: 'drama',
comments: markRaw(MineDramaList),
scrollTop: 0,
mode: 'like',
type: 'drama',
list: <Types.Video.VInfosItem[]>[],
},
{
title: '帖子',
name: 2,
comments: markRaw(PostList),
scrollTop: 0,
type: 'COVER',
list: <Types.Video.VInfosItem[]>[],
},
{
title: '动漫',
name: 6,
comments: markRaw(MangaList),
scrollTop: 0,
type: 'video',
},
{
title: '漫画',
name: 5,
comments: markRaw(MangaList),
scrollTop: 0,
type: 'image',
},
{
title: '图集',
name: 7,
comments: markRaw(TheatresList),
type: 'PIC',
scrollTop: 0,
},
],
isEdit: false,
isAll: false,
});
provide(
'isEdit',
computed(() => state.isEdit),
);
provide('editMode', 'delete');
const handleLoad = (list: Types.Video.VInfosItem[], index: number) => {
state.tabsList[index].list = list;
};
const changeSelectList = (ids: string[], selectAll: boolean) => {
state.selects = ids;
state.isAll = selectAll;
};
const tabChange = () => {
state.isEdit = false;
state.selects = [];
setScrollTop(state.tabsList[state.active].scrollTop);
};
const onScroll = (event: any) => {
state.tabsList[state.active].scrollTop = event.target.scrollTop;
};
onActivated(() => {
setScrollTop(state.tabsList[state.active].scrollTop);
});
const setScrollTop = (top: number) => {
const dom = document.getElementById(`tabContent${state.active}`);
if (dom) {
dom.scrollTop = top;
}
};
watch(
() => state.isEdit,
(val) => {
if (!val) {
state.isAll = false;
state.selects = [];
}
},
);
</script>
<style scoped lang="scss">
.my-like {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
:deep() {
.van-tabs {
flex: 1 0;
height: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
}
.van-tabs__nav {
padding-bottom: 0;
height: 44px;
box-sizing: border-box;
}
.van-tab {
width: auto;
}
.van-tabs__line {
bottom: 7px;
width: 13px;
height: 3px;
border-radius: 12px;
background: linear-gradient(90deg, rgba(255, 192, 0, 0) 0%, #ffc000 100%);
}
.van-tabs__content {
flex: 1 0;
height: 0;
overflow: hidden;
}
.van-tab__panel {
height: 100%;
width: 100%;
overflow: hidden;
}
.tab-content-auto {
height: 100%;
width: 100%;
}
}
.edit {
font-size: 12px;
line-height: 18px;
color: rgba(255, 255, 255, 0.5);
&.active {
color: #ffc000;
}
}
}
</style>
@@ -0,0 +1,192 @@
<template>
<div class="officialCommunityPage">
<Header title="加群开车" />
<div class="content overflow-y-auto" v-if="!state.loading">
<div class="section">
<div class="title">
<p>官方用户交流社群</p>
<span>一起看片一起分享心得</span>
</div>
<div class="bodyer">
<div
class="item"
v-for="(item, index) in state.cooperationList"
:key="index"
@click="toLink(item.officialUrl)"
>
<div class="row">
<div class="left">
<lazyImg class="icon" :src="item.officialImg" :key="item.officialImg" />
<div class="left_mess">
<p>{{ item.officialName }}</p>
</div>
</div>
<div class="right">立即加入</div>
</div>
</div>
</div>
</div>
<div class="section">
<div class="title">
<p>商务合作</p>
<span>代理合作/商务合作</span>
</div>
<div class="bodyer">
<div
class="item"
v-for="(item, index) in state.communityList"
:key="index"
@click="toLink(item.officialUrl)"
>
<div class="row">
<div class="left">
<lazyImg class="icon" :src="item.officialImg" :key="item.officialImg" />
<div class="left_mess">
<p>{{ item.officialName }}</p>
</div>
</div>
<div class="right">立即加入</div>
</div>
</div>
</div>
</div>
</div>
<Loading v-else />
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import CommunityApi from '@/service/community';
import { compatibleAppStoreJump } from '@/utils/index';
const state = reactive({
loading: true,
communityList: <any>[],
cooperationList: <any>[],
});
/**
* 获取官方群聊
*/
const getList = async () => {
try {
let req = {
type: 2,
};
let res = await CommunityApi.getBusiness(req);
state.loading = false;
let list = res;
list.forEach((item: any) => {
if (item.position === 2) {
state.communityList.push(item);
} else {
state.cooperationList.push(item);
}
});
} catch (e) {
console.log(e);
}
};
getList();
/**
* 跳转链接
* @param link
*/
const toLink = (link: string) => {
compatibleAppStoreJump(link);
};
</script>
<style lang="scss" scoped>
.officialCommunityPage {
width: 100%;
height: 100%;
background: #000;
.content {
padding: 18px 16px;
height: calc(100vh - 44px);
box-sizing: border-box;
.section {
margin-bottom: 18px;
}
.title {
margin-bottom: 12px;
p {
font-size: 16px;
margin: 0 0 6px 0;
font-weight: 500;
line-height: normal;
color: #fff;
}
span {
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
line-height: normal;
}
}
.bodyer {
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
overflow: hidden;
}
.item {
width: 100%;
.row {
width: 100%;
height: 76px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 12px;
box-sizing: border-box;
.left {
display: flex;
align-items: center;
min-width: 0;
.icon {
width: 40px;
height: 40px;
border-radius: 4px;
overflow: hidden;
flex-shrink: 0;
}
.left_mess {
margin: 0 0 0 15px;
min-width: 0;
p {
margin: 0;
font-size: 14px;
font-weight: 500;
color: #fff;
line-height: normal;
}
}
}
.right {
width: auto;
min-width: 72px;
height: 28px;
padding: 0 12px;
display: flex;
justify-content: center;
align-items: center;
font-size: 12px;
font-weight: 500;
color: #1f2029;
background: #ffc000;
border-radius: 18px;
flex-shrink: 0;
box-sizing: border-box;
}
}
}
}
}
</style>
+454
View File
@@ -0,0 +1,454 @@
<template>
<div class="feedback">
<Header title="意见反馈" rightTitle="Q&A" @rightFuc="goQa" />
<div class="content overflow-y-auto">
<div class="problem-title">遇到的问题</div>
<div class="feedbackBox">
<div
v-for="item in state.list"
:key="item.id"
:class="item.isChoose ? 'active flex-center-center item' : 'item flex-center-center'"
@click="changeTag(item)"
>
{{ item.name }}
</div>
</div>
<div class="inputBox">
<span class="title">问题描述(必填)</span>
<van-field
class="textarea"
v-model="state.feedbackData"
rows="4"
autosize
type="textarea"
maxlength="300"
placeholder="请简单描述一下您遇到的问题..."
show-word-limit
@blur="blur"
/>
</div>
<div class="inputLabel">
<div class="label">所在地区</div>
<div class="input">
<van-field @blur="blur" v-model="state.area" placeholder="例:浙江杭州" />
</div>
</div>
<div class="inputLabel">
<div class="label">设备信息</div>
<div class="input">
<van-field @blur="blur" v-model="state.device" placeholder="例:苹果14" />
</div>
</div>
<div class="inputLabel">
<div class="label">网络运营商</div>
<div class="input">
<van-field @blur="blur" v-model="state.ip" placeholder="电信/联通/移动" />
</div>
</div>
<div class="inputLabel">
<div class="label">联系方式</div>
<div class="input">
<van-field @blur="blur" v-model="state.way" placeholder="QQ/微信/邮箱等" />
</div>
</div>
<div class="upload">
<div class="upload-title">图片</div>
<van-uploader
@click.stop
v-model="state.fileList"
:after-read="afterRead"
:before-read="beforeRead"
:before-delete="beforeDelete"
:multiple="true"
>
<template #default>
<div class="uploadBox flex-center-center">
<img src="@/assets/images/png/addFile.png" alt="" />
上传
</div>
</template>
<template #preview-delete>
<SvgIcon name="previewDelete" class="previewDelete"></SvgIcon>
</template>
</van-uploader>
</div>
<p class="info">以方便我们给您回复,有效的改进建议,有惊喜赠送哟!</p>
<div class="submit" @click="submit">提交意见</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive, nextTick } from 'vue';
import { showToast } from 'vant';
import UserApi from '@/service/user';
import MessageApi from '@/service/message';
import AppApi from '@/service/app';
import { useRouter } from 'vue-router';
const router = useRouter();
const state = reactive({
feedbackData: <string>'',
device: <string>'',
area: <string>'',
ip: <string>'',
way: <string>'',
list: <any>[
{ id: 1, isChoose: false, name: '账号问题' },
{ id: 2, isChoose: false, name: '影视资源' },
{ id: 3, isChoose: false, name: 'APP体验' },
{ id: 4, isChoose: false, name: '播放失败' },
{ id: 5, isChoose: false, name: '播放卡顿' },
{ id: 6, isChoose: false, name: '分类有误' },
{ id: 7, isChoose: false, name: '充值问题' },
// { id: 8, isChoose: false, name: '提现问题' },
{ id: 9, isChoose: false, name: '其他' },
],
uploadImages: <Array<string>>[],
fileList: [],
fileSize: 500 * 1024,
type: '',
});
const blur = () => {
nextTick(() => {
window.scrollTo(0, 0);
});
};
// 更改是否选中标签
function changeTag(tag: any) {
state.list.map((item: any) => {
item.isChoose = false;
});
tag.isChoose = true;
state.type = tag.name;
}
const submit = async () => {
if (!state.type) {
showToast('请选择遇到的问题');
return;
}
if (!state.feedbackData) {
showToast('问题描述不能为空');
return;
}
let datas: any = {
ISP: state.ip, //网络运营商
cateInfo: state.type, //反馈类型
contact: state.way, //联系方式
desc: state.feedbackData, //问题描述
devInfo: state.device, //设备信息
images: state.uploadImages, //图片
region: state.area, //所在地区
};
await UserApi.feedback(datas);
showToast('提交成功');
state.type = '';
state.area = '';
state.device = '';
state.ip = '';
state.way = '';
state.feedbackData = '';
state.uploadImages = [];
state.fileList = [];
};
const beforeRead = (file: any) => {
if (file.size > state.fileSize) {
showToast('文件不可超过500kb');
return false;
}
return true;
};
// 图片上传
const afterRead = async (file: any) => {
if (file.length) {
file.map(async (item: any) => {
let newFile = await compress(item.file);
item.file = newFile;
await uploadImg(item);
});
} else {
let newFile = await compress(file.file);
file.file = newFile;
uploadImg(file);
}
};
//图片上传
const uploadImg = async (fileInfo: any) => {
let req = new FormData();
req.append('upload', fileInfo.file);
fileInfo.status = 'uploading';
fileInfo.message = '上传中...';
try {
let ret: any = await AppApi.uploadFile(req);
fileInfo.status = 'done';
fileInfo.message = '上传成功';
state.uploadImages.push(ret.coverImg);
} catch (e) {
fileInfo.status = 'failed';
fileInfo.message = '上传失败';
}
};
//将base64转换为文件对象
const convertBase64ToBlob = (base64: any) => {
var base64Arr = base64.split(',');
var imgtype = '';
var base64String = '';
if (base64Arr.length > 1) {
//如果是图片base64,去掉头信息
base64String = base64Arr[1];
imgtype = base64Arr[0].substring(base64Arr[0].indexOf(':') + 1, base64Arr[0].indexOf(';'));
}
// 将base64解码
var bytes = atob(base64String);
//var bytes = base64;
var bytesCode = new ArrayBuffer(bytes.length);
// 转换为类型化数组
var byteArray = new Uint8Array(bytesCode);
// 将base64转换为ascii码
for (var i = 0; i < bytes.length; i++) {
byteArray[i] = bytes.charCodeAt(i);
}
// 生成Blob对象(文件对象)
return new Blob([bytesCode], { type: imgtype });
};
// 压缩图片
const compress = (file: any) => {
if (!file || !window.FileReader) return;
if (/^image/.test(file.type)) {
let reader = new FileReader();
reader.readAsDataURL(file);
return new Promise((resolve) => {
reader.onloadend = function () {
let result: any = reader.result;
let img: any = new Image();
img.src = result;
if (result.length <= 200 * 1024) {
let blob = convertBase64ToBlob(result);
let files = new File([blob], file.name, { type: blob.type });
resolve(files);
return;
}
img.onload = function () {
let canvas = document.createElement('canvas');
let ctx: any = canvas.getContext('2d');
let width = img.width;
let height = img.height;
canvas.width = width;
canvas.height = height;
// 铺底色
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, width, height);
//进行最小压缩
let ndata = canvas.toDataURL('image/jpeg', 0.3);
let blob = convertBase64ToBlob(ndata);
let files = new File([blob], file.name, { type: blob.type });
resolve(files);
};
};
});
}
};
const beforeDelete = async (file: any, detail: any) => {
const newArr = await delElbyIndex(state.uploadImages, detail.index);
state.uploadImages = newArr;
return true;
};
// 根据索引删除数组
const delElbyIndex = (arr: any, index: number) => {
for (let i = index, len = arr.length - 1; i <= len; i++) {
arr[i] = arr[i + 1];
arr.length = len;
}
return arr;
};
const goQa = () => {
router.push('/questionsAnswers');
};
</script>
<style lang="scss" scoped>
.feedback {
background: #000;
:deep(.right-btn) {
color: rgba(255, 255, 255, 0.5);
font-size: 12px;
}
.content {
padding: 12px 10px 70px 10px;
height: calc(100vh - 44px);
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding-bottom: calc(env(safe-area-inset-bottom) + 130px);
.problem-title {
font-size: 18px;
font-weight: 500;
line-height: 24px;
color: #fff;
padding: 0 5px;
}
.inputBox {
margin: 28px 0 0;
padding: 0 5px;
.title {
font-size: 18px;
font-weight: 500;
line-height: 24px;
color: #fff;
}
.textarea {
margin-top: 12px;
background: rgba(255, 255, 255, 0.05);
border-radius: 9px;
padding: 11px 12px;
}
:deep(.van-field__control) {
color: #fff;
font-size: 12px;
line-height: 18px;
}
:deep(.van-field__word-limit) {
color: rgba(255, 255, 255, 0.5);
font-size: 12px;
}
::placeholder {
color: rgba(255, 255, 255, 0.35);
font-size: 12px;
}
}
:deep(.loading) {
height: auto;
}
.feedbackBox {
margin-top: 12px;
width: 100%;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 12px 0;
padding: 0 5px;
box-sizing: border-box;
.item {
background: rgba(255, 255, 255, 0.1);
color: #fff;
border-radius: 3px;
height: 34px;
font-size: 12px;
line-height: 18px;
width: 79px;
box-sizing: border-box;
padding: 0;
}
.active {
background: #ffc000;
color: #1f2029;
border-radius: 3px;
}
}
.inputLabel {
width: 100%;
display: flex;
align-items: center;
border-bottom: solid 1px rgba(255, 255, 255, 0.06);
margin-top: 0;
padding: 12px 5px 12px;
box-sizing: border-box;
.label {
width: 80px;
font-size: 14px;
font-weight: 500;
line-height: 20px;
color: #fff;
flex-shrink: 0;
}
.input {
flex: 1;
:deep(.van-cell) {
background: transparent;
font-size: 12px;
padding: 0;
}
:deep(.van-field__control) {
color: #fff;
}
::placeholder {
color: rgba(255, 255, 255, 0.35);
}
}
}
.upload {
width: 100%;
margin-top: 28px;
padding: 0 5px;
box-sizing: border-box;
}
.info {
font-size: 12px;
color: rgba(255, 255, 255, 0.35);
line-height: 18px;
margin: 12px 5px 0;
}
}
}
.upload-title {
color: #fff;
font-size: 18px;
font-weight: 500;
line-height: 24px;
margin-bottom: 12px;
}
.uploadBox {
width: 111px;
height: 111px;
flex-direction: column;
background: rgba(255, 255, 255, 0.05);
border-radius: 9px;
border: 1px dashed #bdbdbd;
img {
width: 24px;
height: 24px;
margin-bottom: 4px;
}
font-size: 12px;
color: #999;
}
.previewDelete {
margin-top: 4px;
margin-right: 4px;
color: #000;
}
:deep(.van-uploader__preview) {
width: 111px;
height: 111px;
border-radius: 9px;
}
:deep(.van-image) {
width: 111px;
height: 111px;
border-radius: 9px;
box-sizing: border-box;
}
.submit {
height: 44px;
line-height: 44px;
text-align: center;
font-size: 16px;
font-weight: 500;
color: #1f2029;
background: #ffc000;
border-radius: 22px;
position: fixed;
bottom: calc(env(safe-area-inset-bottom) + 20px);
width: 264px;
left: 50%;
transform: translateX(-50%);
}
</style>
@@ -0,0 +1,104 @@
<template>
<!-- 列表数据 -->
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="sixGrid">
<HorizontalGameBox class="gameBox" :videoInfo="item" v-for="item in state.list" :key="item.id" />
</div>
</PullRefresh>
</template>
<script setup lang="ts">
import { reactive, nextTick } from 'vue';
import UserApi from '@/service/user';
// 列表数据项
const state = reactive({
list: <Types.Video.VInfosItem[]>[],
pageNumber: 1,
pageSize: 12,
});
// PullRefresh 参数
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 列表获取
const getList = async (type = '') => {
try {
let res = await UserApi.queryBuyVidList({
newsType: 'SEED_LINK',
pageNumber: state.pageNumber,
pageSize: state.pageSize,
});
const list = res.list || [];
moreParams.loading = false;
moreParams.refreshing = false;
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
// 数据重置
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
// 触底加载
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
// 下拉刷新
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
defineExpose({
reset,
onLoad,
onRefresh,
});
</script>
<style scoped lang="scss">
.sixGrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 8px;
grid-row-gap: 10px;
padding: 0 16px;
:deep() {
.gameBox {
width: 168px;
}
.videoBox {
width: 100%;
overflow: hidden;
.coverBox {
border-radius: 6px !important;
}
}
}
}
</style>
@@ -0,0 +1,118 @@
<template>
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="categoryContent">
<CategoryContent
:contentWrap="true"
:list="state.list"
:contentType="mediaType"
:coverDirection="coverDirection"
/>
</div>
</PullRefresh>
</template>
<script setup lang="ts">
import { reactive, nextTick, computed } from 'vue';
import CategoryContent from '@/views/acgModule/components/categoryContent.vue';
import UserApi from '@/service/user';
const props = defineProps({
mediaType: {
type: String,
default: 'image',
},
});
const coverDirection = computed(() => (props.mediaType === 'video' ? 'landscape' : 'portrait'));
const state = reactive({
list: <any>[],
pageNumber: 1,
pageSize: 12,
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
const getList = async (type = '') => {
try {
let res: any = await UserApi.mediaMybuy({
mediaType: props.mediaType,
pageNumber: state.pageNumber,
pageSize: state.pageSize,
});
const list = res.list || [];
moreParams.loading = false;
moreParams.refreshing = false;
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
defineExpose({
reset,
onLoad,
onRefresh,
});
</script>
<style scoped lang="scss">
.categoryContent {
padding: 12px 10px;
:deep() {
.category-content {
.list {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-column-gap: 5px;
grid-row-gap: 12px;
.item {
width: 100% !important;
overflow: hidden;
margin: 0 !important;
}
}
}
.category-content.landscape .list {
grid-template-columns: 1fr 1fr;
grid-column-gap: 7px;
}
}
}
</style>
@@ -0,0 +1,105 @@
<template>
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="sixGrid">
<HorizontalVideoBox
class="videoBox"
:videoInfo="item"
:videoList="state.list"
v-for="item in state.list"
:key="item.id"
/>
</div>
</PullRefresh>
</template>
<script setup lang="ts">
import { reactive, nextTick } from 'vue';
import UserApi from '@/service/user';
const state = reactive({
list: <Types.Video.VInfosItem[]>[],
pageNumber: 1,
pageSize: 12,
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
const getList = async (type = '') => {
try {
let res = await UserApi.queryBuyVidList({
newsType: 'SP',
pageNumber: state.pageNumber,
pageSize: state.pageSize,
});
const list = res.list || [];
moreParams.loading = false;
moreParams.refreshing = false;
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
defineExpose({
reset,
onLoad,
onRefresh,
});
</script>
<style scoped lang="scss">
.sixGrid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-column-gap: 7px;
grid-row-gap: 12px;
padding: 12px 10px;
:deep() {
.videoBox {
width: 100%;
overflow: hidden;
.coverBox {
border-radius: 6px !important;
height: 96px !important;
}
}
}
}
</style>
@@ -0,0 +1,85 @@
<template>
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="listBox" v-for="(item, index) in state.list" :key="index">
<Post :list="state.list" :postInfo="item" :currentIndex="index" />
</div>
</PullRefresh>
</template>
<script setup lang="ts">
import { reactive, nextTick } from 'vue';
import UserApi from '@/service/user';
const state = reactive({
list: <Types.Video.VInfosItem[]>[],
pageNumber: 1,
pageSize: 12,
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
const getList = async (type = '') => {
try {
let res = await UserApi.queryBuyVidList({
newsType: 'COVER',
pageNumber: state.pageNumber,
pageSize: state.pageSize,
});
const list = res.list || [];
moreParams.loading = false;
moreParams.refreshing = false;
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
defineExpose({
reset,
onLoad,
onRefresh,
});
</script>
<style scoped lang="scss">
.listBox {
margin: 0 10px;
}
</style>
@@ -0,0 +1,120 @@
<template>
<!-- 列表数据 -->
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="sixGrid">
<HorizontalVideoBox
class="videoBox"
:videoInfo="item"
:videoList="state.list"
v-for="item in state.list"
:key="item.id"
:isShort="true"
/>
</div>
</PullRefresh>
</template>
<script setup lang="ts">
import { reactive, nextTick } from 'vue';
import UserApi from '@/service/user';
// 列表数据项
const state = reactive({
list: <Types.Video.VInfosItem[]>[],
pageNumber: 1,
pageSize: 12,
});
// PullRefresh 参数
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 列表获取
const getList = async (type = '') => {
try {
let res: any = await UserApi.queryBuyVidList({
newsType: 'SHORT',
pageNumber: state.pageNumber,
pageSize: state.pageSize,
});
const list = res.list || [];
moreParams.loading = false;
moreParams.refreshing = false;
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
// 数据重置
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
// 触底加载
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
// 下拉刷新
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
defineExpose({
reset,
onLoad,
onRefresh,
});
</script>
<style scoped lang="scss">
:deep(.horizontalVideoBox .coverBox) {
height: 234px !important;
}
.sixGrid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-column-gap: 5px;
grid-row-gap: 12px;
padding: 12px 10px;
:deep() {
.videoBox {
width: 100%;
overflow: hidden;
.coverBox {
border-radius: 6px !important;
height: 148px !important;
}
.videoTitle {
height: auto !important;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1 !important;
-webkit-box-orient: vertical;
}
}
}
}
</style>
@@ -0,0 +1,120 @@
<template>
<!-- 列表数据 -->
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="theatres-list">
<HorizontalVideoBox
class="videoBox"
:videoInfo="item"
:videoList="state.list"
v-for="item in state.list"
:isShort="true"
:key="item.id"
:isPic="true"
/>
</div>
</PullRefresh>
</template>
<script setup lang="ts">
import { reactive, nextTick } from 'vue';
import UserApi from '@/service/user';
// 列表数据项
const state = reactive({
list: <Types.Video.VInfosItem[]>[],
pageNumber: 1,
pageSize: 12,
});
// PullRefresh 参数
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 列表获取
const getList = async (type = '') => {
try {
let res: any = await UserApi.queryBuyVidList({
newsType: 'PIC',
pageNumber: state.pageNumber,
pageSize: state.pageSize,
});
const list = res.list || [];
moreParams.loading = false;
moreParams.refreshing = false;
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
// 数据重置
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
// 触底加载
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
// 下拉刷新
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
defineExpose({
reset,
onLoad,
onRefresh,
});
</script>
<style scoped lang="scss">
:deep(.horizontalVideoBox .coverBox) {
height: 234px !important;
}
.theatres-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-column-gap: 5px;
grid-row-gap: 12px;
padding: 12px 10px;
:deep() {
.videoBox {
width: 100%;
overflow: hidden;
.coverBox {
border-radius: 6px !important;
height: 148px !important;
}
.videoTitle {
height: auto !important;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1 !important;
}
}
}
}
</style>
+159
View File
@@ -0,0 +1,159 @@
<template>
<div class="my-purchases overflow-y-auto" @touchstart.stop @touchmove.stop @touchend.stop>
<Header title="我的购买" />
<van-tabs
v-model:active="state.active"
background="#000"
title-active-color="#fff"
title-inactive-color="rgba(255, 255, 255, 0.5)"
line-height="3"
animated
swipeable
@change="tabChange"
>
<van-tab v-for="(item, index) in state.tabsList" :key="index" :title="item.title">
<div class="tab-content-auto overflow-y-auto" @scroll="onScroll" :id="`tabContent${index}`">
<component
v-if="item.comments"
:is="item.comments"
:key="item.name"
:mediaType="item.mediaType || null"
:mode="item.mode"
:ref="(el) => (childRefs[index] = el)"
/>
</div>
</van-tab>
</van-tabs>
</div>
</template>
<script lang="ts">
export default {
name: 'purchasedVideo',
};
</script>
<script setup lang="ts">
import { ref, reactive, markRaw, onActivated } from 'vue';
import MovieList from './components/MovieList.vue';
import ShortnessVideoList from './components/ShortnessVideoList.vue';
import MangaList from './components/MangaList.vue';
import PostList from './components/PostList.vue';
import TheatresList from './components/TheatresList.vue';
import MineDramaList from '../components/MineDramaList.vue';
const childRefs = ref<any[]>([]);
const state = reactive({
active: 0,
tabsList: [
{
title: '视频',
name: 0,
comments: markRaw(MovieList),
scrollTop: 0,
},
{
title: '抖音',
name: 1,
comments: markRaw(ShortnessVideoList),
scrollTop: 0,
},
{
title: '短剧',
name: 'drama',
comments: markRaw(MineDramaList),
scrollTop: 0,
mode: 'purchased',
},
{
title: '帖子',
name: 2,
comments: markRaw(PostList),
scrollTop: 0,
},
{
title: '动漫',
name: 4,
comments: markRaw(MangaList),
scrollTop: 0,
mediaType: 'video',
},
{
title: '漫画',
name: 3,
comments: markRaw(MangaList),
scrollTop: 0,
mediaType: 'image',
},
{
title: '图集',
name: 5,
comments: markRaw(TheatresList),
scrollTop: 0,
},
{
title: '小说',
name: 6,
comments: markRaw(MangaList),
scrollTop: 0,
mediaType: 'text',
},
],
});
const tabChange = () => {
setScrollTop(state.tabsList[state.active].scrollTop);
};
const onScroll = (event: any) => {
state.tabsList[state.active].scrollTop = event.target.scrollTop;
};
onActivated(() => {
setScrollTop(state.tabsList[state.active].scrollTop);
});
const setScrollTop = (top: number) => {
const dom = document.getElementById(`tabContent${state.active}`);
if (dom) {
dom.scrollTop = top;
}
};
</script>
<style scoped lang="scss">
.my-purchases {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
:deep() {
.van-tabs {
flex: 1 0;
height: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
}
.van-tabs__line {
bottom: 7px;
width: 13px;
height: 3px;
border-radius: 12px;
background: linear-gradient(90deg, rgba(255, 192, 0, 0) 0%, #ffc000 100%);
}
.van-tabs__content {
flex: 1 0;
height: 0;
overflow: hidden;
}
.van-tab__panel {
height: 100%;
width: 100%;
overflow: hidden;
}
.tab-content-auto {
height: 100%;
width: 100%;
}
}
}
</style>
@@ -0,0 +1,109 @@
<template>
<div class="qualityApplication">
<Header title="应用中心" />
<div class="content overflow-y-auto">
<div class="pageBox">
<div class="itemBox" v-for="(item, index) in state.appList" :key="index" @click="download(item)">
<div class="picBox">
<DecryptImg :src="item.cover" />
</div>
<div class="name">{{ item.title }}</div>
<div class="info" v-if="item.description">{{ item.description }}</div>
<div class="download">下载</div>
</div>
</div>
<div class="size"></div>
</div>
</div>
</template>
<script lang="ts" setup>
import { getAdItem } from '@/utils/getConfig';
import { AdType } from '@/enums/app';
import { nextTick, reactive } from 'vue';
import { compatibleAppStoreJump, reportAdClick } from '@/utils/index';
const state = reactive({
appList: <Array<Types.App.AdsInfoListItem>>[],
});
/**
* 下载应用
* @param item 精品应用广告
*/
const download = (item: Types.App.AdsInfoListItem) => {
compatibleAppStoreJump(item.href);
reportAdClick(item.id);
};
nextTick(async () => {
const appList = getAdItem(AdType.QualityApplication);
state.appList = appList;
});
</script>
<style lang="scss" scoped>
.qualityApplication {
width: 100%;
height: 100%;
.content {
height: calc(100vh - 44px);
.pageBox {
display: grid;
grid-template-columns: repeat(3, 31%);
justify-content: space-between;
padding: 20px 12px;
.itemBox {
margin-bottom: 20px;
display: flex;
flex-direction: column;
align-items: center;
.picBox {
width: 100%;
:deep(.van-image__img) {
border-radius: 20px;
}
}
.name {
font-size: 15px;
// color: $themeColors;
color: #ffffff;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 8px 0 5px 0;
}
.info {
font-size: 13px;
color: $textColors;
text-align: center;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.download {
width: 65px;
height: 28px;
// background: linear-gradient(to bottom, rgb( 247 ,138 ,171), rgb( 116, 57 ,138));
background: linear-gradient(180deg, rgb(253, 143, 97), rgb(248, 75, 98), rgb(116, 57, 138));
border-radius: 20px;
font-size: 14px;
color: #ffffff;
display: flex;
justify-content: center;
align-items: center;
}
}
}
.size {
width: 100%;
height: 100px;
}
}
}
</style>
@@ -0,0 +1,206 @@
<template>
<div class="help-center">
<Header class="header" title="常见问题" />
<div v-if="!state.loading" class="help-center-content overflow-y-auto">
<van-collapse v-model="activeNames" :border="false">
<van-collapse-item
v-for="item in typeList"
:key="item.type"
:name="item.type"
:border="false"
>
<template #title>
<div class="qa-title">{{ item.name }}</div>
</template>
<div v-if="item.type === 5" class="qa-content">
<p v-for="(line, index) in contactLines" :key="index" class="qa-contact-line">
{{ line.label }}{{ line.value }}
</p>
</div>
<div v-else class="qa-content" v-html="item.content"></div>
</van-collapse-item>
</van-collapse>
</div>
<Loading v-else />
</div>
</template>
<script lang="ts">
export default {
name: 'Help',
};
</script>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue';
import { useConfigStore } from '@/stores/config';
const configStore = useConfigStore();
const activeNames = ref<number[]>([]);
const state = reactive({
loading: false,
});
const getSourceUrl = (type: string) => {
const list = configStore.appConfig?.sourceList || [];
const matched = list.find((item) => item.isActive !== false && item.type === type);
const domains = matched?.domain || [];
return domains.find((item) => item.status === 0)?.url || domains[0]?.url || '';
};
const contactLines = computed(() => {
const lines: Array<{ label: string; value: string }> = [];
const telegram = getSourceUrl('TELEGRAM');
if (telegram) lines.push({ label: '商务合作TG:', value: telegram });
const email = getSourceUrl('Mailbox');
if (email) lines.push({ label: '官方邮箱:', value: email });
const guide = getSourceUrl('GUIDE') || configStore.sourceInfo?.GUIDE || '';
if (guide) lines.push({ label: '永久下载地址:', value: guide });
return lines;
});
const typeList = computed(() => {
const list = [
{
name: '描述文件安装失败?',
type: 1,
content:
'若提示【新的MDM有效负载与旧的有效负载不匹配】,请移除移动设备管理步骤:【设置-通用-设备管理-移动设备管理-移除管理】',
},
{
name: '怎么找回账号?',
type: 2,
content:
'本平台登录会自动创建账号,需保存账号凭证或绑定手机号码,才能记录之前的账号信息。受行业限制,APP无法正常使用时需升级,未绑定手机号码会导致账号信息丢失。<br/>请及时绑定手机号码或保存账号凭证,以免VIP信息丢失,造成巨大财产损失!<br/>账号丢失的用户可在 『我的』页面-账号找回,原账号的VIP信息会转移至新账号上。',
},
{
name: '怎么支付不成功?',
type: 3,
content:
'1.因超时支付无法到账,请重新发起。<br/>2.每天发起支付不能超过5次,连续发起且未支付,账号可能被加入黑名单。<br/>3.支付通道在夜间比较忙碌,尝试多次发起,后台会为您自动切换不同支付通道。<br/>4.若充值成功,用户权益通常会在数分钟内到账,请刷新APP或重启。',
},
{
name: '收到手机报毒提醒?',
type: 4,
content:
'本平台有主要收益为广告赞助,且保证APP安全无毒,因平台主要展示内容为色情属于特殊行业,某些杀毒软件会误报毒提醒,如遇此类提醒请忽略继续使用。',
},
{
name: '官方联系方式?',
type: 5,
content: '',
},
];
return list.filter((item) => item.type !== 5 || contactLines.value.length > 0);
});
</script>
<style lang="scss" scoped>
.help-center {
background-color: #000;
height: 100%;
position: relative;
:deep(.header) {
height: 48px;
.title {
font-size: 18px;
font-weight: 600;
line-height: 24px;
color: rgba(255, 255, 255, 0.9);
}
}
.help-center-content {
padding: 12px 16px 40px;
height: calc(100vh - 48px);
box-sizing: border-box;
}
:deep(.van-collapse) {
background: transparent;
}
:deep(.van-collapse-item) {
margin-bottom: 12px;
background: transparent;
&:last-child {
margin-bottom: 0;
}
&::after {
display: none;
}
}
:deep(.van-collapse-item__title) {
padding: 0 0 12px;
align-items: center;
background: transparent;
border-bottom: 0.5px solid rgba(255, 255, 255, 0.08);
&::after {
display: none;
}
.van-cell__title {
flex: 1;
overflow: visible;
}
.van-cell__right-icon {
margin-left: 12px;
}
}
:deep(.van-icon-arrow) {
color: #fff;
}
:deep(.van-collapse-item--expanded) {
.van-collapse-item__title {
border-bottom-color: transparent;
}
}
:deep(.van-collapse-item__wrapper) {
background: transparent;
}
:deep(.van-collapse-item__content) {
padding: 0 0 12px;
background: transparent;
border-bottom: 0.5px solid rgba(255, 255, 255, 0.08);
}
:deep(.van-collapse-item:last-child) {
.van-collapse-item__title,
.van-collapse-item__content {
border-bottom: none;
}
}
.qa-title {
font-size: 14px;
font-weight: 500;
line-height: 20px;
color: rgba(255, 255, 255, 0.9);
}
.qa-content {
font-size: 14px;
font-weight: 400;
line-height: 30px;
color: #989898;
text-align: justify;
word-break: break-word;
}
.qa-contact-line {
margin: 0;
word-break: break-all;
}
}
</style>
+197
View File
@@ -0,0 +1,197 @@
<template>
<div class="recharge-record">
<Header title="充值记录" />
<div class="main overflow-y-auto">
<template v-if="!state.loading">
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="record-box" v-for="item in state.list" :key="item.orderId">
<div class="record-info">
<div class="record-header flex-center-between">
<div class="record-header-left">
<div class="record-title">购买{{ item.productName }}</div>
</div>
</div>
<!-- <p>支付方式:{{ item.payType }}</p> -->
<p class="grey order-item">
账单编号:{{ item.orderId }}
<SvgIcon name="copy" class="copy" size="18px" @click="copyLink(item.orderId)" />
</p>
<p class="grey status">
状态:
<span v-if="item.status == 1">进行中</span>
<span v-else-if="item.status == 2">支付失败</span>
<span v-else-if="item.status == 3">购买成功</span>
<span v-else-if="item.status == 4">已退款</span>
<span v-else>支付失败</span>
</p>
<p class="grey small">{{ dateFormat(item.paymentAt) }}</p>
</div>
<div class="line"></div>
<!-- <div class="record-desc" @click="openCertificate(item)">我已充值,提交支付凭证&nbsp;&nbsp;>></div> -->
</div>
</PullRefresh>
</template>
<Loading v-else />
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import MineApi from '@/service/mine';
import { dateFormat } from '@/utils';
import { showToast } from 'vant';
import toClipboard from '@/utils/clipboard';
const state = reactive({
list: <Array<Types.Recharge.RechargeRecord>>[],
pageNumber: 1,
pageSize: 15,
loading: true,
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
const getList = async (type: any) => {
let req = {
pageNumber: state.pageNumber,
pageSize: state.pageSize,
};
try {
let res: any = await MineApi.rechargeRecord(req);
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (type === 'refresh') {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1 && state.list.length == 0) {
moreParams.isNoData = true;
}
if (!res.hasNext) {
moreParams.finished = true;
}
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
getList('');
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList(null);
};
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
getList('refresh');
};
/**
* 复制到剪切板
* @param cod
*/
const copyLink = async (cod: string) => {
try {
if (!cod) {
// showToast('复制失败');
return;
}
await toClipboard(cod); //实现复制
showToast('订单号已经复制');
} catch (e: any) {
console.log(e);
}
};
</script>
<style lang="scss" scoped>
.recharge-record {
height: 100%;
background: #0f0f0f;
:deep(.header) {
background: #000 !important;
}
.main {
height: calc(100% - 44px);
padding: 14px 16px;
box-sizing: border-box;
.record-box {
margin-bottom: 0;
.record-info {
padding: 0;
display: flex;
flex-direction: column;
gap: 2px;
.record-header {
margin-bottom: 0;
.record-header-left {
display: flex;
align-items: center;
.record-title {
font-size: 14px;
font-weight: 500;
line-height: 20px;
color: #fff;
}
}
}
p {
font-size: 12px;
letter-spacing: 0;
line-height: 18px;
}
.grey {
font-weight: 400;
color: #989898;
}
.small {
font-size: 12px;
color: #525252;
}
.order-item {
display: flex;
align-items: center;
color: #989898;
.copy {
margin-left: 6px;
color: rgba(255, 255, 255, 0.85);
}
}
.status {
margin: 0;
color: #989898;
span {
color: #ffc000;
}
}
}
.line {
background: rgba(255, 255, 255, 0.08);
width: 100%;
height: 1px;
margin: 12px 0;
transform: scaleY(0.5);
}
.record-desc {
height: 45px;
background-color: white;
margin-top: 6px;
border-radius: 2px;
text-align: center;
line-height: 45px;
color: $greyDeepColor;
font-size: 14px;
}
}
}
}
</style>
+244
View File
@@ -0,0 +1,244 @@
<template>
<div class="revenueDetails">
<Header title="业绩明细" />
<div class="content overflow-y-auto">
<PullRefresh v-if="!state.loading" :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div v-for="(item, index) in state.list" :key="index">
<!-- <template v-if="item.rechargeUser && item.rechargeUser.hasOwnProperty('uid')"> -->
<div class="listBox">
<div class="flex">
<!-- <div class="left">
<lazyImg class="avatar" :src="item.rechargeUser.portrait" round :key="item.rechargeUser.portrait" />
</div> -->
<div class="right">
<h2>
{{ item.rechargeUser.name }}
<!-- <span>{{ item.tranType || '-' }}</span> -->
</h2>
<!-- <p>{{ item.desc || '-' }}</p> -->
<h3 class="flex-center-between">
<span>{{ dateFormat(item.createdAt) }}</span>
</h3>
</div>
<div class="gold">+ {{ item.actualAmount / 10 || 0 }}金币</div>
</div>
</div>
<!-- </template> -->
<!-- <template v-else>
<div class="detailItem">
<div>
<div class="detailInfo">
<div class="detailTitle">{{ item.tranType }}</div>
<div class="timer">{{ dateFormat(item.createdAt) }}</div>
</div>
<div class="contentBox">
<div class="detailDesc">{{ item.desc }}</div>
<div class="gold">+{{ item.actualAmount }}金币</div>
</div>
</div>
<div class="border"></div>
</div>
</template> -->
</div>
</PullRefresh>
<Loading v-else />
</div>
</div>
</template>
<script lang="ts" setup>
import { dateFormat } from '@/utils/index';
import { reactive, nextTick } from 'vue';
import UserApi from '@/service/user';
const state = reactive({
loading: true,
pageNumber: 1,
pageSize: 10,
list: <any>[],
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 获取收益列表
const getList = async (type: any) => {
try {
let req = {
pageNumber: state.pageNumber,
pageSize: state.pageSize,
startTime: '',
endTime: '',
};
let res: any = await UserApi.getIncomList(req);
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (type === 'refresh') {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1 && state.list.length == 0) {
moreParams.isNoData = true;
}
if (!res.hasNext) {
moreParams.finished = true;
}
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
nextTick(() => {
getList('');
});
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList(null);
};
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
moreParams.isNoData = false;
getList('refresh');
};
</script>
<style lang="scss" scoped>
.revenueDetails {
height: 100%;
background: #000;
.content {
width: 100%;
height: calc(100vh - 44px);
padding: 0 16px;
.listBox {
width: 100%;
padding: 16px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
.flex {
align-items: center;
justify-content: space-between;
}
.left {
width: 54px;
.avatar {
width: 42px;
height: 42px;
margin-right: 12px;
:deep(.loading) {
background-size: 28px 21px;
}
}
}
.right {
flex: 1;
overflow: hidden;
min-width: 0;
h2 {
font-size: 14px;
font-weight: 500;
color: #fff;
margin-bottom: 8px;
span {
font-size: 12px;
padding-left: 10px;
box-sizing: border-box;
}
}
p {
color: #86868f;
font-size: 12px;
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin: 12px 0;
}
h3 {
font-size: 12px;
span {
color: rgba(255, 255, 255, 0.45);
font-size: 12px;
}
}
}
}
.detailItem {
margin: 0 0 20px 0;
.detailInfo {
margin: 22px 0 17px;
display: flex;
align-items: center;
justify-content: space-between;
.detailTitle {
font-size: 16px;
font-weight: 600;
color: #fff;
}
.avatarImg {
width: 43px;
height: 43px;
}
.detailOpt {
flex: 2;
margin: 0 10px;
span {
opacity: 0.6;
}
}
.timer {
font-size: 14px;
color: rgba(255, 255, 255, 0.45);
}
}
.contentBox {
margin-bottom: 22px;
display: flex;
justify-content: space-between;
.detailDesc {
width: 215px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 14px;
}
}
}
.gold {
flex-shrink: 0;
margin-left: 12px;
display: flex;
align-items: center;
font-size: 14px;
color: #ffc000;
white-space: nowrap;
}
.border {
width: 100%;
height: 1px;
margin: 10px 0 0 0;
background: rgba(255, 255, 255, 0.1);
}
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More