初始化导入 hjsq H5 项目代码
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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)">我已充值,提交支付凭证 >></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>
|
||||
@@ -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
Reference in New Issue
Block a user