首次提交

This commit is contained in:
Your Name
2025-05-29 14:25:13 +08:00
parent 75bbcc04aa
commit 86cfcc5af1
18 changed files with 2960 additions and 34 deletions

View File

@@ -1,14 +1,32 @@
<script>
export default {
onLaunch: function () {
console.log('App Launch')
console.log('App Launch');
// 检查本地存储中是否有同意协议的记录
try {
const hasAgreed = uni.getStorageSync('hasAgreedToTerms');
// 如果没有同意过,显示协议页面
if (hasAgreed !== 'true') {
// 使用setTimeout避免可能的导航冲突
setTimeout(() => {
uni.navigateTo({
url: '/pages/agreement/agreement'
});
}, 500);
}
} catch (e) {
console.error('Check agreement status error:', e);
}
},
onShow: function () {
console.log('App Show')
},
onHide: function () {
console.log('App Hide')
},
}
}
</script>

View File

@@ -0,0 +1,168 @@
<template>
<view class="agreement-modal" v-if="visible">
<view class="agreement-content">
<view class="agreement-header">
<text class="agreement-title">用户须知</text>
</view>
<scroll-view class="agreement-body" scroll-y show-scrollbar="true">
<view class="agreement-inner">
<view class="agreement-section">
<view class="section-title">1 年龄与身份</view>
<text class="section-item">1.1 本人已年满 18 周岁具备完全民事行为能力</text>
<text class="section-item">1.2 本人承诺不得让未成年人接触或使用本平台内容及成人用品</text>
</view>
<view class="agreement-section">
<view class="section-title">2 AIGC 生成规范</view>
<text class="section-item">2.1 不得生成发布或传播淫秽色情含未成年人或性交易赌博暴力恐怖歧视仇恨危害国家安全违法诈骗等信息</text>
<text class="section-item">2.2 不得输出侵犯知识产权肖像权隐私权或其他合法权益的内容</text>
<text class="section-item">2.3 应自行甄别生成内容的真实性与准确性不得用生成内容冒充官方信息或误导他人</text>
<text class="section-item">2.4 不得上传或指令模型生成他人身份证号联系方式人脸图像等可识别个人信息除非已获得合法授权</text>
<text class="section-item highlight-item">2.5 不得尝试逆向工程导出模型权重或利用注入攻击规避审查</text>
<text class="section-item highlight-item">2.6 发现输出违规或存在安全隐患时应立即停止使用并通过平台渠道反馈</text>
</view>
<view class="agreement-section">
<view class="section-title">3 其他遵守事项</view>
<text class="section-item">3.1 成人用品仅供个人私密场景使用应按说明书清洁消毒并适度使用如有疾病史应先咨询医生</text>
<text class="section-item">3.2 违反本须知或AIGC 平台及成人用品中国境内用户使用规范将导致警告限制功能封禁账号或配合监管调查</text>
<text class="section-item">3.3 若不同意本须知请立即退出并停止使用本平台及相关产品</text>
</view>
</view>
</scroll-view>
<view class="agreement-footer">
<button class="btn-disagree" @click="handleDisagree">暂不进入</button>
<button class="btn-agree" @click="handleAgree">确定</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref, defineEmits } from 'vue';
const props = defineProps({
visible: {
type: Boolean,
default: false
}
});
const emit = defineEmits(['agree', 'disagree']);
// 处理同意事件
const handleAgree = () => {
emit('agree');
};
// 处理不同意事件
const handleDisagree = () => {
emit('disagree');
};
</script>
<style scoped>
.agreement-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.agreement-content {
width: 80%;
max-width: 600rpx;
background: #fff;
border-radius: 24rpx;
overflow: hidden;
display: flex;
flex-direction: column;
max-height: 88vh;
}
.agreement-header {
padding: 20rpx 0;
text-align: center;
border-bottom: 1rpx solid #EEEEEE;
}
.agreement-title {
font-size: 32rpx;
font-weight: 600;
color: #FF9800;
}
.agreement-body {
max-height: 70vh;
box-sizing: border-box;
}
.agreement-inner {
padding: 20rpx 35rpx 20rpx 30rpx;
}
.agreement-section {
margin-bottom: 18rpx;
display: flex;
flex-direction: column;
}
.section-title {
font-size: 28rpx;
font-weight: 600;
margin-bottom: 10rpx;
color: #333333;
letter-spacing: 2rpx;
}
.section-item {
font-size: 22rpx;
color: #666666;
line-height: 1.8;
margin-bottom: 12rpx;
letter-spacing: 3.5rpx;
display: block;
text-align: left;
white-space: pre-wrap;
text-justify: inter-character;
max-width: 520rpx;
}
.highlight-item {
font-weight: 500;
color: #333333;
}
.agreement-footer {
display: flex;
border-top: 1rpx solid #EEEEEE;
margin-top: auto;
}
.btn-disagree, .btn-agree {
flex: 1;
height: 86rpx;
line-height: 86rpx;
text-align: center;
font-size: 30rpx;
margin: 0;
border-radius: 0;
}
.btn-disagree {
background: #FFFFFF;
color: #999999;
border-right: 1rpx solid #EEEEEE;
}
.btn-agree {
background: #FF9800;
color: #FFFFFF;
}
</style>

View File

@@ -0,0 +1,90 @@
<template>
<view class="user-auth">
<button
v-if="!userStore.isLoggedIn"
class="auth-btn"
:class="[type]"
open-type="getUserProfile"
@click="handleLogin"
>
<slot>{{ btnText }}</slot>
</button>
<slot v-else name="logged"></slot>
</view>
</template>
<script setup>
import { computed } from 'vue';
import { useUserStore } from '@/stores/user.js';
// Props 定义
const props = defineProps({
// 按钮类型: primary, default, plain
type: {
type: String,
default: 'primary'
},
// 按钮文本
text: {
type: String,
default: '微信一键登录'
}
});
// 事件
const emits = defineEmits(['login-success', 'login-fail']);
// 状态
const userStore = useUserStore();
const btnText = computed(() => props.text);
// 登录处理
const handleLogin = async () => {
try {
await userStore.login();
uni.showToast({
title: '登录成功',
icon: 'success'
});
emits('login-success');
} catch (error) {
console.error('Login failed:', error);
uni.showToast({
title: '登录失败',
icon: 'none'
});
emits('login-fail', error);
}
};
</script>
<style scoped>
.user-auth {
display: inline-block;
}
.auth-btn {
border: none;
font-size: 28rpx;
border-radius: 40rpx;
padding: 0 30rpx;
height: 70rpx;
line-height: 70rpx;
}
.primary {
background-color: #07c160;
color: #ffffff;
}
.default {
background-color: #f0f0f0;
color: #333333;
}
.plain {
background-color: transparent;
color: #07c160;
border: 1px solid #07c160;
}
</style>

View File

@@ -1,9 +1,15 @@
import {
createSSRApp
} from "vue";
import { createPinia } from 'pinia';
import App from "./App.vue";
export function createApp() {
const app = createSSRApp(App);
const pinia = createPinia();
app.use(pinia);
return {
app,
};

View File

@@ -1,9 +1,43 @@
{
"pages": [ //pages数组中第一项表示应用启动页参考https://uniapp.dcloud.io/collocation/pages
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "uni-app"
"navigationStyle": "custom"
}
},
{
"path": "pages/mine/mine",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/create/create",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/script/editor",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/voice/clone",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/agreement/agreement",
"style": {
"navigationStyle": "custom",
"disableScroll": true,
"app-plus": {
"popGesture": "none"
}
}
}
],
@@ -12,5 +46,25 @@
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"tabBar": {
"color": "#999999",
"selectedColor": "#ff9800",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/index/index",
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/home_selected.png",
"text": "首页"
},
{
"pagePath": "pages/mine/mine",
"iconPath": "static/tabbar/mine.png",
"selectedIconPath": "static/tabbar/mine_selected.png",
"text": "我的"
}
]
}
}

View File

@@ -0,0 +1,59 @@
<template>
<view class="container">
<user-agreement
:visible="true"
@agree="handleAgree"
@disagree="handleDisagree"
/>
</view>
</template>
<script setup>
import UserAgreement from '@/components/UserAgreement.vue';
// 处理同意事件
const handleAgree = () => {
try {
// 保存到本地存储
uni.setStorageSync('hasAgreedToTerms', 'true');
// 返回上一页或首页
uni.navigateBack({
fail: () => {
uni.switchTab({
url: '/pages/index/index'
});
}
});
} catch (e) {
console.error('Save agreement status error:', e);
// 发生错误时也返回首页
uni.switchTab({
url: '/pages/index/index'
});
}
};
// 处理不同意事件
const handleDisagree = () => {
uni.showModal({
title: '提示',
content: '您需要同意用户须知才能继续使用本应用',
showCancel: false,
success: () => {
// H5环境下返回首页
uni.switchTab({
url: '/pages/index/index'
});
}
});
};
</script>
<style>
.container {
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
}
</style>

155
src/pages/create/create.vue Normal file
View File

@@ -0,0 +1,155 @@
<template>
<view class="container">
<!-- 顶部自定义导航栏 -->
<view class="custom-navbar fixed-navbar">
<view class="navbar-left" @click="goBack">
<text class="back-icon"></text>
</view>
<text class="navbar-title">创建</text>
<view class="navbar-right"></view>
</view>
<!-- 创建选项区域 -->
<view class="create-options">
<view class="option-card" @click="goToScriptEditor">
<view class="option-icon">📝</view>
<view class="option-content">
<text class="option-title">新建剧本</text>
<text class="option-desc">创建自定义剧本和对话</text>
</view>
</view>
<view class="option-card" @click="goToVoiceClone">
<view class="option-icon">🎙</view>
<view class="option-content">
<text class="option-title">克隆声音</text>
<text class="option-desc">定制个性化语音</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { useUserStore } from '@/stores/user.js';
// 初始化用户状态
const userStore = useUserStore();
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
// 跳转到剧本编辑页
const goToScriptEditor = () => {
// 检查是否已登录
if (!userStore.isLoggedIn) {
showLoginTip();
return;
}
uni.navigateTo({
url: '/pages/script/editor'
});
};
// 跳转到声音克隆页
const goToVoiceClone = () => {
// 检查是否已登录
if (!userStore.isLoggedIn) {
showLoginTip();
return;
}
uni.navigateTo({
url: '/pages/voice/clone'
});
};
// 显示登录提示
const showLoginTip = () => {
uni.showModal({
title: '提示',
content: '请先登录后再操作',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
uni.switchTab({
url: '/pages/mine/mine'
});
}
}
});
};
</script>
<style scoped>
.fixed-navbar {
position: fixed;
top: 0;
left: 0;
width: 100vw;
z-index: 100;
}
.container {
background: #fff9f2;
min-height: 100vh;
padding-top: 100rpx;
padding-bottom: 40rpx;
}
.custom-navbar {
height: 100rpx;
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(90deg, #ffe5c2, #fff6e5);
padding: 0 30rpx;
}
.navbar-left, .navbar-right {
width: 60rpx;
display: flex;
align-items: center;
}
.back-icon {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.navbar-title {
color: #333;
font-weight: bold;
font-size: 38rpx;
letter-spacing: 2rpx;
}
/* 创建选项区域 */
.create-options {
padding: 40rpx 30rpx;
}
.option-card {
display: flex;
align-items: center;
background: #fff;
border-radius: 20rpx;
padding: 40rpx 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
}
.option-icon {
font-size: 60rpx;
margin-right: 30rpx;
}
.option-content {
flex: 1;
}
.option-title {
font-size: 34rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
.option-desc {
font-size: 28rpx;
color: #999;
}
</style>

View File

@@ -1,48 +1,320 @@
<template>
<view class="content">
<image class="logo" src="/static/logo.png"></image>
<view class="text-area">
<text class="title">{{ title }}</text>
<view class="container">
<!-- 顶部自定义导航栏固定在最上方 -->
<view class="custom-navbar fixed-navbar">
<text class="navbar-title">TEST玩具</text>
</view>
<!-- 卡片区 -->
<view class="card-row">
<view class="card create-card" @click="goToCreate">
<view class="card-icon create-icon">+</view>
<text class="card-title">创建</text>
</view>
<view class="card connect-card">
<view class="card-icon connect-icon">🌐</view>
<text class="card-title bold">连接设备</text>
<text class="card-sn">SN: xxxxxxxx</text>
</view>
</view>
<!-- 二级Tab -->
<view class="tab-row">
<view :class="['tab-btn', tabActive === 'drama' ? 'active' : '']" @tap="tabActive = 'drama'">剧情</view>
<view :class="['tab-btn', tabActive === 'chat' ? 'active' : '']" @tap="tabActive = 'chat'">聊天</view>
</view>
<!-- 瀑布流卡片区 -->
<view class="waterfall">
<block v-for="(item, idx) in currentList" :key="item.id">
<view class="waterfall-card">
<image class="cover" :src="item.cover" mode="aspectFill" />
<view class="tag">{{ item.tag }}</view>
<view class="content-area">
<view class="title">{{ item.title }}</view>
<view class="card-bottom">
<button v-if="userStore.isLoggedIn" class="use-btn" @click="handleUse(item)">去使用</button>
<button v-else class="use-btn login-required" @click="showLoginTip">去使用</button>
</view>
</view>
</view>
</block>
</view>
<!-- 登录提示弹窗 -->
<view class="login-modal" v-if="showLoginModal">
<view class="login-modal-content">
<view class="login-modal-title">提示</view>
<view class="login-modal-text">请先登录后再操作</view>
<view class="login-modal-btns">
<button class="modal-btn cancel" @click="showLoginModal = false">取消</button>
<button class="modal-btn confirm" @click="goToLogin">去登录</button>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
title: 'Hello',
}
},
onLoad() {},
methods: {},
}
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useUserStore } from '@/stores/user.js';
// 状态管理
const userStore = useUserStore();
const tabActive = ref('drama');
const showLoginModal = ref(false);
// 数据
const dramaList = ref([
{ id: 1, cover: '/static/drama1.jpg', tag: '台词腔', title: '白领女友的温柔早安' },
{ id: 2, cover: '/static/drama2.jpg', tag: '人妻', title: '豪门女主的秘密生活' },
{ id: 3, cover: '/static/drama3.jpg', tag: '外卖媛', title: '外卖小姐姐的贴心问候' },
{ id: 4, cover: '/static/drama4.jpg', tag: '病娇', title: '因爱执仗的少女' },
]);
const chatList = ref([
{ id: 5, cover: '/static/chat1.jpg', tag: '萌妹', title: '可爱萌妹陪你聊' },
{ id: 6, cover: '/static/chat2.jpg', tag: '御姐', title: '知性御姐的温柔夜话' },
{ id: 7, cover: '/static/chat3.jpg', tag: '萝莉', title: '萝莉的童真世界' },
{ id: 8, cover: '/static/chat4.jpg', tag: '男友', title: '贴心男友的陪伴' },
]);
// 计算属性
const currentList = computed(() => {
return tabActive.value === 'drama' ? dramaList.value : chatList.value;
});
// 生命周期
onMounted(() => {
userStore.init();
});
// 方法
const handleUse = (item) => {
console.log('使用角色:', item);
uni.showToast({
title: `正在加载: ${item.title}`,
icon: 'none'
});
// 这里添加跳转到详情页或使用页面的逻辑
};
const showLoginTip = () => {
showLoginModal.value = true;
};
const goToLogin = () => {
showLoginModal.value = false;
uni.switchTab({
url: '/pages/mine/mine'
});
};
const goToCreate = () => {
uni.navigateTo({
url: '/pages/create/create'
});
};
</script>
<style>
.content {
<style scoped>
.fixed-navbar {
position: fixed;
top: 0;
left: 0;
width: 100vw;
z-index: 100;
}
.container {
background: #fff;
min-height: 100vh;
padding-bottom: 120rpx;
padding-top: 100rpx;
}
.custom-navbar {
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(90deg, #ffe5c2, #fff6e5);
font-weight: bold;
font-size: 38rpx;
letter-spacing: 2rpx;
}
.navbar-title {
color: #333;
}
.card-row {
display: flex;
justify-content: space-between;
margin: 32rpx 24rpx 0 24rpx;
}
.card {
flex: 1;
background: #fff;
border-radius: 18rpx;
box-shadow: 0 2rpx 12rpx #f5e6d6;
margin: 0 8rpx;
padding: 24rpx 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.logo {
height: 200rpx;
width: 200rpx;
margin-top: 200rpx;
margin-left: auto;
margin-right: auto;
margin-bottom: 50rpx;
.create-icon {
font-size: 48rpx;
color: #ff9800;
}
.text-area {
.connect-icon {
font-size: 40rpx;
color: #ff9800;
}
.card-title {
margin-top: 8rpx;
font-size: 30rpx;
}
.bold {
font-weight: bold;
}
.card-sn {
font-size: 22rpx;
color: #bbb;
margin-top: 4rpx;
}
.tab-row {
display: flex;
margin: 32rpx 24rpx 0 24rpx;
border-bottom: 2rpx solid #f5e6d6;
}
.tab-btn {
flex: 1;
text-align: center;
font-size: 32rpx;
padding: 18rpx 0;
color: #bbb;
font-weight: 500;
border-bottom: 4rpx solid transparent;
}
.tab-btn.active {
color: #ff9800;
border-bottom: 4rpx solid #ff9800;
font-weight: bold;
}
.waterfall {
display: flex;
flex-wrap: wrap;
margin: 24rpx 12rpx 0 12rpx;
}
.waterfall-card {
width: 46%;
margin: 2%;
background: #fff;
border-radius: 18rpx;
box-shadow: 0 2rpx 12rpx #f5e6d6;
position: relative;
overflow: hidden;
margin-bottom: 24rpx;
display: flex;
flex-direction: column;
}
.cover {
width: 100%;
height: 220rpx;
object-fit: cover;
border-radius: 18rpx 18rpx 0 0;
}
.tag {
position: absolute;
top: 12rpx;
left: 12rpx;
background: #ff9800;
color: #fff;
font-size: 22rpx;
border-radius: 8rpx;
padding: 4rpx 14rpx;
z-index: 2;
}
.content-area {
padding: 16rpx;
display: flex;
flex-direction: column;
}
.title {
color: #333;
font-size: 28rpx;
margin-bottom: 12rpx;
line-height: 1.3;
font-weight: 500;
}
.card-bottom {
display: flex;
justify-content: center;
align-items: center;
margin-top: 8rpx;
}
.use-btn {
background: #ff9800;
color: #fff;
border-radius: 24rpx;
font-size: 28rpx;
padding: 8rpx 36rpx;
border: none;
}
.login-required {
opacity: 0.8;
}
.title {
font-size: 36rpx;
color: #8f8f94;
/* 登录提示弹窗 */
.login-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.login-modal-content {
width: 580rpx;
background: #fff;
border-radius: 20rpx;
overflow: hidden;
padding: 40rpx 0 0 0;
}
.login-modal-title {
font-size: 34rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
}
.login-modal-text {
font-size: 30rpx;
color: #666;
text-align: center;
padding: 0 40rpx;
margin-bottom: 50rpx;
}
.login-modal-btns {
display: flex;
border-top: 1rpx solid #eee;
}
.modal-btn {
flex: 1;
height: 90rpx;
line-height: 90rpx;
text-align: center;
font-size: 32rpx;
background: #fff;
border-radius: 0;
}
.modal-btn.cancel {
color: #999;
border-right: 1rpx solid #eee;
}
.modal-btn.confirm {
color: #ff9800;
font-weight: bold;
}
</style>

368
src/pages/mine/mine.vue Normal file
View File

@@ -0,0 +1,368 @@
<template>
<view class="container">
<!-- 顶部自定义导航栏 -->
<view class="custom-navbar fixed-navbar">
<text class="navbar-title">我的</text>
</view>
<!-- 用户信息区域 -->
<view class="user-info">
<image class="avatar" :src="avatarUrl" mode="aspectFill"></image>
<view class="user-details" v-if="isLoggedIn">
<text class="login-text">{{ nickName }}</text>
<text class="user-id">ID: {{ openid }}</text>
</view>
<view class="user-details" v-else>
<text class="login-text">请登录</text>
<text class="user-id">ID:</text>
<button class="login-btn" @click="handleLogin">{{ loginButtonText }}</button>
</view>
</view>
<!-- 菜单卡片 -->
<view class="menu-cards">
<!-- 用户须知 -->
<view class="menu-card" @click="showAgreement">
<view class="card-icon notice-icon">🔊</view>
<text class="card-title">用户须知</text>
</view>
<!-- 用户设备 -->
<view class="menu-card">
<view class="card-icon device-icon">📦</view>
<text class="card-title">用户设备</text>
</view>
</view>
<!-- 剧情角色区块 -->
<view class="section-block">
<view class="section-header">
<text class="section-title">剧情角色</text>
<text class="view-all">查看全部</text>
</view>
</view>
<!-- 克隆声音区块 -->
<view class="section-block">
<view class="section-header">
<text class="section-title">克隆声音</text>
<text class="view-all">查看全部</text>
</view>
</view>
<!-- 退出登录按钮仅登录后显示 -->
<view class="logout-section" v-if="isLoggedIn">
<button class="logout-btn" @click="handleLogout">退出登录</button>
</view>
<!-- 用户须知弹窗 -->
<user-agreement
:visible="showAgreementModal"
@agree="handleAgreeTerms"
@disagree="handleDisagreeTerms"
/>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useUserStore } from '@/stores/user.js';
import UserAgreement from '@/components/UserAgreement.vue';
// 状态管理 - 避免直接依赖store的响应式
const userStore = useUserStore();
const showAgreementModal = ref(false);
// 本地状态避免直接引用store中的响应式状态
const isLoggedIn = ref(false);
const nickName = ref('');
const openid = ref('');
const avatarUrl = ref('/static/default-avatar.png');
// 登录按钮文本
const loginButtonText = computed(() => {
// #ifdef MP-WEIXIN
return '微信一键登录';
// #endif
// #ifdef H5
return '立即登录';
// #endif
return '一键登录';
});
// 初始化函数
const initUserInfo = () => {
try {
// 先尝试加载本地存储的用户信息
const userInfo = uni.getStorageSync('userInfo');
if (userInfo) {
try {
const parsedInfo = JSON.parse(userInfo);
nickName.value = parsedInfo.nickName || '';
avatarUrl.value = parsedInfo.avatarUrl || '/static/default-avatar.png';
openid.value = parsedInfo.openid || '';
isLoggedIn.value = !!parsedInfo.token;
} catch (e) {
console.error('Parse user info error:', e);
}
}
} catch (e) {
console.error('Load user info error:', e);
}
};
// 页面加载时初始化
onMounted(() => {
// 初始化用户信息
initUserInfo();
});
// 登录处理
const handleLogin = () => {
// H5环境
// #ifdef H5
nickName.value = 'H5测试用户';
avatarUrl.value = '/static/default-avatar.png';
openid.value = 'h5-mock-openid';
isLoggedIn.value = true;
// 保存到本地
const userInfo = {
token: 'h5-mock-token',
nickName: nickName.value,
avatarUrl: avatarUrl.value,
openid: openid.value
};
try {
uni.setStorageSync('userInfo', JSON.stringify(userInfo));
} catch (e) {
console.error('Save user info error:', e);
}
uni.showToast({
title: '登录成功',
icon: 'success'
});
return;
// #endif
// 微信环境
// #ifdef MP-WEIXIN
uni.getUserProfile({
desc: '用于完善用户资料',
success: (res) => {
const userInfo = res.userInfo;
nickName.value = userInfo.nickName;
avatarUrl.value = userInfo.avatarUrl;
openid.value = 'wx-' + Date.now();
isLoggedIn.value = true;
// 保存到本地
const storeInfo = {
token: 'wx-mock-token',
nickName: nickName.value,
avatarUrl: avatarUrl.value,
openid: openid.value
};
try {
uni.setStorageSync('userInfo', JSON.stringify(storeInfo));
} catch (e) {
console.error('Save user info error:', e);
}
uni.showToast({
title: '登录成功',
icon: 'success'
});
},
fail: (err) => {
console.error('Login failed:', err);
uni.showToast({
title: '登录失败',
icon: 'none'
});
}
});
// #endif
};
// 退出登录
const handleLogout = () => {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
try {
uni.removeStorageSync('userInfo');
} catch (e) {
console.error('Remove user info error:', e);
}
nickName.value = '';
avatarUrl.value = '/static/default-avatar.png';
openid.value = '';
isLoggedIn.value = false;
uni.showToast({
title: '已退出登录',
icon: 'success'
});
}
}
});
};
// 显示用户须知
const showAgreement = () => {
showAgreementModal.value = true;
};
// 处理同意用户须知
const handleAgreeTerms = () => {
showAgreementModal.value = false;
try {
// 设置本地存储
uni.setStorageSync('hasAgreedToTerms', 'true');
uni.setStorageSync('hasVisitedMinePage', 'true');
} catch (e) {
console.error('Save agreement status error:', e);
}
};
// 处理不同意用户须知
const handleDisagreeTerms = () => {
showAgreementModal.value = false;
};
</script>
<style scoped>
.fixed-navbar {
position: fixed;
top: 0;
left: 0;
width: 100vw;
z-index: 100;
}
.container {
background: #fff9f2;
min-height: 100vh;
padding-top: 100rpx;
padding-bottom: 120rpx;
}
.custom-navbar {
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
background: #fff9f2;
font-weight: bold;
font-size: 38rpx;
letter-spacing: 2rpx;
}
.navbar-title {
color: #333;
}
/* 用户信息区域 */
.user-info {
display: flex;
align-items: center;
padding: 32rpx;
background: #fff9f2;
}
.avatar {
width: 140rpx;
height: 140rpx;
border-radius: 50%;
background-color: #e0e0e0;
}
.user-details {
margin-left: 30rpx;
display: flex;
flex-direction: column;
}
.login-text {
font-size: 40rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
.user-id {
font-size: 28rpx;
color: #999;
margin-bottom: 20rpx;
}
.login-btn {
margin-top: 20rpx;
background-color: #07c160;
color: #fff;
font-size: 28rpx;
border-radius: 40rpx;
width: 240rpx;
height: 70rpx;
line-height: 70rpx;
padding: 0;
}
/* 菜单卡片 */
.menu-cards {
padding: 0 24rpx;
}
.menu-card {
display: flex;
align-items: center;
padding: 40rpx 30rpx;
background: #fff;
border-radius: 20rpx;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
}
.card-icon {
font-size: 48rpx;
color: #ff9800;
margin-right: 24rpx;
}
.card-title {
font-size: 32rpx;
font-weight: 500;
}
/* 区块样式 */
.section-block {
background: #fff;
border-radius: 20rpx;
margin: 24rpx;
padding: 30rpx;
margin-bottom: 24rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
}
.view-all {
font-size: 28rpx;
color: #999;
}
/* 退出登录按钮 */
.logout-section {
padding: 40rpx 24rpx;
}
.logout-btn {
width: 100%;
background-color: #f5f5f5;
color: #666;
font-size: 30rpx;
border-radius: 8rpx;
}
</style>

180
src/pages/script/editor.vue Normal file
View File

@@ -0,0 +1,180 @@
<template>
<view class="container">
<!-- 顶部自定义导航栏 -->
<view class="custom-navbar fixed-navbar">
<view class="navbar-left" @click="goBack">
<text class="back-icon"></text>
</view>
<text class="navbar-title">新建剧本</text>
<view class="navbar-right"></view>
</view>
<!-- 脚本编辑器区域 -->
<view class="editor-content">
<view class="form-group">
<text class="form-label">剧本标题</text>
<input type="text" class="form-input" v-model="scriptTitle" placeholder="请输入剧本标题" />
</view>
<view class="form-group">
<text class="form-label">剧本类型</text>
<picker mode="selector" :range="scriptTypes" @change="handleTypeChange" class="form-picker">
<view class="picker-value">{{ scriptTypes[scriptTypeIndex] }}</view>
</picker>
</view>
<view class="form-group">
<text class="form-label">剧本内容</text>
<textarea class="form-textarea" v-model="scriptContent" placeholder="请输入剧本内容" />
</view>
<button class="save-btn" @click="saveScript">保存剧本</button>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
// 状态
const scriptTitle = ref('');
const scriptTypeIndex = ref(0);
const scriptTypes = ['台词腔', '人妻', '外卖媛', '病娇', '其他'];
const scriptContent = ref('');
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
// 处理类型选择
const handleTypeChange = (e) => {
scriptTypeIndex.value = e.detail.value;
};
// 保存剧本
const saveScript = () => {
// 验证输入
if (!scriptTitle.value) {
uni.showToast({
title: '请输入剧本标题',
icon: 'none'
});
return;
}
if (!scriptContent.value) {
uni.showToast({
title: '请输入剧本内容',
icon: 'none'
});
return;
}
// 这里添加保存逻辑
uni.showToast({
title: '保存成功',
icon: 'success'
});
// 延迟返回
setTimeout(() => {
uni.navigateBack();
}, 1500);
};
</script>
<style scoped>
.fixed-navbar {
position: fixed;
top: 0;
left: 0;
width: 100vw;
z-index: 100;
}
.container {
background: #fff9f2;
min-height: 100vh;
padding-top: 100rpx;
padding-bottom: 40rpx;
}
.custom-navbar {
height: 100rpx;
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(90deg, #ffe5c2, #fff6e5);
padding: 0 30rpx;
}
.navbar-left, .navbar-right {
width: 60rpx;
display: flex;
align-items: center;
}
.back-icon {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.navbar-title {
color: #333;
font-weight: bold;
font-size: 38rpx;
letter-spacing: 2rpx;
}
/* 编辑器样式 */
.editor-content {
padding: 30rpx;
}
.form-group {
margin-bottom: 30rpx;
}
.form-label {
display: block;
font-size: 30rpx;
font-weight: 500;
margin-bottom: 15rpx;
}
.form-input {
width: 100%;
height: 80rpx;
background: #fff;
border-radius: 12rpx;
padding: 0 20rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.form-picker {
width: 100%;
height: 80rpx;
background: #fff;
border-radius: 12rpx;
padding: 0 20rpx;
font-size: 28rpx;
display: flex;
align-items: center;
}
.picker-value {
color: #333;
}
.form-textarea {
width: 100%;
height: 400rpx;
background: #fff;
border-radius: 12rpx;
padding: 20rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.save-btn {
margin-top: 50rpx;
background: #ff9800;
color: #fff;
height: 90rpx;
line-height: 90rpx;
border-radius: 45rpx;
font-size: 32rpx;
font-weight: 500;
}
</style>

338
src/pages/voice/clone.vue Normal file
View File

@@ -0,0 +1,338 @@
<template>
<view class="container">
<!-- 顶部自定义导航栏 -->
<view class="custom-navbar fixed-navbar">
<view class="navbar-left" @click="goBack">
<text class="back-icon"></text>
</view>
<text class="navbar-title">克隆声音</text>
<view class="navbar-right"></view>
</view>
<!-- 声音克隆内容 -->
<view class="voice-content">
<view class="section-title">上传声音样本</view>
<!-- 录音区域 -->
<view class="voice-card">
<view class="voice-action">
<view class="action-icon" @click="toggleRecord">
<text v-if="!isRecording">🎙</text>
<text v-else></text>
</view>
<text class="action-text">{{ isRecording ? '停止录音' : '开始录音' }}</text>
</view>
<view class="voice-status">
<text>{{ recordStatus }}</text>
<text v-if="recordDuration > 0" class="duration">{{ recordDuration }}s</text>
</view>
</view>
<!-- 上传录音文件 -->
<view class="upload-card">
<view class="upload-icon" @click="uploadVoiceFile">+</view>
<text class="upload-text">上传音频文件</text>
<text class="upload-desc">.mp3, .wav, 最大 10MB</text>
</view>
<!-- 声音样本列表 -->
<view class="sample-list" v-if="voiceSamples.length > 0">
<view class="section-title">已上传的样本</view>
<view class="sample-item" v-for="(sample, index) in voiceSamples" :key="index">
<view class="sample-info">
<text class="sample-name">样本{{ index + 1 }}</text>
<text class="sample-duration">{{ sample.duration }}s</text>
</view>
<view class="sample-actions">
<text class="play-icon" @click="playSample(index)"></text>
<text class="delete-icon" @click="deleteSample(index)">🗑</text>
</view>
</view>
</view>
<!-- 提交按钮 -->
<button class="submit-btn" @click="submitVoiceClone" :disabled="voiceSamples.length === 0">
{{ voiceSamples.length === 0 ? '请先上传声音样本' : '开始克隆声音' }}
</button>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
// 状态
const isRecording = ref(false);
const recordStatus = ref('准备录音');
const recordDuration = ref(0);
const voiceSamples = ref([]);
let recordTimer = null;
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
// 切换录音状态
const toggleRecord = () => {
if (isRecording.value) {
stopRecord();
} else {
startRecord();
}
};
// 开始录音
const startRecord = () => {
isRecording.value = true;
recordStatus.value = '正在录音...';
recordDuration.value = 0;
// 模拟录音计时
recordTimer = setInterval(() => {
recordDuration.value++;
// 最多录制60秒
if (recordDuration.value >= 60) {
stopRecord();
}
}, 1000);
// 这里添加实际录音的API调用
uni.showToast({
title: '开始录音',
icon: 'none'
});
};
// 停止录音
const stopRecord = () => {
isRecording.value = false;
recordStatus.value = '录音已完成';
clearInterval(recordTimer);
// 模拟添加录音样本
voiceSamples.value.push({
duration: recordDuration.value,
url: 'sample.mp3'
});
// 这里添加实际停止录音API调用
uni.showToast({
title: '录音已保存',
icon: 'success'
});
};
// 上传音频文件
const uploadVoiceFile = () => {
uni.showToast({
title: '上传功能开发中',
icon: 'none'
});
// 模拟添加上传样本
setTimeout(() => {
voiceSamples.value.push({
duration: 25,
url: 'upload.mp3'
});
}, 1000);
};
// 播放样本
const playSample = (index) => {
uni.showToast({
title: `播放样本${index + 1}`,
icon: 'none'
});
};
// 删除样本
const deleteSample = (index) => {
voiceSamples.value.splice(index, 1);
uni.showToast({
title: '样本已删除',
icon: 'success'
});
};
// 提交声音克隆
const submitVoiceClone = () => {
if (voiceSamples.value.length === 0) {
return;
}
uni.showLoading({
title: '处理中...'
});
// 模拟处理时间
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: '声音克隆成功',
icon: 'success'
});
// 延迟返回
setTimeout(() => {
uni.navigateBack();
}, 1500);
}, 2000);
};
</script>
<style scoped>
.fixed-navbar {
position: fixed;
top: 0;
left: 0;
width: 100vw;
z-index: 100;
}
.container {
background: #fff9f2;
min-height: 100vh;
padding-top: 100rpx;
padding-bottom: 40rpx;
}
.custom-navbar {
height: 100rpx;
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(90deg, #ffe5c2, #fff6e5);
padding: 0 30rpx;
}
.navbar-left, .navbar-right {
width: 60rpx;
display: flex;
align-items: center;
}
.back-icon {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.navbar-title {
color: #333;
font-weight: bold;
font-size: 38rpx;
letter-spacing: 2rpx;
}
/* 声音克隆样式 */
.voice-content {
padding: 30rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 500;
margin: 20rpx 0;
}
.voice-card {
background: #fff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
}
.voice-action {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.action-icon {
font-size: 80rpx;
margin-bottom: 20rpx;
}
.action-text {
font-size: 28rpx;
color: #666;
}
.voice-status {
display: flex;
justify-content: space-between;
font-size: 26rpx;
color: #999;
margin-top: 30rpx;
}
.duration {
color: #ff9800;
}
.upload-card {
background: #fff;
border-radius: 20rpx;
padding: 40rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
display: flex;
flex-direction: column;
align-items: center;
}
.upload-icon {
width: 100rpx;
height: 100rpx;
background: #f5f5f5;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 60rpx;
margin-bottom: 20rpx;
color: #999;
}
.upload-text {
font-size: 28rpx;
font-weight: 500;
margin-bottom: 10rpx;
}
.upload-desc {
font-size: 24rpx;
color: #999;
}
.sample-list {
margin-top: 30rpx;
}
.sample-item {
background: #fff;
border-radius: 15rpx;
padding: 20rpx 30rpx;
margin-bottom: 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.sample-name {
font-size: 28rpx;
margin-bottom: 5rpx;
}
.sample-duration {
font-size: 24rpx;
color: #999;
}
.sample-actions {
display: flex;
gap: 20rpx;
}
.play-icon, .delete-icon {
font-size: 34rpx;
}
.submit-btn {
margin-top: 50rpx;
background: #ff9800;
color: #fff;
height: 90rpx;
line-height: 90rpx;
border-radius: 45rpx;
font-size: 32rpx;
font-weight: 500;
}
button[disabled] {
background: #f5f5f5;
color: #999;
}
</style>

View File

@@ -0,0 +1 @@

1
src/static/tabbar/.keep Normal file
View File

@@ -0,0 +1 @@
# 保持 tabbar 目录存在,用于存放底部导航图标。

214
src/stores/user.js Normal file
View File

@@ -0,0 +1,214 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useUserStore = defineStore('user', () => {
// 状态
const token = ref('');
const nickName = ref('');
const avatarUrl = ref('');
const openid = ref('');
const isLoggedIn = ref(false);
const hasAgreedToTerms = ref(false);
const hasVisitedMinePage = ref(false);
// 初始化:从本地存储加载用户信息
function init() {
try {
const userInfo = uni.getStorageSync('userInfo');
if (userInfo) {
const parsedInfo = JSON.parse(userInfo);
token.value = parsedInfo.token || '';
nickName.value = parsedInfo.nickName || '';
avatarUrl.value = parsedInfo.avatarUrl || '';
openid.value = parsedInfo.openid || '';
isLoggedIn.value = !!parsedInfo.token;
}
const agreedStatus = uni.getStorageSync('hasAgreedToTerms');
hasAgreedToTerms.value = agreedStatus === 'true';
const visitedMine = uni.getStorageSync('hasVisitedMinePage');
hasVisitedMinePage.value = visitedMine === 'true';
} catch (e) {
console.error('Failed to load user info from storage', e);
}
}
// 登录
async function login() {
try {
// 1. 获取用户信息
const userProfileRes = await getUserProfile();
// 2. 获取微信登录凭证
const loginRes = await getWxLogin();
// 3. 调用后端登录接口
const loginData = await wxLogin(loginRes.code, userProfileRes.userInfo);
// 4. 保存用户信息
setUserInfo({
token: loginData.token,
nickName: userProfileRes.userInfo.nickName,
avatarUrl: userProfileRes.userInfo.avatarUrl,
openid: loginData.openid
});
return Promise.resolve(loginData);
} catch (error) {
console.error('Login failed:', error);
return Promise.reject(error);
}
}
// 获取用户信息
function getUserProfile() {
// H5环境下模拟用户信息
// #ifdef H5
return new Promise((resolve) => {
resolve({
userInfo: {
nickName: 'H5测试用户',
avatarUrl: '/static/default-avatar.png'
}
});
});
// #endif
// 非H5环境下获取真实用户信息
// #ifndef H5
return new Promise((resolve, reject) => {
uni.getUserProfile({
desc: '用于完善用户资料',
success: (res) => {
resolve(res);
},
fail: (err) => {
reject(err);
}
});
});
// #endif
}
// 获取微信登录凭证
function getWxLogin() {
// H5环境下模拟登录凭证
// #ifdef H5
return new Promise((resolve) => {
resolve({ code: 'mock-code-for-h5' });
});
// #endif
// 非H5环境下获取真实登录凭证
// #ifndef H5
return new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
success: (res) => {
resolve(res);
},
fail: (err) => {
reject(err);
}
});
});
// #endif
}
// 调用后端登录接口
function wxLogin(code, userInfo) {
// H5环境下模拟登录避免API超时
// #ifdef H5
return new Promise((resolve) => {
setTimeout(() => {
resolve({
token: 'mock-token-for-h5',
openid: 'mock-openid-for-h5'
});
}, 300);
});
// #endif
// 非H5环境下调用实际接口
// #ifndef H5
return new Promise((resolve, reject) => {
uni.request({
url: 'https://your-api.com/api/login/wechat',
method: 'POST',
data: {
code,
userInfo
},
success: (res) => {
if (res.statusCode === 200 && res.data.token) {
resolve(res.data);
} else {
reject(new Error('Login failed: ' + JSON.stringify(res.data)));
}
},
fail: (err) => {
reject(err);
},
// 设置超时时间
timeout: 5000
});
});
// #endif
}
// 保存用户信息
function setUserInfo(userInfo) {
token.value = userInfo.token || '';
nickName.value = userInfo.nickName || '';
avatarUrl.value = userInfo.avatarUrl || '';
openid.value = userInfo.openid || '';
isLoggedIn.value = !!userInfo.token;
// 保存到本地存储
uni.setStorageSync('userInfo', JSON.stringify(userInfo));
}
// 退出登录
function logout() {
token.value = '';
nickName.value = '';
avatarUrl.value = '';
openid.value = '';
isLoggedIn.value = false;
// 清除本地存储
uni.removeStorageSync('userInfo');
}
// 新增:设置用户同意须知状态
function setAgreedToTerms(status) {
hasAgreedToTerms.value = status;
uni.setStorageSync('hasAgreedToTerms', status.toString());
}
// 新增:设置用户已访问我的页面
function setVisitedMinePage(status) {
hasVisitedMinePage.value = status;
uni.setStorageSync('hasVisitedMinePage', status.toString());
}
return {
// 状态
token,
nickName,
avatarUrl,
openid,
isLoggedIn,
hasAgreedToTerms,
hasVisitedMinePage,
// 方法
init,
login,
setUserInfo,
logout,
setAgreedToTerms,
setVisitedMinePage
};
});