初始化
CI/CD / build (push) Failing after 6s
Details
CI/CD / build (push) Failing after 6s
Details
This commit is contained in:
parent
e6dae59650
commit
4cc9488499
|
|
@ -0,0 +1,55 @@
|
|||
name: CI/CD
|
||||
|
||||
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# container: docker.gitea.com/runner-images:ubuntu-latest
|
||||
container:
|
||||
image: docker.gitea.com/runner-images:ubuntu-latest
|
||||
options: --add-host=my_gitea:host-gateway
|
||||
|
||||
|
||||
steps:
|
||||
|
||||
|
||||
# - name: Add host entry for Gitea
|
||||
# run: |
|
||||
# echo "${{ vars.GITEAHOST }} my_gitea" | tee -a /etc/hosts
|
||||
|
||||
- name: Test access to Gitea
|
||||
run: |
|
||||
curl -v http://host.docker.internal:3000
|
||||
curl -v http://my_gitea:3000
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build Docker image
|
||||
run: docker build -t vue-app .
|
||||
|
||||
- name: Create container and extract dist
|
||||
run: |
|
||||
docker rm --force vue-container || true;
|
||||
|
||||
rm -rf /docker/front/apps/*
|
||||
mkdir -p /docker/front/apps
|
||||
|
||||
# 创建容器但不运行
|
||||
docker run --name vue-container vue-app
|
||||
# -v 卷挂载是直接映射整个目录,而 docker cp 会保留源路径的目录结构。
|
||||
docker cp vue-container:build/apps/. /docker/front/apps/
|
||||
|
||||
chmod -R 777 /docker/front/apps
|
||||
# -v 卷挂载是直接映射整个目录,而 docker cp 会保留源路径的目录结构。
|
||||
docker cp /docker/front/apps/frontend/dist/. my_caddy:/docker/front/www/
|
||||
docker cp /docker/front/apps/frontendDesigner/dist/. my_caddy:/docker/front/admin/
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
## 实施计划
|
||||
|
||||
### 1. 分析当前代码结构
|
||||
|
||||
- **PhoneLogin.vue**:手机号登录页面主组件,包含登录卡片和子组件调用
|
||||
- **PhoneLoginForm.vue**:登录表单组件,包含登录方式切换和表单字段
|
||||
- **login.js**:包含登录相关的API调用方法,如`sendPhoneCode`和`phoneLoginCode`
|
||||
|
||||
### 2. 修改PhoneLoginForm.vue组件
|
||||
|
||||
#### 模板部分修改
|
||||
- **更新登录模式切换**:将现有的三种模式(密码登录、注册、重置密码)改为仅保留登录相关的模式
|
||||
- **添加密码/验证码登录切换**:在登录模式下,添加一个新的切换开关,允许用户在密码登录和验证码登录之间切换
|
||||
- **调整表单字段显示**:根据选择的登录方式显示相应的表单字段
|
||||
|
||||
#### 脚本部分修改
|
||||
- **添加登录方式状态**:添加`loginMethod` ref,用于跟踪用户选择的是密码登录还是验证码登录
|
||||
- **设置默认登录方式**:将默认登录方式设置为验证码登录
|
||||
- **更新表单验证逻辑**:根据选择的登录方式调整验证规则
|
||||
- **更新发送验证码逻辑**:发送登录验证码时传递`purpose: 'login'`
|
||||
- **更新表单提交逻辑**:验证码登录时调用`phoneLoginCode`函数
|
||||
|
||||
### 3. 修改PhoneLogin.vue组件
|
||||
|
||||
- **更新handleLogin方法**:确保根据登录方式调用正确的登录函数
|
||||
|
||||
### 4. 实施细节
|
||||
|
||||
#### PhoneLoginForm.vue修改点
|
||||
- **第155行**:将默认`loginMode`改为'code'
|
||||
- **第38-70行**:添加密码/验证码登录切换按钮
|
||||
- **第256-285行**:更新`handleSendCode`方法,确保发送验证码时传递正确的purpose
|
||||
- **第288-316行**:更新`handleSubmit`方法,处理验证码登录逻辑
|
||||
|
||||
#### PhoneLogin.vue修改点
|
||||
- **第79-81行**:更新`handleLogin`方法,根据登录方式调用正确的登录函数
|
||||
|
||||
### 5. 预期行为
|
||||
|
||||
- 用户默认看到验证码登录界面
|
||||
- 用户可以在密码登录和验证码登录之间切换
|
||||
- 发送登录验证码时,传递`purpose: 'login'`
|
||||
- 验证码登录调用`phoneLoginCode`函数
|
||||
- 密码登录保持原有功能不变
|
||||
|
||||
### 6. 需要修改的文件
|
||||
|
||||
- `d:\work\Aiproject\DeotalandAi\apps\frontend\src\components\auth\PhoneLoginForm.vue`
|
||||
- `d:\work\Aiproject\DeotalandAi\apps\frontend\src\views\Login\PhoneLogin.vue`
|
||||
|
|
@ -0,0 +1,386 @@
|
|||
# 实现竖屏移动端卡片展示组件
|
||||
|
||||
## 组件设计目标
|
||||
|
||||
* 适配竖屏移动端的卡片展示组件
|
||||
|
||||
* 包含图片展示、加载蒙层、预览功能
|
||||
|
||||
* 底部功能按钮(修改、编辑、场景图、下载、删除)
|
||||
|
||||
* 向父组件抛出事件
|
||||
|
||||
## 实现步骤
|
||||
|
||||
### 1. 实现 `shu.vue` 组件
|
||||
|
||||
* 设计组件结构:主容器、图片区域、功能按钮区域
|
||||
|
||||
* 实现响应式布局,适配竖屏移动端
|
||||
|
||||
* 使用 Scoped CSS + CSS 变量实现样式隔离与主题定制
|
||||
|
||||
### 2. 图片展示与加载效果
|
||||
|
||||
* 使用 `el-image` 组件实现图片展示
|
||||
|
||||
* 添加加载状态管理,显示加载蒙层
|
||||
|
||||
* 实现图片加载完成后的过渡效果
|
||||
|
||||
### 3. 图片预览功能
|
||||
|
||||
* 集成图片预览功能,适配 H5 移动端
|
||||
|
||||
* 点击图片触发预览,支持手势操作
|
||||
|
||||
### 4. 底部功能按钮
|
||||
|
||||
* 实现修改、编辑、场景图、下载、删除按钮
|
||||
|
||||
* 每个按钮向父组件抛出对应的事件
|
||||
|
||||
* 按钮样式设计符合 UI/UX 要求
|
||||
|
||||
### 5. 与父组件交互
|
||||
|
||||
* 在 `CreateProjectShu.vue` 中引入并使用 `shu.vue` 组件
|
||||
|
||||
* 实现事件处理逻辑,接收并处理子组件抛出的事件
|
||||
|
||||
## 代码实现
|
||||
|
||||
### `shu.vue` 组件
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="shu-card-container">
|
||||
<!-- 图片展示区域 -->
|
||||
<div class="image-wrapper" @click="handlePreview">
|
||||
<el-image
|
||||
:src="props.imageUrl"
|
||||
:fit="'cover'"
|
||||
:preview-src-list="[props.imageUrl]"
|
||||
@load="handleImageLoad"
|
||||
@error="handleImageError"
|
||||
>
|
||||
<!-- 加载蒙层 -->
|
||||
<template #loading>
|
||||
<div class="loading-mask">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">图片加载中...</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
|
||||
<!-- 功能按钮区域 -->
|
||||
<div class="action-buttons">
|
||||
<button class="action-btn" @click="handleModify" title="修改">
|
||||
<el-icon class="btn-icon"><ChatDotRound /></el-icon>
|
||||
<span class="btn-text">修改</span>
|
||||
</button>
|
||||
<button class="action-btn" @click="handleEdit" title="编辑">
|
||||
<el-icon class="btn-icon"><EditPen /></el-icon>
|
||||
<span class="btn-text">编辑</span>
|
||||
</button>
|
||||
<button class="action-btn" @click="handleScene" title="场景图">
|
||||
<el-icon class="btn-icon"><Grid /></el-icon>
|
||||
<span class="btn-text">场景图</span>
|
||||
</button>
|
||||
<button class="action-btn" @click="handleDownload" title="下载">
|
||||
<el-icon class="btn-icon"><Download /></el-icon>
|
||||
<span class="btn-text">下载</span>
|
||||
</button>
|
||||
<button class="action-btn delete-btn" @click="handleDelete" title="删除">
|
||||
<el-icon class="btn-icon"><Delete /></el-icon>
|
||||
<span class="btn-text">删除</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ElImage, ElIcon } from 'element-plus';
|
||||
import { ChatDotRound, EditPen, Grid, Download, Delete } from '@element-plus/icons-vue';
|
||||
|
||||
// 定义组件属性
|
||||
const props = defineProps({
|
||||
// 图片URL
|
||||
imageUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 卡片数据
|
||||
cardData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['preview', 'modify', 'edit', 'scene', 'download', 'delete']);
|
||||
|
||||
// 图片加载状态
|
||||
const isLoading = ref(true);
|
||||
|
||||
// 处理图片加载完成
|
||||
const handleImageLoad = () => {
|
||||
isLoading.value = false;
|
||||
};
|
||||
|
||||
// 处理图片加载错误
|
||||
const handleImageError = () => {
|
||||
isLoading.value = false;
|
||||
};
|
||||
|
||||
// 处理图片预览
|
||||
const handlePreview = () => {
|
||||
emit('preview', props.imageUrl);
|
||||
};
|
||||
|
||||
// 处理修改按钮点击
|
||||
const handleModify = (e) => {
|
||||
e.stopPropagation();
|
||||
emit('modify', props.cardData);
|
||||
};
|
||||
|
||||
// 处理编辑按钮点击
|
||||
const handleEdit = (e) => {
|
||||
e.stopPropagation();
|
||||
emit('edit', props.cardData);
|
||||
};
|
||||
|
||||
// 处理场景图按钮点击
|
||||
const handleScene = (e) => {
|
||||
e.stopPropagation();
|
||||
emit('scene', props.cardData);
|
||||
};
|
||||
|
||||
// 处理下载按钮点击
|
||||
const handleDownload = (e) => {
|
||||
e.stopPropagation();
|
||||
emit('download', props.imageUrl);
|
||||
};
|
||||
|
||||
// 处理删除按钮点击
|
||||
const handleDelete = (e) => {
|
||||
e.stopPropagation();
|
||||
emit('delete', props.cardData);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 组件样式 */
|
||||
.shu-card-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 图片区域样式 */
|
||||
.image-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-bottom: 150%; /* 2:3 竖屏比例 */
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.image-wrapper :deep(.el-image) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.image-wrapper :deep(.el-image__inner) {
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.image-wrapper:hover :deep(.el-image__inner) {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
/* 加载蒙层样式 */
|
||||
.loading-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.9) 0%, rgba(248, 249, 250, 0.95) 100%);
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid rgba(107, 70, 193, 0.2);
|
||||
border-top: 4px solid #6B46C1;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: #6B46C1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 功能按钮区域样式 */
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 12px;
|
||||
background: #f8f9fa;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
min-width: calc(20% - 8px);
|
||||
margin: 4px;
|
||||
padding: 12px 8px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #6B46C1 0%, #A78BFA 100%);
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(107, 70, 193, 0.3);
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.action-btn.delete-btn {
|
||||
background: linear-gradient(135deg, #EF4444 0%, #F87171 100%);
|
||||
}
|
||||
|
||||
.action-btn.delete-btn:hover {
|
||||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.shu-card-container {
|
||||
margin: 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 10px 6px;
|
||||
min-width: calc(20% - 6px);
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.shu-card-container {
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### 2. 集成到父组件 `CreateProjectShu.vue`
|
||||
|
||||
* 在父组件中引入 `shu.vue` 组件
|
||||
|
||||
* 实现事件处理逻辑,接收子组件抛出的事件
|
||||
|
||||
* 在模板中使用新组件展示卡片
|
||||
|
||||
## 设计规范
|
||||
|
||||
* 主色调:深紫色(#6B46C1)、浅紫色(#A78BFA)
|
||||
|
||||
* 辅助色:深灰色(#1F2937)、浅灰色(#F3F4F6)
|
||||
|
||||
* 按钮样式:圆角设计(8px半径)、微妙阴影、悬停效果
|
||||
|
||||
* 字体排版:Inter字体系列、16px基础大小、响应式缩放
|
||||
|
||||
* 布局风格:基于卡片的设计、统一间距(8px网格系统)
|
||||
|
||||
* 动画效果:平滑过渡(200ms缓入缓出)、加载蒙层
|
||||
|
||||
## 测试与验证
|
||||
|
||||
* 确保组件在竖屏移动端正常显示
|
||||
|
||||
* 测试图片加载、预览功能
|
||||
|
||||
* 验证按钮事件正确抛出
|
||||
|
||||
* 检查响应式布局适配情况
|
||||
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="https://draft-user.s3.us-east-2.amazonaws.com/images/2f8e057e-a677-44cd-b709-38245bdec423.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, minimum-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,834 @@
|
|||
<template>
|
||||
<div v-if="show" class="add-modal-overlay" @click="handleOverlayClick">
|
||||
<div class="add-modal-container" @click.stop>
|
||||
<div class="add-header">
|
||||
<h2 class="add-title">{{ $t('addModal.title') }}</h2>
|
||||
<button class="close-button" @click="closeModal">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="add-content">
|
||||
<!-- IP类型选择 -->
|
||||
<div class="form-section">
|
||||
<div class="section-label">{{ $t('addModal.ipType') }}</div>
|
||||
<div class="ip-type-grid">
|
||||
<div
|
||||
class="ip-type-card"
|
||||
:class="{ active: ipType === 1 }"
|
||||
@click="handleIpTypeSelect(1)"
|
||||
>
|
||||
<div class="ip-type-label">{{ $t('addModal.character') }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="ip-type-card"
|
||||
:class="{ active: ipType === 2 }"
|
||||
@click="handleIpTypeSelect(2)"
|
||||
>
|
||||
<div class="ip-type-label">{{ $t('addModal.animal') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 是否需要挂钩 - 仅当series为E1时显示 -->
|
||||
<div class="form-section" v-if="series === 'E1'">
|
||||
<div class="section-label">{{ $t('iPandCardLeft.needHook') }}</div>
|
||||
<div class="hook-selection">
|
||||
<div
|
||||
class="hook-option"
|
||||
:class="{ active: needHook === true }"
|
||||
@click="handleHookSelect(true)"
|
||||
>
|
||||
<div class="hook-label">{{ $t('common.yes') }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="hook-option"
|
||||
:class="{ active: needHook === false }"
|
||||
@click="handleHookSelect(false)"
|
||||
>
|
||||
<div class="hook-label">{{ $t('common.no') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 文本提示输入 -->
|
||||
<div class="form-section">
|
||||
<div class="section-label">{{ $t('addModal.textPrompt') }}</div>
|
||||
<div class="prompt-input-container">
|
||||
<textarea
|
||||
class="prompt-input"
|
||||
:placeholder="$t('addModal.placeholder')"
|
||||
v-model="formData.prompt"
|
||||
rows="4"
|
||||
:disabled="isOptimizing"
|
||||
></textarea>
|
||||
<div v-if="isOptimizing" class="scan-overlay">
|
||||
<div class="scan-line"></div>
|
||||
</div>
|
||||
<button
|
||||
class="optimizer-btn"
|
||||
@click="handleOptimizePrompt"
|
||||
:disabled="isOptimizing"
|
||||
>
|
||||
<span class="btn-icon">🪄</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 参考图片上传 -->
|
||||
<div class="form-section">
|
||||
<div class="section-label">{{ $t('addModal.referenceImage') }}</div>
|
||||
<div
|
||||
class="image-upload-area"
|
||||
@click="triggerFileUpload"
|
||||
@dragover.prevent="handleDragOver"
|
||||
@dragenter.prevent="handleDragEnter"
|
||||
@dragleave.prevent="handleDragLeave"
|
||||
@drop.prevent="handleDrop"
|
||||
:class="{ 'drag-over': isDragOver, 'uploading': isUploading }"
|
||||
>
|
||||
<!-- 图片预览 -->
|
||||
<div v-if="formData.previewImage && !isUploading" class="image-preview">
|
||||
<img :src="formData.previewImage" alt="Selected image">
|
||||
<button class="remove-image" @click.stop="removeImage">×</button>
|
||||
</div>
|
||||
<!-- 上传进度 -->
|
||||
<div v-else-if="isUploading" class="upload-progress">
|
||||
<div class="upload-spinner">
|
||||
<div class="spinner-ring"></div>
|
||||
</div>
|
||||
<div class="upload-progress-text">{{ $t('addModal.uploading') }}</div>
|
||||
</div>
|
||||
<!-- 上传提示 -->
|
||||
<div v-else class="upload-prompt">
|
||||
<div class="upload-icon">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||||
<polyline points="21 15 16 10 5 21"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="upload-text">{{ $t('addModal.uploadOrSelect') }}</span>
|
||||
</div>
|
||||
<!-- 隐藏的文件输入 -->
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@change="handleFileChange"
|
||||
class="hidden-file-input"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 生成按钮 -->
|
||||
<div class="add-footer">
|
||||
<button
|
||||
class="generate-btn"
|
||||
@click="handleGenerate"
|
||||
:disabled="isGenerateDisabled"
|
||||
>
|
||||
{{ $t('addModal.generate') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onBeforeUnmount } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { FileServer, GiminiServer } from '@deotaland/utils';
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const fileServer = new FileServer();
|
||||
const giminiServer = new GiminiServer();
|
||||
|
||||
const props = defineProps({
|
||||
show: { type: Boolean, default: false },
|
||||
series: {
|
||||
type: String,
|
||||
default: 'E1'
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close', 'generate', 'handleHookSelected']);
|
||||
|
||||
const formData = ref({
|
||||
prompt: '',
|
||||
previewImage: ''
|
||||
});
|
||||
|
||||
const ipType = ref(1);
|
||||
const isDragOver = ref(false);
|
||||
const isUploading = ref(false);
|
||||
const isOptimizing = ref(false);
|
||||
const fileInput = ref(null);
|
||||
const needHook = ref(true);
|
||||
|
||||
const isGenerateDisabled = computed(() => {
|
||||
return !formData.value.prompt.trim();
|
||||
});
|
||||
|
||||
const handleIpTypeSelect = (type) => {
|
||||
ipType.value = type;
|
||||
};
|
||||
|
||||
const handleHookSelect = (value) => {
|
||||
needHook.value = value;
|
||||
emit('handleHookSelected', value);
|
||||
};
|
||||
|
||||
const triggerFileUpload = () => {
|
||||
fileInput.value?.click();
|
||||
};
|
||||
|
||||
const handleFileChange = async (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
await uploadImage(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = () => {
|
||||
isDragOver.value = true;
|
||||
};
|
||||
|
||||
const handleDragEnter = () => {
|
||||
isDragOver.value = true;
|
||||
};
|
||||
|
||||
const handleDragLeave = () => {
|
||||
isDragOver.value = false;
|
||||
};
|
||||
|
||||
const handleDrop = async (event) => {
|
||||
isDragOver.value = false;
|
||||
const file = event.dataTransfer.files[0];
|
||||
if (file && file.type.startsWith('image/')) {
|
||||
await uploadImage(file);
|
||||
}
|
||||
};
|
||||
|
||||
const uploadImage = async (file) => {
|
||||
try {
|
||||
isUploading.value = true;
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
formData.value.previewImage = e.target.result;
|
||||
const url = await fileServer.uploadFile(e.target.result);
|
||||
formData.value.previewImage = url;
|
||||
isUploading.value = false;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} catch (error) {
|
||||
console.error('上传图片失败:', error);
|
||||
isUploading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const removeImage = () => {
|
||||
formData.value.previewImage = '';
|
||||
if (fileInput.value) {
|
||||
fileInput.value.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleOptimizePrompt = async () => {
|
||||
isOptimizing.value = true;
|
||||
const prompt = await giminiServer.handleOptimizePrompt(formData.value.prompt, {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "STRING",
|
||||
"description": "角色的名称"
|
||||
},
|
||||
"gender": {
|
||||
"type": "STRING",
|
||||
"description": "角色的性别"
|
||||
},
|
||||
"type": {
|
||||
"type": "STRING",
|
||||
"description": `角色的类型,${ipType.value == 1 ? `人物` : `动物`}`
|
||||
},
|
||||
"appearance": {
|
||||
"type": "STRING",
|
||||
"description": "角色的外观描述,如果描述到了颜色相关的,统一用浅米色或浅卡其色,不少于50字数"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"gender",
|
||||
"appearance"
|
||||
]
|
||||
}, locale.value, formData.value.previewImage);
|
||||
|
||||
isOptimizing.value = false;
|
||||
if (prompt) {
|
||||
const nameKey = t('addModal.optimizedPrompt.name');
|
||||
const genderKey = t('addModal.optimizedPrompt.gender');
|
||||
const appearanceKey = t('addModal.optimizedPrompt.appearance');
|
||||
formData.value.prompt = `${nameKey}: ${prompt.name}\n${genderKey}: ${prompt.gender}\n${appearanceKey}: ${prompt.appearance}`;
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerate = () => {
|
||||
if (!formData.value.prompt.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit('generate', {
|
||||
ipType: ipType.value,
|
||||
prompt: formData.value.prompt,
|
||||
inspirationImage: formData.value.previewImage,
|
||||
needHook: needHook.value
|
||||
});
|
||||
closeModal();
|
||||
};
|
||||
const handleOverlayClick = () => {
|
||||
closeModal();
|
||||
};
|
||||
const closeModal = () => {
|
||||
emit('close');
|
||||
};
|
||||
watch(() => props.show, (newVal) => {
|
||||
document.body.style.overflow = newVal ? 'hidden' : '';
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.body.style.overflow = '';
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.add-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.add-modal-container {
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
max-height: 90vh;
|
||||
background: var(--bg-color, #F3F4F6);
|
||||
border-radius: 16px 16px 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: slideUp 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.add-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border-color, #E5E7EB);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.add-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #1F2937);
|
||||
}
|
||||
|
||||
.close-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-color, #1F2937);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--border-color, #E5E7EB);
|
||||
}
|
||||
|
||||
|
||||
.add-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-color, #1F2937);
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ip-type-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ip-type-card {
|
||||
background: var(--card-bg, #FFFFFF);
|
||||
border: 2px solid var(--border-color, #E5E7EB);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.ip-type-card:hover {
|
||||
border-color: var(--primary-color, #6B46C1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.ip-type-card.active {
|
||||
border-color: var(--primary-color, #6B46C1);
|
||||
background: var(--primary-light, rgba(107, 70, 193, 0.1));
|
||||
}
|
||||
|
||||
.ip-type-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-color, #1F2937);
|
||||
}
|
||||
|
||||
.hook-selection {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.hook-option {
|
||||
background: var(--card-bg, #FFFFFF);
|
||||
border: 2px solid var(--border-color, #E5E7EB);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.hook-option:hover {
|
||||
border-color: var(--primary-color, #6B46C1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.hook-option.active {
|
||||
border-color: var(--primary-color, #6B46C1);
|
||||
background: var(--primary-light, rgba(107, 70, 193, 0.1));
|
||||
}
|
||||
|
||||
.hook-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-color, #1F2937);
|
||||
}
|
||||
|
||||
.prompt-input-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.prompt-input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border-color, #E5E7EB);
|
||||
border-radius: 8px;
|
||||
background: var(--card-bg, #FFFFFF);
|
||||
color: var(--text-color, #1F2937);
|
||||
font-size: 14px;
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.prompt-input::placeholder {
|
||||
color: var(--text-secondary, #6B7280);
|
||||
}
|
||||
|
||||
.prompt-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color, #6B46C1);
|
||||
}
|
||||
|
||||
.prompt-input:disabled {
|
||||
border-color: rgba(99, 102, 241, 0.5);
|
||||
box-shadow: 0 0 0 1px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
/* 扫描线动画效果 */
|
||||
.scan-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.scan-line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(99, 102, 241, 0.8) 20%,
|
||||
rgba(99, 102, 241, 1) 50%,
|
||||
rgba(99, 102, 241, 0.8) 80%,
|
||||
transparent 100%);
|
||||
animation: scan 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes scan {
|
||||
0% {
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.optimizer-btn {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 8px;
|
||||
padding: 6px 12px;
|
||||
background-color: #6366f1;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: background-color 0.2s;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.optimizer-btn:hover:not(:disabled) {
|
||||
background-color: #4f46e5;
|
||||
}
|
||||
|
||||
.optimizer-btn:disabled {
|
||||
background-color: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.image-upload-area {
|
||||
width: 100%;
|
||||
min-height: 150px;
|
||||
border: 2px dashed var(--border-color, #E5E7EB);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background: var(--card-bg, #FFFFFF);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image-upload-area:hover {
|
||||
border-color: var(--primary-color, #6B46C1);
|
||||
}
|
||||
|
||||
.image-upload-area.drag-over {
|
||||
border-color: var(--primary-color, #6B46C1);
|
||||
background: var(--primary-light, rgba(107, 70, 193, 0.1));
|
||||
}
|
||||
|
||||
.image-upload-area.uploading {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.image-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.remove-image {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.remove-image:hover {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.upload-progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.upload-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.spinner-ring {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 3px solid var(--border-color, #E5E7EB);
|
||||
border-top-color: var(--primary-color, #6B46C1);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.upload-progress-text {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary, #6B7280);
|
||||
}
|
||||
|
||||
.upload-prompt {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
color: var(--text-secondary, #6B7280);
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary, #6B7280);
|
||||
}
|
||||
|
||||
.hidden-file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.add-footer {
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--border-color, #E5E7EB);
|
||||
background: var(--card-bg, #FFFFFF);
|
||||
}
|
||||
|
||||
.generate-btn {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: var(--primary-color, #6B46C1);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.generate-btn:hover:not(:disabled) {
|
||||
background: var(--primary-hover, #5B3A9E);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.generate-btn:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.generate-btn:disabled {
|
||||
background: var(--border-color, #E5E7EB);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.add-modal-overlay {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.add-modal-container {
|
||||
border-radius: 16px;
|
||||
max-height: 80vh;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.hook-option {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.hook-label {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.ip-type-card {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.ip-type-label {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
html.dark .add-modal-container {
|
||||
background: var(--bg-color-dark, #1F2937);
|
||||
}
|
||||
|
||||
html.dark .add-title {
|
||||
color: var(--text-color-dark, #F3F4F6);
|
||||
}
|
||||
|
||||
html.dark .section-label {
|
||||
color: var(--text-color-dark, #F3F4F6);
|
||||
}
|
||||
|
||||
html.dark .ip-type-card {
|
||||
background: var(--card-bg-dark, #374151);
|
||||
border-color: var(--border-color-dark, #4B5563);
|
||||
}
|
||||
|
||||
html.dark .ip-type-card.active {
|
||||
background: rgba(107, 70, 193, 0.3);
|
||||
border-color: var(--primary-color, #6B46C1);
|
||||
box-shadow: 0 0 0 2px rgba(107, 70, 193, 0.2), 0 4px 12px rgba(107, 70, 193, 0.15);
|
||||
}
|
||||
|
||||
html.dark .ip-type-label {
|
||||
color: var(--text-color-dark, #F3F4F6);
|
||||
}
|
||||
|
||||
html.dark .ip-type-card.active .ip-type-label {
|
||||
color: #A78BFA;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
html.dark .hook-option {
|
||||
background: var(--card-bg-dark, #374151);
|
||||
border-color: var(--border-color-dark, #4B5563);
|
||||
}
|
||||
|
||||
html.dark .hook-option.active {
|
||||
background: rgba(107, 70, 193, 0.3);
|
||||
border-color: var(--primary-color, #6B46C1);
|
||||
box-shadow: 0 0 0 2px rgba(107, 70, 193, 0.2), 0 4px 12px rgba(107, 70, 193, 0.15);
|
||||
}
|
||||
|
||||
html.dark .hook-label {
|
||||
color: var(--text-color-dark, #F3F4F6);
|
||||
}
|
||||
|
||||
html.dark .hook-option.active .hook-label {
|
||||
color: #A78BFA;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
html.dark .prompt-input {
|
||||
background: var(--card-bg-dark, #374151);
|
||||
border-color: var(--border-color-dark, #4B5563);
|
||||
color: var(--text-color-dark, #F3F4F6);
|
||||
}
|
||||
|
||||
html.dark .prompt-input::placeholder {
|
||||
color: var(--text-secondary-dark, #9CA3AF);
|
||||
}
|
||||
|
||||
html.dark .image-upload-area {
|
||||
background: var(--card-bg-dark, #374151);
|
||||
border-color: var(--border-color-dark, #4B5563);
|
||||
}
|
||||
|
||||
html.dark .upload-text {
|
||||
color: var(--text-secondary-dark, #9CA3AF);
|
||||
}
|
||||
|
||||
html.dark .upload-icon {
|
||||
color: var(--text-secondary-dark, #9CA3AF);
|
||||
}
|
||||
|
||||
html.dark .upload-progress-text {
|
||||
color: var(--text-secondary-dark, #9CA3AF);
|
||||
}
|
||||
|
||||
html.dark .add-footer {
|
||||
background: var(--card-bg-dark, #374151);
|
||||
border-color: var(--border-color-dark, #4B5563);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
<!-- 图片展示区域 -->
|
||||
<div class="image-wrapper" @click="handlePreview">
|
||||
<el-image
|
||||
:src="props.imageUrl"
|
||||
:src="formData.internalImageUrl"
|
||||
:fit="'cover'"
|
||||
:preview-src-list="[props.imageUrl]"
|
||||
@load="handleImageLoad"
|
||||
|
|
@ -12,42 +12,97 @@
|
|||
<!-- 加载蒙层 -->
|
||||
<template #loading>
|
||||
<div class="loading-mask">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">图片加载中...</div>
|
||||
<div class="light-scan-container">
|
||||
<div class="light-beam"></div>
|
||||
<div class="light-particles">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
<div class="loading-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="10" stroke="url(#gradient)" stroke-width="2"/>
|
||||
<path d="M12 6V12L16 14" stroke="url(#gradient)" stroke-width="2" stroke-linecap="round"/>
|
||||
<defs>
|
||||
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#6B46C1"/>
|
||||
<stop offset="100%" stop-color="#A78BFA"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loading-text">{{ t('loading') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #error>
|
||||
<div class="loading-mask">
|
||||
<div class="loading-spinner-wrapper">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-spinner-inner"></div>
|
||||
</div>
|
||||
<div class="loading-text">{{ t('loading') }}</div>
|
||||
<div class="loading-dots">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
|
||||
<!-- 购买按钮 -->
|
||||
<button class="purchase-btn" @click.stop="handleCustomizeToHome" :title="t('modelModal.purchase')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="9" cy="21" r="1"/>
|
||||
<circle cx="20" cy="21" r="1"/>
|
||||
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 功能按钮区域 -->
|
||||
<div class="action-buttons">
|
||||
<button class="action-btn" @click="handleModify" title="修改">
|
||||
<button class="action-btn" @click="handleModify" :title="t('modelModal.modify')">
|
||||
<el-icon class="btn-icon"><ChatDotRound /></el-icon>
|
||||
<span class="btn-text">修改</span>
|
||||
<span class="btn-text">{{ t('modelModal.modify') }}</span>
|
||||
</button>
|
||||
<button class="action-btn" @click="handlePartialEdit" title="局部">
|
||||
<button class="action-btn" @click="handlePartialEdit" :title="t('modelModal.edit')">
|
||||
<el-icon class="btn-icon"><Brush /></el-icon>
|
||||
<span class="btn-text">局部</span>
|
||||
<span class="btn-text">{{ t('modelModal.edit') }}</span>
|
||||
</button>
|
||||
<button class="action-btn" @click="handleDownload" title="下载">
|
||||
<button class="action-btn" @click="handleDownload" :title="t('modelModal.download')">
|
||||
<el-icon class="btn-icon"><Download /></el-icon>
|
||||
<span class="btn-text">下载</span>
|
||||
<span class="btn-text">{{ t('modelModal.download') }}</span>
|
||||
</button>
|
||||
<button class="action-btn delete-btn" @click="handleDelete" title="删除">
|
||||
<button class="action-btn delete-btn" @click="handleDelete" :title="t('modelModal.delete')">
|
||||
<el-icon class="btn-icon"><Delete /></el-icon>
|
||||
<span class="btn-text">删除</span>
|
||||
<span class="btn-text">{{ t('modelModal.delete') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref,onMounted } from 'vue';
|
||||
import { ElImage, ElIcon } from 'element-plus';
|
||||
import { ChatDotRound, Brush, Download, Delete } from '@element-plus/icons-vue';
|
||||
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { GiminiServer } from '@deotaland/utils';
|
||||
const { t } = useI18n();
|
||||
const cjimg = 'https://draft-user.s3.us-east-2.amazonaws.com/images/14f98f33-06a7-4629-a42e-d7cfbced786f';
|
||||
const formData = ref({
|
||||
internalImageUrl: '',//内部图片URL
|
||||
status:'loading',//状态
|
||||
});
|
||||
// 定义组件属性
|
||||
const props = defineProps({
|
||||
combinedPromptJson:{//动态提示词
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
// 图片URL
|
||||
imageUrl: {
|
||||
type: String,
|
||||
|
|
@ -61,7 +116,7 @@ const props = defineProps({
|
|||
});
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['preview', 'modify', 'edit', 'download', 'delete']);
|
||||
const emit = defineEmits(['preview', 'modify', 'edit', 'download', 'delete','customize-to-home']);
|
||||
|
||||
// 图片加载状态
|
||||
const isLoading = ref(true);
|
||||
|
|
@ -90,11 +145,8 @@ const handleModify = (e) => {
|
|||
// 处理局部按钮点击
|
||||
const handlePartialEdit = (e) => {
|
||||
e.stopPropagation();
|
||||
emit('edit', props.cardData);
|
||||
emit('edit', formData.value.internalImageUrl);
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 处理下载按钮点击
|
||||
const handleDownload = (e) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -106,6 +158,99 @@ const handleDelete = (e) => {
|
|||
e.stopPropagation();
|
||||
emit('delete', props.cardData);
|
||||
};
|
||||
const giminiServer = new GiminiServer();
|
||||
// 处理图片生成
|
||||
const handleGenerateImage = async () => {
|
||||
const iscjt = props?.cardData?.diyPromptText&&props?.cardData?.diyPromptText?.indexOf('[CJT_DEOTA]')!=-1;
|
||||
try {
|
||||
// 使用多图参考生成
|
||||
let referenceImages = [];
|
||||
// 如果有灵感图片,也添加到参考图片列表
|
||||
if (props?.cardData?.inspirationImage) {
|
||||
referenceImages.push(props.cardData.inspirationImage);
|
||||
}
|
||||
if(iscjt){
|
||||
props.cardData.imgyt&&referenceImages.push(props.cardData.imgyt);
|
||||
referenceImages.push(props.cardData.diyPromptImg);
|
||||
referenceImages.push(cjimg);
|
||||
}
|
||||
if(props.cardData.diyPromptText||props.cardData.addDiyPromptImg){
|
||||
referenceImages.push(props.cardData.diyPromptImg);
|
||||
}
|
||||
let dtprompt;
|
||||
if(props?.cardData?.ipType==1){
|
||||
dtprompt = props.combinedPromptJson.person.content;
|
||||
referenceImages.push(...props.combinedPromptJson.person.imgs);
|
||||
}else if(props?.cardData?.ipType==2){
|
||||
dtprompt = props.combinedPromptJson.animal.content;
|
||||
referenceImages.push(...props.combinedPromptJson.animal.imgs);
|
||||
}
|
||||
if(props.cardData.prompt){
|
||||
dtprompt = `角色外观:${props.cardData.prompt}.${dtprompt}`
|
||||
}
|
||||
let prompt = props.cardData.diyPromptText|| dtprompt
|
||||
// 角色姿势:${props.cardData.ipType==1?``:``}
|
||||
if(props.cardData.prompt&&props.cardData.prompt.indexOf('nospec')!=-1){
|
||||
prompt = '按原图生成'
|
||||
referenceImages = [props.cardData.inspirationImage];
|
||||
formData.value.internalImageUrl = props?.cardData?.inspirationImage;
|
||||
return
|
||||
}
|
||||
const taskResult = await giminiServer.handleGenerateImage(referenceImages, prompt,{
|
||||
project_id: props.cardData.project_id,
|
||||
aspect_ratio:iscjt?'16:9':'9:16',
|
||||
});
|
||||
saveProject(taskResult);
|
||||
getImageTask(taskResult.taskId,taskResult.taskQueue);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
emit('delete');
|
||||
}
|
||||
};
|
||||
const handleCustomizeToHome = () => {
|
||||
emit('customize-to-home', {
|
||||
imageUrl: formData.value.internalImageUrl,
|
||||
cardData: props.cardData
|
||||
});
|
||||
}
|
||||
//查询图片任务
|
||||
const getImageTask = async (taskId,taskQueue)=>{
|
||||
giminiServer.getTaskGinimi(taskId,taskQueue,(imgurls)=>{
|
||||
let imgItem = imgurls.splice(0,1)[0]
|
||||
formData.value.internalImageUrl=imgItem.url;
|
||||
saveProject({
|
||||
taskId:taskId,
|
||||
taskQueue:taskQueue,
|
||||
});
|
||||
// createRemainingImageData(imgurls);
|
||||
},()=>{
|
||||
ElMessage.error('Failed to generate image, please try again later.');
|
||||
emit('delete');
|
||||
});
|
||||
}
|
||||
//保存到项目
|
||||
const saveProject = (taskResult)=>{
|
||||
emit('save-project', {
|
||||
imageUrl:formData.value.internalImageUrl,
|
||||
taskId:taskResult?.taskId||'',
|
||||
taskQueue:taskResult?.taskQueue||'',
|
||||
// status:formData.value.status,
|
||||
status:'success',
|
||||
});
|
||||
}
|
||||
const init = ()=>{
|
||||
if(props.cardData.imageUrl){
|
||||
formData.value.internalImageUrl = props.cardData.imageUrl;
|
||||
// formData.value.internalImageUrl = '';
|
||||
}else if(props.cardData.taskId&&props.cardData.taskQueue){
|
||||
getImageTask(props.cardData.taskId,props.cardData.taskQueue);
|
||||
}else{
|
||||
handleGenerateImage();
|
||||
}
|
||||
}
|
||||
onMounted(()=>{
|
||||
init();
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -122,6 +267,11 @@ const handleDelete = (e) => {
|
|||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
html.dark .shu-card-container {
|
||||
background: #1f2937;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 图片区域样式 */
|
||||
.image-wrapper {
|
||||
position: relative;
|
||||
|
|
@ -149,6 +299,64 @@ const handleDelete = (e) => {
|
|||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
/* 购买按钮样式 */
|
||||
.purchase-btn {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #6B46C1, #A78BFA);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 12px rgba(107, 70, 193, 0.3);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.image-wrapper:hover .purchase-btn {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(107, 70, 193, 0.4);
|
||||
}
|
||||
|
||||
.purchase-btn:hover {
|
||||
background: linear-gradient(135deg, #5B3C9F, #9775E8);
|
||||
transform: translateY(-2px) scale(1.05);
|
||||
box-shadow: 0 6px 16px rgba(107, 70, 193, 0.4);
|
||||
}
|
||||
|
||||
.purchase-btn:active {
|
||||
transform: translateY(0) scale(1);
|
||||
box-shadow: 0 2px 8px rgba(107, 70, 193, 0.3);
|
||||
}
|
||||
|
||||
.purchase-btn svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
/* 移动端购买按钮始终显示 */
|
||||
@media (max-width: 768px) {
|
||||
.purchase-btn {
|
||||
opacity: 1;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.purchase-btn svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载蒙层样式 */
|
||||
.loading-mask {
|
||||
position: absolute;
|
||||
|
|
@ -160,25 +368,165 @@ const handleDelete = (e) => {
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.9) 0%, rgba(248, 249, 250, 0.95) 100%);
|
||||
transition: opacity 0.3s ease;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(8px);
|
||||
transition: opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid rgba(107, 70, 193, 0.2);
|
||||
border-top: 4px solid #6B46C1;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 12px;
|
||||
html.dark .loading-mask {
|
||||
background: rgba(31, 41, 55, 0.85);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
/* 加载动画容器 */
|
||||
.loading-spinner-wrapper {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 外层光晕效果 */
|
||||
.loading-spinner-wrapper::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
transform: translate(-50%, -50%);
|
||||
background: radial-gradient(circle, rgba(107, 70, 193, 0.15) 0%, transparent 70%);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 主旋转圈 */
|
||||
.loading-spinner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 2px solid transparent;
|
||||
border-top-color: #6B46C1;
|
||||
border-right-color: #A78BFA;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* 内层反向旋转圈 */
|
||||
.loading-spinner-inner {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
width: calc(100% - 12px);
|
||||
height: calc(100% - 12px);
|
||||
border: 2px solid transparent;
|
||||
border-bottom-color: #A78BFA;
|
||||
border-left-color: #6B46C1;
|
||||
border-radius: 50%;
|
||||
animation: spinReverse 0.8s linear infinite;
|
||||
}
|
||||
|
||||
/* 中心装饰点 */
|
||||
.loading-spinner-wrapper::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: linear-gradient(135deg, #6B46C1 0%, #A78BFA 100%);
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
animation: centerPulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 加载文字 */
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
color: #6B46C1;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.3px;
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
html.dark .loading-text {
|
||||
color: #A78BFA;
|
||||
}
|
||||
|
||||
/* 跳动圆点 */
|
||||
.loading-dots {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-dots span {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
background: linear-gradient(135deg, #6B46C1 0%, #A78BFA 100%);
|
||||
border-radius: 50%;
|
||||
animation: bounce 1.4s ease-in-out infinite;
|
||||
box-shadow: 0 2px 8px rgba(107, 70, 193, 0.2);
|
||||
}
|
||||
|
||||
.loading-dots span:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.loading-dots span:nth-child(2) {
|
||||
animation-delay: 0.16s;
|
||||
}
|
||||
|
||||
.loading-dots span:nth-child(3) {
|
||||
animation-delay: 0.32s;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes spinReverse {
|
||||
0% { transform: rotate(360deg); }
|
||||
100% { transform: rotate(0deg); }
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 80%, 100% {
|
||||
transform: translateY(0) scale(1);
|
||||
opacity: 0.5;
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-10px) scale(1.3);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
opacity: 0.3;
|
||||
}
|
||||
50% {
|
||||
transform: translate(-50%, -50%) scale(1.2);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes centerPulse {
|
||||
0%, 100% {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
transform: translate(-50%, -50%) scale(1.4);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 功能按钮区域样式 */
|
||||
|
|
@ -193,6 +541,11 @@ const handleDelete = (e) => {
|
|||
z-index: 1;
|
||||
}
|
||||
|
||||
html.dark .action-buttons {
|
||||
background: #1f2937;
|
||||
border-top: 1px solid #374151;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
|
|
@ -216,13 +569,23 @@ const handleDelete = (e) => {
|
|||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05), 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
html.dark .action-btn {
|
||||
background: #1f2937;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* 按钮悬停效果 */
|
||||
.action-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
color: #6B46C1;
|
||||
color: var(--el-color-primary);
|
||||
background: linear-gradient(135deg, rgba(107, 70, 193, 0.1) 0%, rgba(167, 139, 250, 0.1) 100%);
|
||||
box-shadow: 0 4px 12px rgba(107, 70, 193, 0.15), 0 2px 4px rgba(107, 70, 193, 0.1);
|
||||
}
|
||||
|
||||
html.dark .action-btn:hover {
|
||||
background: linear-gradient(135deg, rgba(139, 92, 246, 0.2) 0%, rgba(167, 139, 250, 0.2) 100%);
|
||||
}
|
||||
|
||||
/* 按钮背景渐变效果 - 仅在悬停时显示 */
|
||||
.action-btn::before {
|
||||
content: '';
|
||||
|
|
@ -231,24 +594,21 @@ const handleDelete = (e) => {
|
|||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, #6B46C1 0%, #A78BFA 100%);
|
||||
background: linear-gradient(135deg, var(--el-color-primary) 0%, var(--accent-color) 100%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s ease;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.action-btn:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.action-btn:hover .btn-icon,
|
||||
.action-btn:hover .btn-text {
|
||||
color: #ffffff;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
/* 删除按钮特殊样式 */
|
||||
.action-btn.delete-btn:hover {
|
||||
color: #EF4444;
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.1) 0%, rgba(248, 113, 113, 0.1) 100%);
|
||||
}
|
||||
|
||||
.action-btn.delete-btn::before {
|
||||
|
|
@ -257,7 +617,7 @@ const handleDelete = (e) => {
|
|||
|
||||
.action-btn.delete-btn:hover .btn-icon,
|
||||
.action-btn.delete-btn:hover .btn-text {
|
||||
color: #ffffff;
|
||||
color: #EF4444;
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@
|
|||
<button
|
||||
type="button"
|
||||
class="mode-button"
|
||||
:class="{ 'active': loginMode === 'login' }"
|
||||
@click="loginMode = 'login'"
|
||||
:class="{ 'active': loginMode === 'password' }"
|
||||
@click="loginMode = 'password'"
|
||||
>
|
||||
{{ t('login.phone_login_mode') }}
|
||||
{{ t('login.phone_password_login_mode') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -18,6 +18,14 @@
|
|||
>
|
||||
{{ t('login.phone_register_mode') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="mode-button"
|
||||
:class="{ 'active': loginMode === 'reset' }"
|
||||
@click="loginMode = 'reset'"
|
||||
>
|
||||
{{ t('login.phone_reset_password_mode') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 手机号输入 -->
|
||||
|
|
@ -43,8 +51,28 @@
|
|||
<div v-if="phoneError" class="error-message">{{ phoneError }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 验证码输入 -->
|
||||
<div class="form-group">
|
||||
<!-- 密码/验证码登录切换 -->
|
||||
<div v-if="loginMode === 'password'" class="login-method-toggle">
|
||||
<button
|
||||
type="button"
|
||||
class="method-button"
|
||||
:class="{ 'active': loginMethod === 'code' }"
|
||||
@click="loginMethod = 'code'"
|
||||
>
|
||||
{{ t('login.phone_code_login_method') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="method-button"
|
||||
:class="{ 'active': loginMethod === 'password' }"
|
||||
@click="loginMethod = 'password'"
|
||||
>
|
||||
{{ t('login.phone_password_login_method') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 验证码输入(验证码登录显示) -->
|
||||
<div v-if="(loginMode === 'password' && loginMethod === 'code') || loginMode === 'register' || loginMode === 'reset'" class="form-group">
|
||||
<label class="form-label" for="code">{{ t('login.code_label') }}</label>
|
||||
<div class="input-wrapper verification-code-wrapper" :class="{ 'focused': isCodeFocused }">
|
||||
<input
|
||||
|
|
@ -75,9 +103,9 @@
|
|||
<div v-if="codeError" class="error-message">{{ codeError }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 密码输入(仅在注册模式下显示) -->
|
||||
<div v-if="loginMode === 'register'" class="form-group">
|
||||
<label class="form-label" for="password">{{ t('login.password_label') }}</label>
|
||||
<!-- 密码输入(注册、密码登录、重置密码时显示,验证码登录不显示) -->
|
||||
<div v-if="(loginMode === 'register' || loginMode === 'password' || loginMode === 'reset') && !(loginMode === 'password' && loginMethod === 'code')" class="form-group">
|
||||
<label class="form-label" for="password">{{ loginMode === 'reset' ? t('login.new_password_label') : t('login.password_label') }}</label>
|
||||
<div class="input-wrapper" :class="{ 'focused': isPasswordFocused }">
|
||||
<input
|
||||
id="password"
|
||||
|
|
@ -85,11 +113,11 @@
|
|||
:type="showPassword ? 'text' : 'password'"
|
||||
class="form-input"
|
||||
:class="{ 'error': passwordError }"
|
||||
:placeholder="t('login.password_placeholder')"
|
||||
:placeholder="loginMode === 'reset' ? t('login.new_password_placeholder') : t('login.password_placeholder')"
|
||||
@focus="isPasswordFocused = true"
|
||||
@blur="isPasswordFocused = false"
|
||||
:disabled="loading"
|
||||
autocomplete="new-password"
|
||||
:autocomplete="loginMode === 'password' ? 'current-password' : 'new-password'"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -107,13 +135,7 @@
|
|||
<div v-if="passwordError" class="error-message">{{ passwordError }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 邀请码输入 -->
|
||||
<InviteCodeInput
|
||||
v-model="form.inviteCode"
|
||||
:error="inviteCodeError"
|
||||
:disabled="loading"
|
||||
@validate="validateInviteCode"
|
||||
/>
|
||||
|
||||
|
||||
<!-- 登录/注册按钮 -->
|
||||
<button
|
||||
|
|
@ -122,13 +144,23 @@
|
|||
:disabled="loading || !isFormValid"
|
||||
>
|
||||
<span class="button-text">
|
||||
<span v-if="!loading">
|
||||
{{ loginMode === 'login' ? t('login.phone_login_button') : t('login.phone_register_button') }}
|
||||
<span v-if="!loading">
|
||||
{{
|
||||
(loginMode === 'code' && loginMethod === 'code') ? t('login.phone_code_login_button') :
|
||||
(loginMode === 'code' && loginMethod === 'password') ? t('login.phone_password_login_button') :
|
||||
(loginMode === 'password') ? t('login.phone_password_login_button') :
|
||||
(loginMode === 'register') ? t('login.phone_register_button') :
|
||||
t('login.phone_reset_password_button')
|
||||
}}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{
|
||||
(loginMode === 'code' || loginMode === 'password') ? t('login.phone_logging') :
|
||||
(loginMode === 'register') ? t('login.phone_registering') :
|
||||
t('login.phone_resetting')
|
||||
}}
|
||||
</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ loginMode === 'login' ? t('login.phone_logging') : t('login.phone_registering') }}
|
||||
</span>
|
||||
</span>
|
||||
<div v-if="loading" class="loading-spinner"></div>
|
||||
</button>
|
||||
</form>
|
||||
|
|
@ -140,9 +172,8 @@ import { useI18n } from 'vue-i18n'
|
|||
import { WarningFilled, View, Hide } from '@element-plus/icons-vue'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { required, minLength } from '@vuelidate/validators'
|
||||
import InviteCodeInput from './InviteCodeInput.vue'
|
||||
const { t } = useI18n()
|
||||
const emit = defineEmits(['login', 'register'])
|
||||
const emit = defineEmits(['login', 'codeLogin', 'register', 'resetPassword', 'resetSuccess', 'sendCode'])
|
||||
const props = defineProps({
|
||||
loading: {
|
||||
type: Boolean,
|
||||
|
|
@ -150,15 +181,17 @@ const props = defineProps({
|
|||
}
|
||||
})
|
||||
|
||||
// 登录模式:login 或 register
|
||||
const loginMode = ref('login')
|
||||
// 登录模式:password 或 register 或 reset
|
||||
const loginMode = ref('password')
|
||||
|
||||
// 登录方法:password 或 code
|
||||
const loginMethod = ref('code')
|
||||
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
phone: '',
|
||||
code: '',
|
||||
password: '',
|
||||
inviteCode: ''
|
||||
password: ''
|
||||
})
|
||||
|
||||
// 输入状态
|
||||
|
|
@ -171,7 +204,6 @@ const showPassword = ref(false)
|
|||
const phoneError = ref('')
|
||||
const codeError = ref('')
|
||||
const passwordError = ref('')
|
||||
const inviteCodeError = ref('')
|
||||
|
||||
// 验证码倒计时
|
||||
const countdown = ref(0)
|
||||
|
|
@ -180,12 +212,14 @@ const canSendCode = ref(true)
|
|||
// 表单验证规则
|
||||
const rules = {
|
||||
phone: { required },
|
||||
code: { required, minLength: minLength(4) },
|
||||
password: {
|
||||
required: computed(() => loginMode.value === 'register'),
|
||||
minLength: minLength(6)
|
||||
code: {
|
||||
required: computed(() => loginMode.value !== 'password'),
|
||||
minLength: computed(() => loginMode.value !== 'password' ? minLength(4) : false)
|
||||
},
|
||||
inviteCode: {}
|
||||
password: {
|
||||
required: computed(() => loginMode.value === 'register' || loginMode.value === 'password' || loginMode.value === 'reset'),
|
||||
minLength: minLength(6)
|
||||
}
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(rules, form)
|
||||
|
|
@ -197,11 +231,18 @@ const isPhoneValid = computed(() => {
|
|||
|
||||
// 计算属性:表单是否有效
|
||||
const isFormValid = computed(() => {
|
||||
if (!form.value.phone || !form.value.code) {
|
||||
return false
|
||||
// 登录模式下的验证码登录:需要手机号和验证码
|
||||
if (loginMode.value === 'password' && loginMethod.value === 'code') {
|
||||
return !!form.value.phone && !!form.value.code && !phoneError.value && !codeError.value
|
||||
}
|
||||
|
||||
if (loginMode.value === 'register' && !form.value.password) {
|
||||
// 登录模式下的密码登录:需要手机号和密码
|
||||
if (loginMode.value === 'password' && loginMethod.value === 'password') {
|
||||
return !!form.value.phone && !!form.value.password && !phoneError.value && !passwordError.value
|
||||
}
|
||||
|
||||
// 注册或重置密码模式:需要手机号、验证码和密码
|
||||
if (!form.value.phone || !form.value.code || !form.value.password) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -220,9 +261,9 @@ const validatePhone = () => {
|
|||
}
|
||||
|
||||
const validateCode = () => {
|
||||
if (!form.value.code) {
|
||||
if (loginMode.value !== 'password' && !form.value.code) {
|
||||
codeError.value = t('login.code_empty_error')
|
||||
} else if (form.value.code.length < 4) {
|
||||
} else if (loginMode.value !== 'password' && form.value.code.length < 4) {
|
||||
codeError.value = t('login.code_invalid_error')
|
||||
} else {
|
||||
codeError.value = ''
|
||||
|
|
@ -230,7 +271,14 @@ const validateCode = () => {
|
|||
}
|
||||
|
||||
const validatePassword = () => {
|
||||
if (loginMode.value === 'register') {
|
||||
if (loginMode.value === 'register' || loginMode.value === 'password' || loginMode.value === 'reset') {
|
||||
// 登录模式下的验证码登录:不需要验证密码
|
||||
if (loginMode.value === 'password' && loginMethod.value === 'code') {
|
||||
passwordError.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
// 其他情况:需要验证密码
|
||||
if (!form.value.password) {
|
||||
passwordError.value = t('login.password_empty_error')
|
||||
} else if (form.value.password.length < 6) {
|
||||
|
|
@ -243,16 +291,12 @@ const validatePassword = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const validateInviteCode = () => {
|
||||
inviteCodeError.value = ''
|
||||
}
|
||||
|
||||
// 监听表单变化,实时验证
|
||||
watch(() => form.value.phone, validatePhone)
|
||||
watch(() => form.value.code, validateCode)
|
||||
watch(() => form.value.password, validatePassword)
|
||||
watch(() => form.value.inviteCode, validateInviteCode)
|
||||
watch(() => loginMode.value, validatePassword)
|
||||
watch(() => loginMethod.value, validatePassword)
|
||||
|
||||
// 发送验证码
|
||||
const handleSendCode = async () => {
|
||||
|
|
@ -260,13 +304,17 @@ const handleSendCode = async () => {
|
|||
return
|
||||
}
|
||||
|
||||
// 模拟发送验证码
|
||||
// 发送验证码到父组件处理
|
||||
try {
|
||||
canSendCode.value = false
|
||||
countdown.value = 60
|
||||
|
||||
// 这里应该调用发送验证码的API
|
||||
console.log('发送验证码到:', form.value.phone)
|
||||
// 触发发送验证码事件,传递用途
|
||||
emit('sendCode', {
|
||||
phone: form.value.phone,
|
||||
purpose: loginMode.value === 'reset' ? 'reset-password' :
|
||||
loginMode.value === 'password' ? 'login' : 'register'
|
||||
})
|
||||
|
||||
// 倒计时逻辑
|
||||
const timer = setInterval(() => {
|
||||
|
|
@ -285,22 +333,65 @@ const handleSendCode = async () => {
|
|||
|
||||
// 表单提交
|
||||
const handleSubmit = async () => {
|
||||
// 验证表单
|
||||
await v$.value.$validate()
|
||||
if (v$.value.$invalid) {
|
||||
// 根据登录模式进行不同的验证
|
||||
if (loginMode.value === 'password') {
|
||||
// 密码登录模式:只验证手机号和密码
|
||||
validatePhone()
|
||||
validateCode()
|
||||
validatePassword()
|
||||
return
|
||||
if (phoneError.value || passwordError.value) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 其他模式:使用 Vuelidate 验证所有字段
|
||||
await v$.value.$validate()
|
||||
if (v$.value.$invalid) {
|
||||
validatePhone()
|
||||
validateCode()
|
||||
validatePassword()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 根据登录模式触发不同的事件
|
||||
if (loginMode.value === 'login') {
|
||||
emit('login', form.value)
|
||||
} else {
|
||||
// 根据登录模式和登录方法触发不同的事件
|
||||
if (loginMode.value === 'password') {
|
||||
if (loginMethod.value === 'code') {
|
||||
emit('codeLogin', form.value)
|
||||
} else {
|
||||
emit('login', form.value)
|
||||
}
|
||||
} else if (loginMode.value === 'register') {
|
||||
emit('register', form.value)
|
||||
} else if (loginMode.value === 'reset') {
|
||||
emit('resetPassword', form.value, () => {
|
||||
// 密码重置成功后切换到登录模式
|
||||
loginMode.value = 'password'
|
||||
// 重置表单数据
|
||||
form.value = {
|
||||
phone: '',
|
||||
code: '',
|
||||
password: ''
|
||||
}
|
||||
// 重置错误信息
|
||||
phoneError.value = ''
|
||||
codeError.value = ''
|
||||
passwordError.value = ''
|
||||
})
|
||||
}
|
||||
}
|
||||
//切换登录方式
|
||||
const toggleLoginMode = (mode, method = 'password') => {
|
||||
loginMode.value = mode
|
||||
loginMethod.value = method
|
||||
// 重置错误信息
|
||||
phoneError.value = ''
|
||||
codeError.value = ''
|
||||
passwordError.value = ''
|
||||
}
|
||||
// 抛出方法:供父组件直接调用
|
||||
defineExpose({
|
||||
toggleLoginMode
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -346,6 +437,41 @@ const handleSubmit = async () => {
|
|||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 登录方法切换 */
|
||||
.login-method-toggle {
|
||||
display: flex;
|
||||
background: rgba(139, 92, 246, 0.05);
|
||||
border: 1px solid rgba(139, 92, 246, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 4px;
|
||||
gap: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.method-button {
|
||||
flex: 1;
|
||||
padding: 10px 16px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: #6B7280;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.method-button:hover {
|
||||
color: #6B46C1;
|
||||
}
|
||||
|
||||
.method-button.active {
|
||||
background: white;
|
||||
color: #6B46C1;
|
||||
box-shadow: 0 2px 8px rgba(107, 70, 193, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 表单组 */
|
||||
.form-group {
|
||||
display: flex;
|
||||
|
|
@ -644,6 +770,25 @@ html.dark .mode-button.active {
|
|||
box-shadow: 0 2px 8px rgba(139, 92, 246, 0.25);
|
||||
}
|
||||
|
||||
html.dark .login-method-toggle {
|
||||
background: rgba(139, 92, 246, 0.15);
|
||||
border-color: rgba(139, 92, 246, 0.25);
|
||||
}
|
||||
|
||||
html.dark .method-button {
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
html.dark .method-button:hover {
|
||||
color: #a78bfa;
|
||||
}
|
||||
|
||||
html.dark .method-button.active {
|
||||
background: rgba(31, 41, 55, 0.8);
|
||||
color: #a78bfa;
|
||||
box-shadow: 0 2px 8px rgba(139, 92, 246, 0.25);
|
||||
}
|
||||
|
||||
html.dark .form-label {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@
|
|||
<nav class="sidebar-nav">
|
||||
<ul class="nav-list">
|
||||
<li v-for="item in coreMenuItems" :key="item.id">
|
||||
<router-link
|
||||
:to="item.path"
|
||||
<div
|
||||
class="nav-item"
|
||||
:class="{ 'active': isActiveRoute(item.path) }"
|
||||
@click="handleNavClick(item)"
|
||||
|
|
@ -25,7 +24,7 @@
|
|||
<transition name="fade">
|
||||
<span v-if="!collapsed && item.badge" class="nav-badge">{{ item.badge }}</span>
|
||||
</transition>
|
||||
</router-link>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@
|
|||
import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import AppHeader from './AppHeader.vue'
|
||||
import AppSidebar from './AppSidebar.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
// 注册组件
|
||||
defineOptions({
|
||||
name: 'MainLayout'
|
||||
|
|
@ -105,9 +107,10 @@ const toggleSidebar = () => {
|
|||
state.sidebarVisible = !state.sidebarVisible
|
||||
}
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
// 处理导航
|
||||
const handleNavigate = (route) => {
|
||||
router.replace(route)
|
||||
// 移动端点击导航后隐藏侧边栏
|
||||
if (state.isMobile) {
|
||||
state.sidebarVisible = false
|
||||
|
|
|
|||
|
|
@ -131,7 +131,27 @@ export default {
|
|||
modify: '修改',
|
||||
sceneGraph: '场景图',
|
||||
edit: '局部',
|
||||
download: '下载'
|
||||
download: '下载',
|
||||
delete: '删除',
|
||||
purchase: '购买',
|
||||
loadingText: '图片加载中...'
|
||||
},
|
||||
addModal: {
|
||||
title: '添加内容',
|
||||
ipType: 'IP类型',
|
||||
character: '人物',
|
||||
animal: '动物',
|
||||
textPrompt: '文本提示',
|
||||
placeholder: '请输入角色描述,例如:一个可爱的卡通人物',
|
||||
referenceImage: '参考图片',
|
||||
uploadOrSelect: '点击或拖拽上传图片',
|
||||
uploading: '上传中...',
|
||||
generate: '生成',
|
||||
optimizedPrompt: {
|
||||
name: '名称',
|
||||
gender: '性别',
|
||||
appearance: '外观'
|
||||
}
|
||||
},
|
||||
modelCard: {
|
||||
generateModelButton: '生成模型',
|
||||
|
|
@ -220,6 +240,7 @@ export default {
|
|||
times: '次',
|
||||
remainingCredits: '积分',
|
||||
recharge: '充值',
|
||||
add: '添加',
|
||||
guide: '使用指南',
|
||||
back: '返回',
|
||||
skip: '跳过',
|
||||
|
|
@ -229,6 +250,14 @@ export default {
|
|||
skipGuide: '跳过引导',
|
||||
step: '步骤'
|
||||
},
|
||||
emptyState: {
|
||||
title: '开始你的创作之旅',
|
||||
description: '还没有创建任何内容。点击下方按钮开始创建你的第一个作品,探索无限创意可能。',
|
||||
createButton: '创建新内容',
|
||||
tip1: '选择角色或动物类型',
|
||||
tip2: '输入创意描述或上传参考图',
|
||||
tip3: '生成并保存你的作品'
|
||||
},
|
||||
roles: {
|
||||
creator: '达人会员',
|
||||
free: '免费会员',
|
||||
|
|
@ -681,15 +710,24 @@ export default {
|
|||
code_empty_error: '请输入验证码',
|
||||
code_invalid_error: '请输入有效的验证码',
|
||||
send_code: '发送验证码',
|
||||
resend_code_after: '{}秒后重新发送',
|
||||
phone_login_mode: '手机号登录',
|
||||
phone_register_mode: '手机号注册',
|
||||
phone_login_button: '登录',
|
||||
phone_register_button: '注册',
|
||||
phone_logging: '正在登录...',
|
||||
phone_registering: '正在注册...',
|
||||
phone_login_link: '手机号',
|
||||
email_login_link: '邮箱登录',
|
||||
resend_code_after: '{seconds}秒后重新发送',
|
||||
phone_password_login_mode: '登录',
|
||||
phone_register_mode: '注册',
|
||||
phone_reset_password_mode: '重置密码',
|
||||
phone_login_button: '登录',
|
||||
phone_password_login_button: '登录',
|
||||
phone_register_button: '注册',
|
||||
phone_reset_password_button: '重置密码',
|
||||
phone_logging: '正在登录...',
|
||||
phone_registering: '正在注册...',
|
||||
phone_resetting: '正在重置...',
|
||||
phone_login_link: '手机号',
|
||||
email_login_link: '邮箱登录',
|
||||
new_password_label: '新密码',
|
||||
new_password_placeholder: '请输入新密码',
|
||||
phone_code_login_method: '验证码登录',
|
||||
phone_password_login_method: '密码登录',
|
||||
phone_code_login_button: '登录',
|
||||
},
|
||||
payment: {
|
||||
methods: '支付方式',
|
||||
|
|
@ -1641,10 +1679,30 @@ export default {
|
|||
textInputTitle: 'Text Input',
|
||||
textInputPlaceholder: 'Please enter adjustment content, e.g. change character expression',
|
||||
preview: 'Preview',
|
||||
modify: 'Modify',
|
||||
modify: 'Edit',
|
||||
sceneGraph: 'Scene Graph',
|
||||
edit: 'Edit',
|
||||
download: 'Download'
|
||||
edit: 'Part',
|
||||
download: 'Save',
|
||||
delete: 'Remove',
|
||||
purchase: 'Purchase',
|
||||
loadingText: 'Loading image...'
|
||||
},
|
||||
addModal: {
|
||||
title: 'Add Content',
|
||||
ipType: 'IP Type',
|
||||
character: 'Character',
|
||||
animal: 'Animal',
|
||||
textPrompt: 'Text Prompt',
|
||||
placeholder: 'Enter character description, e.g. a cute cartoon character',
|
||||
referenceImage: 'Reference Image',
|
||||
uploadOrSelect: 'Click or drag to upload image',
|
||||
uploading: 'Uploading...',
|
||||
generate: 'Generate',
|
||||
optimizedPrompt: {
|
||||
name: 'Name',
|
||||
gender: 'Gender',
|
||||
appearance: 'Appearance'
|
||||
}
|
||||
},
|
||||
modelCard: {
|
||||
generateModelButton: 'Generate Model',
|
||||
|
|
@ -1733,6 +1791,7 @@ export default {
|
|||
times: 'times',
|
||||
remainingCredits: 'Credits',
|
||||
recharge: 'Recharge',
|
||||
add: 'Add',
|
||||
guide: 'User Guide',
|
||||
back: 'Back',
|
||||
skip: 'Skip',
|
||||
|
|
@ -1742,6 +1801,14 @@ export default {
|
|||
skipGuide: 'Skip Guide',
|
||||
step: 'Step'
|
||||
},
|
||||
emptyState: {
|
||||
title: 'Start Your Creative Journey',
|
||||
description: 'No content created yet. Click the button below to create your first work and explore infinite creative possibilities.',
|
||||
createButton: 'Create New Content',
|
||||
tip1: 'Choose character or animal type',
|
||||
tip2: 'Enter creative description or upload reference image',
|
||||
tip3: 'Generate and save your work'
|
||||
},
|
||||
userCenter: {
|
||||
title: 'User Center',
|
||||
description: 'Manage your account information and settings',
|
||||
|
|
@ -2185,70 +2252,79 @@ export default {
|
|||
status: 'Status',
|
||||
timeline: 'Tracking Timeline'
|
||||
},
|
||||
login: {
|
||||
divider_text: 'Or',
|
||||
role_system: 'Role System',
|
||||
creator_role: 'Creator',
|
||||
admin_role: 'Administrator',
|
||||
viewer_role: 'Viewer',
|
||||
creator_desc: 'Full system access, including user management and system configuration',
|
||||
admin_desc: 'Content management and user management permissions',
|
||||
viewer_desc: 'Basic feature access permissions',
|
||||
theme_toggle_tooltip: 'Switch to dark theme',
|
||||
theme_toggle_tooltip_light: 'Switch to light theme',
|
||||
language_toggle_tooltip: 'Switch to English',
|
||||
login_success: 'Login successful',
|
||||
login_error: 'Login failed',
|
||||
google_login: 'Login with Google',
|
||||
google_logging: 'Logging in...',
|
||||
email_login: 'Login',
|
||||
email_logging: 'Logging in...',
|
||||
email_placeholder: 'Please enter your email',
|
||||
password_placeholder: 'Please enter your password',
|
||||
email_label: 'Email Address',
|
||||
password_label: 'Password',
|
||||
email_empty_error: 'Please enter email address',
|
||||
email_invalid_error: 'Please enter a valid email address',
|
||||
password_empty_error: 'Please enter password',
|
||||
password_min_error: 'Password must be at least 6 characters',
|
||||
login_success_message: 'Login successful!',
|
||||
login_error_message: 'Login failed',
|
||||
google_login_success: 'Google login successful!',
|
||||
google_login_error: 'Google login failed',
|
||||
login_processing_error: 'An error occurred during login',
|
||||
google_login_processing_error: 'An error occurred during Google login',
|
||||
email_login_notice: 'Email login feature is under development, stay tuned',
|
||||
theme_toggle_light: 'Switch to light theme',
|
||||
theme_toggle_dark: 'Switch to dark theme',
|
||||
forgot_password: 'Forgot Password?',
|
||||
register_account: 'Register Account',
|
||||
invite_code_label: 'Invite Code',
|
||||
invite_code_placeholder: 'Please enter invite code',
|
||||
invite_code_empty_error: 'Please enter invite code',
|
||||
join_waitlist: 'Join Waitlist',
|
||||
join_waitlist_success: 'Successfully joined the waitlist, we will contact you soon',
|
||||
// Phone login related
|
||||
phone_login_title: 'Phone Login',
|
||||
phone_login_subtitle: 'Login with phone number and verification code',
|
||||
phone_label: 'Phone Number',
|
||||
phone_placeholder: 'Please enter your phone number',
|
||||
phone_empty_error: 'Please enter phone number',
|
||||
phone_invalid_error: 'Please enter a valid phone number',
|
||||
code_label: 'Verification Code',
|
||||
code_placeholder: 'Please enter verification code',
|
||||
code_empty_error: 'Please enter verification code',
|
||||
code_invalid_error: 'Please enter a valid verification code',
|
||||
send_code: 'Send Code',
|
||||
resend_code_after: 'Resend after {seconds}s',
|
||||
phone_login_mode: 'Phone Login',
|
||||
phone_register_mode: 'Phone Register',
|
||||
phone_login_button: 'Login',
|
||||
login: {
|
||||
divider_text: 'OR',
|
||||
role_system: 'Role System',
|
||||
creator_role: 'Creator',
|
||||
admin_role: 'Admin',
|
||||
viewer_role: 'Viewer',
|
||||
creator_desc: 'Full system access, including user management and system configuration',
|
||||
admin_desc: 'Content and user management permissions',
|
||||
viewer_desc: 'Basic feature access permissions',
|
||||
theme_toggle_tooltip: 'Switch to dark theme',
|
||||
theme_toggle_tooltip_light: 'Switch to light theme',
|
||||
language_toggle_tooltip: 'Switch to English',
|
||||
login_success: 'Login successful',
|
||||
login_error: 'Login failed',
|
||||
google_login: 'Sign in with Google',
|
||||
google_logging: 'Signing in...',
|
||||
email_login: 'Sign in',
|
||||
email_logging: 'Signing in...',
|
||||
email_placeholder: 'Enter your email',
|
||||
password_placeholder: 'Enter your password',
|
||||
email_label: 'Email Address',
|
||||
password_label: 'Password',
|
||||
email_empty_error: 'Please enter your email address',
|
||||
email_invalid_error: 'Please enter a valid email address',
|
||||
password_empty_error: 'Please enter your password',
|
||||
password_min_error: 'Password must be at least 6 characters',
|
||||
login_success_message: 'Login successful!',
|
||||
login_error_message: 'Login failed',
|
||||
google_login_success: 'Google sign-in successful!',
|
||||
google_login_error: 'Google sign-in failed',
|
||||
login_processing_error: 'An error occurred during login',
|
||||
google_login_processing_error: 'An error occurred during Google sign-in',
|
||||
email_login_notice: 'Email sign-in feature coming soon',
|
||||
theme_toggle_light: 'Switch to light theme',
|
||||
theme_toggle_dark: 'Switch to dark theme',
|
||||
forgot_password: 'Forgot password?',
|
||||
register_account: 'Register account',
|
||||
invite_code_label: 'Invitation Code',
|
||||
invite_code_placeholder: 'Enter invitation code',
|
||||
invite_code_empty_error: 'Please enter invitation code',
|
||||
join_waitlist: 'Join waitlist',
|
||||
join_waitlist_success: 'Successfully joined waitlist, we will contact you soon',
|
||||
// Phone login related
|
||||
phone_login_title: 'Phone Login',
|
||||
phone_login_subtitle: 'Sign in with phone number and verification code',
|
||||
phone_label: 'Phone Number',
|
||||
phone_placeholder: 'Enter your phone number',
|
||||
phone_empty_error: 'Please enter phone number',
|
||||
phone_invalid_error: 'Please enter a valid phone number',
|
||||
code_label: 'Verification Code',
|
||||
code_placeholder: 'Enter verification code',
|
||||
code_empty_error: 'Please enter verification code',
|
||||
code_invalid_error: 'Please enter a valid verification code',
|
||||
send_code: 'Send Code',
|
||||
resend_code_after: 'Resend after {} seconds',
|
||||
phone_password_login_mode: 'Sign',
|
||||
phone_register_mode: 'Register',
|
||||
phone_reset_password_mode: 'Reset',
|
||||
phone_login_button: 'Sign in',
|
||||
phone_password_login_button: 'Sign in',
|
||||
phone_register_button: 'Register',
|
||||
phone_logging: 'Logging in...',
|
||||
phone_reset_password_button: 'Reset Password',
|
||||
phone_logging: 'Signing in...',
|
||||
phone_registering: 'Registering...',
|
||||
phone_resetting: 'Resetting...',
|
||||
phone_login_link: 'Phone',
|
||||
email_login_link: 'Login with Email',
|
||||
},
|
||||
email_login_link: 'Email Sign-in',
|
||||
new_password_label: 'New Password',
|
||||
new_password_placeholder: 'Enter new password',
|
||||
phone_code_login_method: 'Code Login',
|
||||
phone_password_login_method: 'Password Login',
|
||||
phone_code_login_button: 'Sign in',
|
||||
},
|
||||
payment: {
|
||||
methods: 'Payment Methods',
|
||||
methods: {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import 'element-plus/theme-chalk/dark/css-vars.css'
|
|||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
// Plugins
|
||||
import ElementPlus from 'element-plus'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import i18nConfig from './locales/index.js'
|
||||
import router from './router'
|
||||
|
|
@ -50,6 +51,10 @@ app.use(i18n)
|
|||
// Router & UI
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
// Lazyload
|
||||
// app.use(VueLazyload, {
|
||||
// loading: '/logo.png',
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ const AgentManagement = () => import('../views/AgentManagement.vue')
|
|||
const AddAgent = () => import('../views/AddAgent.vue')
|
||||
const DeviceList = () => import('../views/DeviceList.vue')
|
||||
const UiTest = () => import('../views/UiTest.vue')
|
||||
const home = () => import('../views/home/index.vue')
|
||||
const PointsRecharge = () => import('../views/PointsRecharge/PointsRecharge.vue')
|
||||
const UserCenter = () => import('../views/user/index.vue')
|
||||
const NotFound = () => import('../views/NotFound.vue')
|
||||
|
|
@ -25,12 +24,22 @@ const KefuReduce = () => import('../views/kefuReduce.vue')
|
|||
const isPortraitMobile = () => {
|
||||
return window.innerWidth < 768 && window.innerHeight > window.innerWidth
|
||||
}
|
||||
const isWeChatBrowser = () => {
|
||||
const ua = navigator.userAgent.toLowerCase()
|
||||
return ua.indexOf('micromessenger') !== -1
|
||||
}
|
||||
const CreateProject = () => {
|
||||
if (isPortraitMobile()) {
|
||||
return import('../views/Project/CreateProjectShu/CreateProjectShu.vue')
|
||||
}
|
||||
return import('../views/Project/CreateProject.vue')
|
||||
}
|
||||
const home = () => {
|
||||
return import('../views/home/index.vue')
|
||||
// if (isWeChatBrowser()) {
|
||||
// return import('../views/ModernHome/ModernHome.vue')
|
||||
// }
|
||||
}
|
||||
NProgress.configure({
|
||||
showSpinner: false,
|
||||
})// 开启轻量模式(顶部细线)
|
||||
|
|
@ -90,6 +99,7 @@ const routes = [
|
|||
meta: { requiresAuth: false, keepAlive: false, fullScreen: true }
|
||||
}
|
||||
]
|
||||
|
||||
//免费会员/达人会员动态路由
|
||||
export const freeRoutes = [
|
||||
{
|
||||
|
|
@ -195,6 +205,11 @@ router.beforeEach(async (to, from, next) => {
|
|||
// window.localStorage.setItem('token','123')
|
||||
// return next()
|
||||
// }
|
||||
if(to.path=='/'){
|
||||
if(isWeChatBrowser()){//如果是微信浏览器,跳转到现代首页
|
||||
return next('/czhome')
|
||||
}
|
||||
}
|
||||
if (to.path == '/login' || to.path == '/login/phone' || to.path == '/register' || to.path == '/forgot-password') {
|
||||
const token = localStorage.getItem('token')
|
||||
// 如果有 token,跳转到首页
|
||||
|
|
|
|||
|
|
@ -22,42 +22,9 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
} finally {
|
||||
}
|
||||
}
|
||||
// 手机号+验证码登录方法
|
||||
const phoneLogin = async (data,callback=null) => {
|
||||
try {
|
||||
const res = await requestUtils.common(clientApi.default.PHONE_LOGIN, data)
|
||||
if(res.code === 0){
|
||||
let data = res.data;
|
||||
// 登录成功,保存token和用户信息
|
||||
loginSuccess(data,callback)
|
||||
return res
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('手机号登录失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
// 手机号+验证码+密码注册方法
|
||||
const phoneRegister = async (data,callback=null) => {
|
||||
try {
|
||||
const res = await requestUtils.common(clientApi.default.PHONE_REGISTER, data)
|
||||
if(res.code === 0){
|
||||
let data = res.data;
|
||||
// 注册成功,保存token和用户信息
|
||||
loginSuccess(data,callback)
|
||||
return res
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('手机号注册失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
//登录成功方法
|
||||
const loginSuccess = (data,callback=null) => {
|
||||
token.value = data.accessToken
|
||||
// user.value = data
|
||||
localStorage.setItem('token', token.value);
|
||||
updateUserInfo().then(res => {
|
||||
callback&&callback(data);
|
||||
|
|
@ -100,13 +67,44 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const phoneLogin = async (data,callback=null) => {
|
||||
try {
|
||||
const res = await requestUtils.common(clientApi.default.LOGIN_PHONE, data)
|
||||
if(res.code === 0){
|
||||
let data = res.data;
|
||||
// 登录成功,保存token和用户信息
|
||||
loginSuccess(data,callback)
|
||||
return res
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
//使用手机号和验证码登录
|
||||
const phoneLoginCode = async (data,callback=null) => {
|
||||
try {
|
||||
const res = await requestUtils.common(clientApi.default.LOGIN_PHONE_CODE, data)
|
||||
if(res.code === 0){
|
||||
let data = res.data;
|
||||
// 登录成功,保存token和用户信息
|
||||
loginSuccess(data,callback)
|
||||
return res
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
return {
|
||||
phoneLoginCode,
|
||||
phoneLogin,
|
||||
updateUserInfo,
|
||||
user,
|
||||
token,
|
||||
login,
|
||||
phoneLogin,
|
||||
phoneRegister,
|
||||
logout,
|
||||
loginSuccess,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
--accent-color: #A78BFA;
|
||||
--text-color: #1F2937;
|
||||
--bg-color: #F3F4F6;
|
||||
--border-color: #E5E7EB;
|
||||
--card-bg: #FFFFFF;
|
||||
--text-secondary: #6B7280;
|
||||
--primary-light: rgba(107, 70, 193, 0.1);
|
||||
/* Explicitly set light color scheme by default */
|
||||
color-scheme: light;
|
||||
|
||||
|
|
@ -114,6 +118,10 @@ html.dark * {
|
|||
html.dark {
|
||||
--text-color: #F3F4F6;
|
||||
--bg-color: #0b0d12;
|
||||
--border-color: #374151;
|
||||
--card-bg: #1F2937;
|
||||
--text-secondary: #9CA3AF;
|
||||
--primary-light: rgba(139, 92, 246, 0.15);
|
||||
|
||||
/* Element Plus dark theme variables */
|
||||
--el-color-primary: #8B5CF6;
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@
|
|||
<div class="auth-links">
|
||||
<div class="auth-links-row">
|
||||
<button
|
||||
v-if="showPhoneLogin"
|
||||
type="button"
|
||||
class="auth-link phone-login-link"
|
||||
@click="goToPhoneLogin"
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
<el-icon class="link-icon"><Phone /></el-icon>
|
||||
<span>{{ t('login.phone_login_link') }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="auth-link forgot-password-link"
|
||||
|
|
@ -85,6 +85,7 @@
|
|||
import { onMounted, reactive, ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { environmentUtils } from '@deotaland/utils'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { WarningFilled, InfoFilled, QuestionFilled, UserFilled, Phone } from '@element-plus/icons-vue'
|
||||
// 导入子组件
|
||||
|
|
@ -102,11 +103,45 @@ const inviteCode = ref('')
|
|||
const isInviteCodeValid = computed(() => {
|
||||
return inviteCode.value.trim() !== ''
|
||||
})
|
||||
// 环境检测状态
|
||||
const environmentInfo = ref(null)
|
||||
const isDetectingEnvironment = ref(true)
|
||||
// 判断是否显示手机号登录入口
|
||||
const showPhoneLogin = computed(() => {
|
||||
// 如果还在检测中,默认显示
|
||||
if (isDetectingEnvironment.value) {
|
||||
return true
|
||||
}
|
||||
// 如果检测失败,默认显示
|
||||
if (!environmentInfo.value) {
|
||||
return true
|
||||
}
|
||||
// 如果是国内环境,显示手机号登录入口
|
||||
return environmentInfo.value.isDomestic
|
||||
})
|
||||
|
||||
// 更新邀请码
|
||||
const updateInviteCode = (value) => {
|
||||
inviteCode.value = value
|
||||
}
|
||||
|
||||
// 检测环境
|
||||
const detectEnvironment = async () => {
|
||||
try {
|
||||
const result = await environmentUtils.detectEnvironment({
|
||||
useCache: true,
|
||||
ipDetection: true,
|
||||
timeout: 5000
|
||||
})
|
||||
environmentInfo.value = result
|
||||
} catch (error) {
|
||||
environmentInfo.value = null
|
||||
isDetectingEnvironment.value = true
|
||||
} finally {
|
||||
isDetectingEnvironment.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogin = async (data) => {
|
||||
plugin.login(data)
|
||||
// if (data.inviteCode) {
|
||||
|
|
@ -136,8 +171,11 @@ const goToRegister = () => {
|
|||
const goToPhoneLogin = () => {
|
||||
router.push('/login/phone')
|
||||
}
|
||||
//判断是否是国内环境显示手机号登录入口
|
||||
|
||||
// 页面挂载时初始化认证状态
|
||||
onMounted(() => {
|
||||
detectEnvironment()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -28,8 +28,13 @@
|
|||
<!-- 手机号登录表单 -->
|
||||
<div class="phone-login-section">
|
||||
<PhoneLoginForm
|
||||
ref="PhoneLoginFormRef"
|
||||
@login="handleLogin"
|
||||
@codeLogin="handleCodeLogin"
|
||||
@register="handleRegister"
|
||||
@resetPassword="handleResetPassword"
|
||||
@resetSuccess="handleResetSuccess"
|
||||
@sendCode="handleSendCode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -50,15 +55,6 @@
|
|||
<el-icon class="link-icon"><Message /></el-icon>
|
||||
<span>{{ t('login.email_login_link') }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="auth-link forgot-password-link"
|
||||
@click="goToForgotPassword"
|
||||
>
|
||||
<el-icon class="link-icon"><QuestionFilled /></el-icon>
|
||||
<span>{{ t('login.forgot_password') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -81,17 +77,35 @@ const router = useRouter()
|
|||
const authStore = useAuthStore()
|
||||
const { t } = useI18n()
|
||||
const plugin = reactive(new LOGIN());
|
||||
|
||||
const PhoneLoginFormRef = ref(null)
|
||||
// 处理登录
|
||||
const handleLogin = async (data) => {
|
||||
plugin.phoneLogin(data)
|
||||
}
|
||||
|
||||
// 处理验证码登录
|
||||
const handleCodeLogin = async (data) => {
|
||||
plugin.phoneLoginCode(data)
|
||||
}
|
||||
|
||||
// 处理注册
|
||||
const handleRegister = async (data) => {
|
||||
plugin.phoneRegister(data)
|
||||
}
|
||||
|
||||
// 处理重置密码
|
||||
const handleResetPassword = async (data) => {
|
||||
plugin.phoneResetPassword(data,()=>{
|
||||
// 密码重置成功后切换到登录模式
|
||||
PhoneLoginFormRef.value.toggleLoginMode('password', 'password')
|
||||
})
|
||||
}
|
||||
|
||||
// 处理发送验证码
|
||||
const handleSendCode = async (data) => {
|
||||
plugin.sendPhoneCode(data)
|
||||
}
|
||||
|
||||
// 跳转到邮箱登录页面
|
||||
const goToEmailLogin = () => {
|
||||
router.push('/login')
|
||||
|
|
@ -236,7 +250,7 @@ onMounted(() => {
|
|||
|
||||
.auth-links-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { requestUtils,clientApi } from '@deotaland/utils';
|
||||
import { requestUtils, clientApi } from '@deotaland/utils';
|
||||
import { useRouter } from 'vue-router'
|
||||
import {ElMessage} from 'element-plus';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { useAuthStore } from '@/stores/auth.js';
|
||||
export default class Login {
|
||||
loading = false;//登录loading
|
||||
|
|
@ -10,105 +10,144 @@ export default class Login {
|
|||
}
|
||||
async login(data) {
|
||||
this.loading = true;
|
||||
this.authStore.login(data,(userData)=>{
|
||||
this.authStore.login(data, (userData) => {
|
||||
this.loading = false;
|
||||
this.handleLoginSuccess(userData);
|
||||
// this.router.push({ name: 'czhome' })
|
||||
// this.router.push({ name: 'czhome' })
|
||||
// this.refreshGoogleRefreshToken()
|
||||
});
|
||||
}
|
||||
//手机号登录
|
||||
async phoneLogin(data) {
|
||||
this.loading = true;
|
||||
this.authStore.phoneLogin(data,(userData)=>{
|
||||
this.loading = false;
|
||||
this.handleLoginSuccess(userData);
|
||||
});
|
||||
}
|
||||
//手机号注册
|
||||
async phoneRegister(data) {
|
||||
this.loading = true;
|
||||
this.authStore.phoneRegister(data,(userData)=>{
|
||||
this.loading = false;
|
||||
this.handleLoginSuccess(userData);
|
||||
});
|
||||
}
|
||||
|
||||
//登录成功处理根据不同角色标识
|
||||
handleLoginSuccess(userData){//0候补1免费2达人
|
||||
handleLoginSuccess(userData) {//0候补1免费2达人
|
||||
// userData.user_role=1
|
||||
this.router.push({ name: 'czhome' });
|
||||
// if(userData.user_role != '0'){
|
||||
// this.router.push({ name: 'czhome' });
|
||||
// }else{
|
||||
// ElMessage.success('You have been added to the waitlist');
|
||||
// window.localStorage.removeItem('token')
|
||||
// }
|
||||
this.router.replace({ name: 'czhome' });
|
||||
}
|
||||
//发送邮箱验证码
|
||||
sendEmailCode(item,callback){
|
||||
requestUtils.common(clientApi.default.SEND_EMAIL_CODE,{
|
||||
email:item.email,
|
||||
purpose:item.purpose||'register' //forgot-password
|
||||
}).then(res=>{
|
||||
ElMessage.success('验证码发送成功');
|
||||
callback&&callback();
|
||||
sendEmailCode(item, callback) {
|
||||
requestUtils.common(clientApi.default.SEND_EMAIL_CODE, {
|
||||
email: item.email,
|
||||
purpose: item.purpose || 'register' //forgot-password
|
||||
}).then(res => {
|
||||
ElMessage.success('验证码发送成功');
|
||||
callback && callback();
|
||||
})
|
||||
}
|
||||
|
||||
//发送手机验证码
|
||||
sendPhoneCode(item,callback){
|
||||
requestUtils.common(clientApi.default.SEND_PHONE_CODE,{
|
||||
phone:item.phone,
|
||||
purpose:item.purpose||'login' //register
|
||||
}).then(res=>{
|
||||
ElMessage.success('验证码发送成功');
|
||||
callback&&callback();
|
||||
async sendPhoneCode(data) {
|
||||
let parmas = {
|
||||
"phone": data.phone,
|
||||
"purpose": data.purpose//register注册 //reset-password重置密码//login登录
|
||||
}
|
||||
requestUtils.common(clientApi.default.SEND_SMS_CODE, parmas).then(res => {
|
||||
ElMessage.success('验证码发送成功');
|
||||
})
|
||||
}
|
||||
//确认注册功能
|
||||
confirmRegister(data,callback){
|
||||
//手机号密码登录
|
||||
async phoneLogin(data) {
|
||||
let params = {
|
||||
"email": data.email,
|
||||
"emailCode": data.emailCode,
|
||||
"password": data.password
|
||||
"phone": data.phone,
|
||||
"password": data.password
|
||||
}
|
||||
this.loading = true;
|
||||
this.authStore.phoneLogin(params, (userData) => {
|
||||
this.loading = false;
|
||||
this.handleLoginSuccess(userData);
|
||||
});
|
||||
}
|
||||
requestUtils.common(clientApi.default.REGISTER,params).then(res=>{
|
||||
let data = res.data;
|
||||
// this.authStore.loginSuccess(data,()=>{
|
||||
// this.router.push({ name: 'home' })
|
||||
// })
|
||||
ElMessage.success('注册成功');
|
||||
callback&&callback();
|
||||
//使用手机号和验证码登录
|
||||
async phoneLoginCode(data) {
|
||||
let params = {
|
||||
"phone": data.phone,
|
||||
"smsCode": data.code
|
||||
}
|
||||
this.loading = true;
|
||||
this.authStore.phoneLoginCode(params, (userData) => {
|
||||
this.loading = false;
|
||||
this.handleLoginSuccess(userData);
|
||||
});
|
||||
}
|
||||
//手机号重置密码
|
||||
async phoneResetPassword(data, callback) {
|
||||
let params = {
|
||||
"phone": data.phone,
|
||||
"smsCode": data.code,
|
||||
"newPassword": data.password
|
||||
}
|
||||
this.loading = true;
|
||||
await requestUtils.common(clientApi.default.RESET_PASSWORD_PHONE, params).then(res => {
|
||||
this.loading = false;
|
||||
ElMessage.success('密码重置成功');
|
||||
callback && callback();
|
||||
})
|
||||
}
|
||||
//手机号注册
|
||||
async phoneRegister(data) {
|
||||
let params = {
|
||||
phone: data.phone,
|
||||
smsCode: data.code,
|
||||
password: data.password
|
||||
}
|
||||
this.loading = true;
|
||||
requestUtils.common(clientApi.default.REGISTER_PHONE, params).then(res => {
|
||||
this.loading = false;
|
||||
ElMessage.success('注册成功');
|
||||
let data = res.data;
|
||||
this.authStore.loginSuccess(data,()=>{
|
||||
this.handleLoginSuccess(data);
|
||||
});
|
||||
}).catch(err => {
|
||||
this.loading = false;
|
||||
ElMessage.error('注册失败');
|
||||
})
|
||||
}
|
||||
|
||||
//确认注册功能
|
||||
confirmRegister(data, callback) {
|
||||
let params = {
|
||||
"email": data.email,
|
||||
"emailCode": data.emailCode,
|
||||
"password": data.password
|
||||
}
|
||||
requestUtils.common(clientApi.default.REGISTER, params).then(res => {
|
||||
let data = res.data;
|
||||
// this.authStore.loginSuccess(data,()=>{
|
||||
// this.router.push({ name: 'home' })
|
||||
// })
|
||||
ElMessage.success('注册成功');
|
||||
callback && callback();
|
||||
})
|
||||
}
|
||||
//刷新googleRefreshToken
|
||||
refreshGoogleRefreshToken(callback){
|
||||
requestUtils.common(clientApi.default.REFRESH_TOKEN).then(res=>{
|
||||
ElMessage.success('刷新成功');
|
||||
callback&&callback();
|
||||
refreshGoogleRefreshToken(callback) {
|
||||
requestUtils.common(clientApi.default.REFRESH_TOKEN).then(res => {
|
||||
ElMessage.success('刷新成功');
|
||||
callback && callback();
|
||||
})
|
||||
}
|
||||
//登出
|
||||
logout(){
|
||||
this.authStore.logout(()=>{
|
||||
logout() {
|
||||
this.authStore.logout(() => {
|
||||
this.router.replace({ name: 'login' })
|
||||
})
|
||||
}
|
||||
//确认修改密码
|
||||
confirmForgotPassword(data,callback){
|
||||
confirmForgotPassword(data, callback) {
|
||||
let params = {
|
||||
"email": data.email,
|
||||
"emailCode": data.emailCode,
|
||||
"newPassword": data.password
|
||||
}
|
||||
requestUtils.common(clientApi.default.FORGOT_PASSWORD,params).then(res=>{
|
||||
ElMessage.success('密码修改成功');
|
||||
callback&&callback();
|
||||
requestUtils.common(clientApi.default.FORGOT_PASSWORD, params).then(res => {
|
||||
ElMessage.success('密码修改成功');
|
||||
callback && callback();
|
||||
})
|
||||
}
|
||||
//加入候补队列
|
||||
joinWaitlist(data,callback){
|
||||
joinWaitlist(data, callback) {
|
||||
// 这里可以根据实际情况实现加入候补队列的API调用
|
||||
ElMessage.success('已成功加入候补队列,我们将尽快与您联系');
|
||||
callback&&callback();
|
||||
callback && callback();
|
||||
}
|
||||
}
|
||||
|
|
@ -151,7 +151,8 @@ const iconComponents = {
|
|||
Tickets,
|
||||
Setting,
|
||||
Picture,
|
||||
Cpu
|
||||
Cpu,
|
||||
User
|
||||
}
|
||||
|
||||
// 响应式数据 - 使用计算属性来支持国际化
|
||||
|
|
|
|||
|
|
@ -67,7 +67,6 @@
|
|||
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button> -->
|
||||
|
||||
<!-- 根据卡片类型显示不同组件 -->
|
||||
<IPCard
|
||||
@delete="handleDeleteCard(index)"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<!-- 头部导航栏 -->
|
||||
<div class="header-nav">
|
||||
<div class="header-left">
|
||||
<button class="back-button" @click="handleBack">
|
||||
<button class="back-button" @click="handleBack" :title="$t('header.back')">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
|
|
@ -28,11 +28,72 @@
|
|||
</div>
|
||||
<!-- 卡片列表 -->
|
||||
<div class="cards-container">
|
||||
<!-- 空状态页面 -->
|
||||
<div v-if="cards.length === 0" class="empty-state">
|
||||
<div class="empty-state-content">
|
||||
<div class="empty-state-icon">
|
||||
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="60" cy="60" r="50" fill="url(#gradient1)" fill-opacity="0.1"/>
|
||||
<circle cx="60" cy="60" r="40" fill="url(#gradient1)" fill-opacity="0.15"/>
|
||||
<circle cx="60" cy="60" r="30" fill="url(#gradient1)" fill-opacity="0.2"/>
|
||||
<path d="M60 35V85M35 60H85" stroke="url(#gradient2)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="60" cy="60" r="8" fill="url(#gradient2)"/>
|
||||
<defs>
|
||||
<linearGradient id="gradient1" x1="10" y1="10" x2="110" y2="110" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6B46C1"/>
|
||||
<stop offset="1" stop-color="#A78BFA"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="gradient2" x1="35" y1="35" x2="85" y2="85" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6B46C1"/>
|
||||
<stop offset="1" stop-color="#A78BFA"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="empty-state-title">{{ $t('emptyState.title') }}</h2>
|
||||
<p class="empty-state-description">{{ $t('emptyState.description') }}</p>
|
||||
<button class="empty-state-button" @click="handleAdd">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
{{ $t('emptyState.createButton') }}
|
||||
</button>
|
||||
<div class="empty-state-tips">
|
||||
<div class="tip-item">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<path d="M12 16v-4M12 8h.01"/>
|
||||
</svg>
|
||||
<span>{{ $t('emptyState.tip1') }}</span>
|
||||
</div>
|
||||
<div class="tip-item">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
<span>{{ $t('emptyState.tip2') }}</span>
|
||||
</div>
|
||||
<div class="tip-item">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
|
||||
<path d="M2 17l10 5 10-5"/>
|
||||
<path d="M2 12l10 5 10-5"/>
|
||||
</svg>
|
||||
<span>{{ $t('emptyState.tip3') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片列表 -->
|
||||
<shu-card
|
||||
v-for="(card, index) in cards"
|
||||
:key="card.id"
|
||||
:image-url="card.imageUrl"
|
||||
:card-data="card"
|
||||
:combinedPromptJson="combinedPromptJson"
|
||||
@save-project="(item)=>{handleSaveProject(index,item,'image')}"
|
||||
@customize-to-home="handleCustomizeToHome(index)"
|
||||
@preview="handlePreviewImage"
|
||||
@modify="handleModify"
|
||||
@edit="handlePartialEdit"
|
||||
|
|
@ -40,23 +101,91 @@
|
|||
@delete="handleDeleteCard(index)"
|
||||
/>
|
||||
</div>
|
||||
<!-- 固定添加按钮 -->
|
||||
<button class="add-button" @click="handleAdd" :title="$t('header.add')">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- 添加弹窗 -->
|
||||
<AddModal
|
||||
:series="series"
|
||||
:show="showAddModal"
|
||||
@close="showAddModal = false"
|
||||
@generate="handleAddGenerate"
|
||||
@handleHookSelected="handleHookSelected"
|
||||
/>
|
||||
<PurchaseModal
|
||||
v-if="showPurchaseModal"
|
||||
:series="series"
|
||||
:show="showPurchaseModal"
|
||||
:modelData="CustomizeModalData"
|
||||
@close="showPurchaseModal=false" />
|
||||
<!-- 定制到家弹窗 -->
|
||||
<OrderProcessModal
|
||||
:show="showOrderProcessModal"
|
||||
:modelData="CustomizeModalData"
|
||||
@close="showOrderProcessModal=false"
|
||||
@acknowledge="handleBuyFromCustomize" />
|
||||
<DtCanvasEditor
|
||||
:language="locale"
|
||||
v-model:visible="canvasEditorVisible"
|
||||
:image-url="canvasEditorImageUrl"
|
||||
@add-prompt-card="handleCanvasSave"
|
||||
/>
|
||||
<!-- 文本输入弹窗 -->
|
||||
<div v-if="showTextInput" class="text-input-overlay" role="dialog" aria-modal="true" aria-label="文本输入">
|
||||
<div class="text-input-container">
|
||||
<div class="text-input-header">
|
||||
<div class="text-input-title">{{ t('modelModal.textInputTitle') }}</div>
|
||||
<button class="text-input-close" @click="handleTextInputCancel" aria-label="关闭">
|
||||
<el-icon class="close-icon"><CloseBold /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
ref="textInputRef"
|
||||
v-model="textInputValue"
|
||||
class="text-input-area"
|
||||
:placeholder="t('modelModal.textInputPlaceholder')"
|
||||
rows="4"
|
||||
@keydown.enter.ctrl="handleTextInputConfirm"
|
||||
@keydown.esc="handleTextInputCancel"
|
||||
@click.stop
|
||||
@mousedown.stop
|
||||
></textarea>
|
||||
<div class="text-input-actions">
|
||||
<button class="text-input-btn cancel-btn" @click="handleTextInputCancel">
|
||||
{{ t('common.cancel') }}
|
||||
</button>
|
||||
<button class="text-input-btn confirm-btn" @click="handleTextInputConfirm">
|
||||
{{ t('common.confirm') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 创意空间组件逻辑
|
||||
import { ref, computed, onMounted, onUnmounted,watch } from 'vue';
|
||||
import { ref, computed, onMounted, onUnmounted,watch,nextTick } from 'vue';
|
||||
import {useRoute,useRouter} from 'vue-router';
|
||||
import {MeshyServer,GiminiServer,FileServer} from '@deotaland/utils';
|
||||
import {Project} from '../index';
|
||||
import {ModernHome} from '../../ModernHome/index.js'
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ElIcon } from 'element-plus';
|
||||
import { MagicStick } from '@element-plus/icons-vue';
|
||||
import { MagicStick, CloseBold } from '@element-plus/icons-vue';
|
||||
import LanguageToggle from '@/components/ui/LanguageToggle.vue';
|
||||
import ThemeToggle from '@/components/ui/ThemeToggle.vue';
|
||||
import AddModal from '@/components/AddModal/index.vue';
|
||||
import PurchaseModal from '@/components/PurchaseModal/index.vue';
|
||||
import OrderProcessModal from '@/components/OrderProcessModal/index.vue';
|
||||
|
||||
// 引入竖屏卡片组件
|
||||
import ShuCard from '@/components/IPCard/shu.vue';
|
||||
const { t } = useI18n();
|
||||
const { locale } = useI18n()
|
||||
const headerRef = ref(null);
|
||||
const iPandCardLeftRef = ref(null);
|
||||
|
|
@ -68,14 +197,41 @@ const modernHome = new ModernHome();
|
|||
const router = useRouter();
|
||||
const PluginProject = new Project();
|
||||
|
||||
// 文本输入弹窗相关状态
|
||||
const showTextInput = ref(false);
|
||||
const textInputValue = ref('');
|
||||
const currentModifyCardData = ref(null);
|
||||
const textInputRef = ref(null);
|
||||
|
||||
// 充值按钮点击事件
|
||||
const handleRecharge = () => {
|
||||
router.push('/points-recharge');
|
||||
}
|
||||
|
||||
// 添加按钮点击事件
|
||||
const handleAdd = () => {
|
||||
showAddModal.value = true;
|
||||
}
|
||||
|
||||
// 处理添加弹窗的生成事件
|
||||
const handleAddGenerate = (data) => {
|
||||
// 这里可以调用现有的生成逻辑
|
||||
// 例如调用 handleGenerateRequested 或其他相关函数
|
||||
handleGenerateRequested({
|
||||
count: 1,
|
||||
profile: {
|
||||
appearance: data.prompt
|
||||
},
|
||||
inspirationImage: data.inspirationImage,
|
||||
ipType: data.ipType,
|
||||
ipTypeImg: null
|
||||
});
|
||||
}
|
||||
|
||||
// 弹窗相关状态
|
||||
const showModelModal = ref(false);
|
||||
const selectedModel = ref(null);
|
||||
const showAddModal = ref(false);
|
||||
const showImportModal = ref(false);
|
||||
const importUrl = ref('https://xiaozhi.me/console/agents');
|
||||
const showGuideModal = ref(false);
|
||||
|
|
@ -151,18 +307,40 @@ const handlePartialEdit = (imageUrl, index) => {
|
|||
// 处理修改按钮点击
|
||||
const handleModify = (cardData) => {
|
||||
console.log('修改按钮点击', cardData);
|
||||
// 这里可以实现修改逻辑,例如打开修改弹窗等
|
||||
// 可以直接调用现有的创建提示词卡片函数
|
||||
// 显示文本输入弹窗
|
||||
if (cardData) {
|
||||
// 调用现有的创建提示词卡片函数
|
||||
handleCreatePromptCard(null, {
|
||||
img: cardData.imageUrl,
|
||||
diyPromptText: '',
|
||||
cardData: cardData
|
||||
currentModifyCardData.value = cardData;
|
||||
showTextInput.value = true;
|
||||
textInputValue.value = '';
|
||||
nextTick(() => {
|
||||
if (textInputRef.value) {
|
||||
textInputRef.value.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理文本输入确认
|
||||
const handleTextInputConfirm = () => {
|
||||
if (textInputValue.value.trim() && currentModifyCardData.value) {
|
||||
handleCreatePromptCard(null, {
|
||||
img: currentModifyCardData.value.imageUrl,
|
||||
diyPromptText: textInputValue.value,
|
||||
cardData: currentModifyCardData.value
|
||||
});
|
||||
showTextInput.value = false;
|
||||
textInputValue.value = '';
|
||||
currentModifyCardData.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理文本输入取消
|
||||
const handleTextInputCancel = () => {
|
||||
showTextInput.value = false;
|
||||
textInputValue.value = '';
|
||||
currentModifyCardData.value = null;
|
||||
};
|
||||
|
||||
// 处理场景图按钮点击
|
||||
const handleScene = (cardData) => {
|
||||
console.log('场景图按钮点击', cardData);
|
||||
|
|
@ -223,7 +401,6 @@ const getCombinedPrompt = async (config={})=>{
|
|||
try {
|
||||
const data = await PluginProject.getCombinedPrompt(series.value,config);
|
||||
combinedPromptJson.value = data;
|
||||
console.log(data,'combinedPromptJson.value');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
|
@ -544,8 +721,7 @@ const handleGenerateRequested = async (params) => {
|
|||
});
|
||||
// 添加到卡片数组
|
||||
cards.value.push(newCard);
|
||||
// 等待 200 毫秒
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
console.log(cards.value,'cardcardcard');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1066,7 +1242,7 @@ onUnmounted(() => {// 禁用轮询
|
|||
width: 100%;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
background: #F3F4F6;
|
||||
background: var(--bg-color);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
|
@ -1077,7 +1253,7 @@ onUnmounted(() => {// 禁用轮询
|
|||
left: 0;
|
||||
right: 0;
|
||||
height: 64px;
|
||||
background: #FFFFFF;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
|
@ -1086,6 +1262,11 @@ onUnmounted(() => {// 禁用轮询
|
|||
z-index: 1000;
|
||||
}
|
||||
|
||||
html.dark .header-nav {
|
||||
background: #1f2937;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -1099,10 +1280,10 @@ onUnmounted(() => {// 禁用轮询
|
|||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: #F3F4F6;
|
||||
background: var(--bg-color);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
color: #1F2937;
|
||||
color: var(--text-color);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
|
|
@ -1111,6 +1292,10 @@ onUnmounted(() => {// 禁用轮询
|
|||
transform: translateX(-2px);
|
||||
}
|
||||
|
||||
html.dark .back-button:hover {
|
||||
background: #374151;
|
||||
}
|
||||
|
||||
.back-button:active {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
|
@ -1118,14 +1303,14 @@ onUnmounted(() => {// 禁用轮询
|
|||
.header-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1F2937;
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 剩余积分显示样式 */
|
||||
|
|
@ -1139,23 +1324,28 @@ onUnmounted(() => {// 禁用轮询
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
background-color: #F3F4F6;
|
||||
padding: 8px 0px;
|
||||
background-color: var(--bg-color);
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 12px;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
html.dark .model-count {
|
||||
/* background-color: #374151; */
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.count-icon {
|
||||
color: #6B46C1;
|
||||
color: var(--el-color-primary);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.count-text {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #1F2937;
|
||||
color: var(--text-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
|
@ -1165,7 +1355,7 @@ onUnmounted(() => {// 禁用轮询
|
|||
justify-content: center;
|
||||
padding: 4px 10px;
|
||||
border: none;
|
||||
background-color: #6B46C1;
|
||||
background: linear-gradient(135deg, #6B46C1, #A78BFA);
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
|
|
@ -1173,16 +1363,56 @@ onUnmounted(() => {// 禁用轮询
|
|||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
margin-left: 4px;
|
||||
box-shadow: 0 2px 6px rgba(107, 70, 193, 0.25);
|
||||
}
|
||||
|
||||
.recharge-btn:hover {
|
||||
background-color: #7C3AED;
|
||||
background: linear-gradient(135deg, #5B3C9F, #9775E8);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(107, 70, 193, 0.3);
|
||||
box-shadow: 0 4px 12px rgba(107, 70, 193, 0.4);
|
||||
}
|
||||
|
||||
.recharge-btn:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 6px rgba(107, 70, 193, 0.3);
|
||||
}
|
||||
|
||||
.add-button {
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom:140px !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border: none;
|
||||
background: linear-gradient(135deg, #6B46C1 0%, #A78BFA 100%);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 4px 12px rgba(107, 70, 193, 0.4);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.add-button:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 6px 16px rgba(107, 70, 193, 0.5);
|
||||
}
|
||||
|
||||
html.dark .add-button {
|
||||
background: linear-gradient(135deg, #8B5CF6 0%, #C4B5FD 100%);
|
||||
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4);
|
||||
}
|
||||
|
||||
html.dark .add-button:hover {
|
||||
background: linear-gradient(135deg, #9B6CF6 0%, #D4C5FD 100%);
|
||||
box-shadow: 0 6px 16px rgba(139, 92, 246, 0.5);
|
||||
}
|
||||
|
||||
.add-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.recharge-text {
|
||||
|
|
@ -1211,17 +1441,6 @@ onUnmounted(() => {// 禁用轮询
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.cards-container :deep(.shu-card-container) {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
overflow: visible !important;
|
||||
height: auto !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
/* 确保图片区域正常显示 */
|
||||
.cards-container :deep(.image-wrapper) {
|
||||
|
|
@ -1247,6 +1466,283 @@ onUnmounted(() => {// 禁用轮询
|
|||
.header-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 文本输入弹窗样式 */
|
||||
.text-input-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
padding: 20px;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
html.dark .text-input-overlay {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.text-input-container {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
animation: slideUp 0.3s ease;
|
||||
}
|
||||
|
||||
html.dark .text-input-container {
|
||||
background: #1f2937;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.text-input-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
html.dark .text-input-header {
|
||||
border-bottom: 1px solid #374151;
|
||||
}
|
||||
|
||||
.text-input-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
html.dark .text-input-title {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.text-input-close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
color: #6b7280;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.text-input-close:hover {
|
||||
background: rgba(107, 70, 193, 0.1);
|
||||
color: #6B46C1;
|
||||
}
|
||||
|
||||
html.dark .text-input-close:hover {
|
||||
background: rgba(139, 92, 246, 0.2);
|
||||
color: #A78BFA;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.text-input-area {
|
||||
width: 100%;
|
||||
padding: 16px 20px;
|
||||
border: none;
|
||||
background: #f9fafb;
|
||||
color: #1f2937;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
resize: none;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
html.dark .text-input-area {
|
||||
background: #111827;
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.text-input-area::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
html.dark .text-input-area::placeholder {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.text-input-area:focus {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
html.dark .text-input-area:focus {
|
||||
background: #1f2937;
|
||||
}
|
||||
|
||||
.text-input-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
html.dark .text-input-actions {
|
||||
border-top: 1px solid #374151;
|
||||
}
|
||||
|
||||
.text-input-btn {
|
||||
flex: 1;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.cancel-btn:hover {
|
||||
background: #e5e7eb;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
html.dark .cancel-btn {
|
||||
background: #374151;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
html.dark .cancel-btn:hover {
|
||||
background: #4b5563;
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
background: linear-gradient(135deg, #6B46C1 0%, #A78BFA 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.confirm-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(107, 70, 193, 0.3);
|
||||
}
|
||||
|
||||
html.dark .confirm-btn {
|
||||
background: linear-gradient(135deg, #8B5CF6 0%, #C4B5FD 100%);
|
||||
}
|
||||
|
||||
html.dark .confirm-btn:hover {
|
||||
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4);
|
||||
}
|
||||
|
||||
.text-input-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
.text-input-overlay {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.text-input-container {
|
||||
max-width: 100%;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.text-input-header {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.text-input-title {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.text-input-area {
|
||||
padding: 14px 16px;
|
||||
font-size: 14px;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.text-input-actions {
|
||||
padding: 14px 16px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.text-input-btn {
|
||||
padding: 10px 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.text-input-overlay {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.text-input-container {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.text-input-header {
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.text-input-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.text-input-area {
|
||||
padding: 12px 14px;
|
||||
font-size: 13px;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.text-input-actions {
|
||||
padding: 12px 14px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.text-input-btn {
|
||||
padding: 10px 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.filter-nav {
|
||||
gap: 4px;
|
||||
|
|
@ -1262,6 +1758,13 @@ onUnmounted(() => {// 禁用轮询
|
|||
height: 36px;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
}
|
||||
|
||||
.cards-container {
|
||||
top: 56px;
|
||||
padding: 12px;
|
||||
|
|
@ -1280,6 +1783,13 @@ onUnmounted(() => {// 禁用轮询
|
|||
font-size: 14px;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
.cards-container {
|
||||
top: 60px;
|
||||
padding: 16px;
|
||||
|
|
@ -1294,4 +1804,231 @@ onUnmounted(() => {// 禁用轮询
|
|||
gap: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 空状态页面样式 */
|
||||
.empty-state {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-color, #F3F4F6);
|
||||
}
|
||||
|
||||
.empty-state-content {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
max-width: 500px;
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #1F2937);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.empty-state-description {
|
||||
font-size: 16px;
|
||||
color: var(--text-muted, #6B7280);
|
||||
margin-bottom: 32px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.empty-state-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 14px 28px;
|
||||
background: linear-gradient(135deg, #6B46C1, #A78BFA);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 12px rgba(107, 70, 193, 0.3);
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.empty-state-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(107, 70, 193, 0.4);
|
||||
}
|
||||
|
||||
.empty-state-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.empty-state-tips {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 20px;
|
||||
background: var(--card-bg, #FFFFFF);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color, #E5E7EB);
|
||||
transition: all 0.2s ease;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.tip-item:hover {
|
||||
border-color: var(--primary-color, #6B46C1);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.tip-item svg {
|
||||
color: var(--primary-color, #6B46C1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tip-item span {
|
||||
font-size: 14px;
|
||||
color: var(--text-color, #1F2937);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* 暗色主题适配 */
|
||||
html.dark .empty-state {
|
||||
background: var(--bg-color-dark, #111827);
|
||||
}
|
||||
|
||||
html.dark .empty-state-title {
|
||||
color: var(--text-color-dark, #F3F4F6);
|
||||
}
|
||||
|
||||
html.dark .empty-state-description {
|
||||
color: var(--text-muted-dark, #9CA3AF);
|
||||
}
|
||||
|
||||
html.dark .tip-item {
|
||||
background: var(--card-bg-dark, #1F2937);
|
||||
border-color: var(--border-color-dark, #374151);
|
||||
}
|
||||
|
||||
html.dark .tip-item span {
|
||||
color: var(--text-color-dark, #F3F4F6);
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
.empty-state-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.empty-state-icon svg {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.empty-state-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.empty-state-description {
|
||||
font-size: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.empty-state-button {
|
||||
padding: 12px 24px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
padding: 10px 16px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.tip-item span {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.empty-state-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.empty-state-icon svg {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.empty-state-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.empty-state-description {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.empty-state-button {
|
||||
padding: 10px 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
padding: 8px 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 平板端适配 */
|
||||
@media (min-width: 768px) and (max-width: 1024px) {
|
||||
.empty-state-tips {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,820 @@
|
|||
<template>
|
||||
<Bg>
|
||||
<!-- <div :style="getResponsiveWidthStyle()">
|
||||
<div style="position: sticky;top: 0;">
|
||||
<spline :scene="'https://prod.spline.design/kZDDjO5HuC9GJUM2/scene.splinecode'"/>
|
||||
</div>
|
||||
</div> -->
|
||||
<div style="position: relative;pointer-events:none;" class="min-h-screen flex flex-col w-full selection:bg-purple-500 selection:text-white">
|
||||
<!-- Navbar -->
|
||||
<header
|
||||
:class="[
|
||||
'fixed top-0 left-0 right-0 z-900 transition-all duration-300 pointer-events-auto',
|
||||
isScrolled ? 'bg-black/90 py-4' : 'bg-transparent py-6'
|
||||
]"
|
||||
:style="{ backdropFilter: isScrolled && supportsBackdropFilter ? 'blur(12px)' : 'none' }"
|
||||
>
|
||||
<div class="container mx-auto px-6 flex items-center justify-between">
|
||||
<!-- Logo -->
|
||||
<a href="#" class="text-2xl font-bold tracking-tighter text-white">
|
||||
Deotaland
|
||||
</a>
|
||||
|
||||
<!-- Desktop Nav -->
|
||||
<nav class="hidden md:flex items-center gap-8">
|
||||
<!-- <a
|
||||
href="#"
|
||||
class="text-sm font-medium text-gray-300 hover:text-white transition-colors"
|
||||
>
|
||||
{{ t('nav.creator') }}
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
class="text-sm font-medium text-gray-300 hover:text-white transition-colors"
|
||||
>
|
||||
{{ t('nav.land') }}
|
||||
</a> -->
|
||||
|
||||
<a
|
||||
href="#"
|
||||
class="text-sm font-medium text-gray-300 hover:text-white transition-colors"
|
||||
>
|
||||
{{ t('nav.creator') }}
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
class="text-sm font-medium text-gray-300 hover:text-white transition-colors"
|
||||
>
|
||||
{{ t('nav.done') }}
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
class="text-sm font-medium text-gray-300 hover:text-white transition-colors"
|
||||
>
|
||||
{{ t('nav.about') }}
|
||||
</a>
|
||||
<router-link
|
||||
to="/points-recharge"
|
||||
class="text-sm font-medium text-gray-300 hover:text-white transition-colors"
|
||||
>
|
||||
pricing
|
||||
</router-link>
|
||||
</nav>
|
||||
|
||||
<!-- Right Action & Mobile Toggle -->
|
||||
<div class="flex items-center gap-4">
|
||||
<button
|
||||
@click="$router.push('czhome')"
|
||||
class="hidden md:inline-flex items-center justify-center px-5 py-2 text-sm font-semibold text-black bg-white rounded-full hover:bg-gray-200 transition-colors cursor-pointer"
|
||||
>
|
||||
{{ t('hero.start') }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="md:hidden text-white"
|
||||
@click="isMobileMenuOpen = !isMobileMenuOpen"
|
||||
>
|
||||
<svg v-if="isMobileMenuOpen" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu -->
|
||||
<div v-if="isMobileMenuOpen" class="absolute top-full left-0 right-0 border-t border-gray-800 p-6 flex flex-col gap-4 md:hidden">
|
||||
<!-- <a
|
||||
href="#"
|
||||
class="text-lg font-medium text-gray-300 hover:text-white"
|
||||
>
|
||||
{{ t('nav.creator') }}
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
class="text-lg font-medium text-gray-300 hover:text-white"
|
||||
>
|
||||
{{ t('nav.land') }}
|
||||
</a>
|
||||
<router-link
|
||||
to="/points-recharge"
|
||||
class="text-lg font-medium text-gray-300 hover:text-white"
|
||||
>
|
||||
{{ t('nav.pricing') }}
|
||||
</router-link> -->
|
||||
<a
|
||||
href="#"
|
||||
class="text-lg font-medium text-gray-300 hover:text-white"
|
||||
>
|
||||
{{ t('nav.creator') }}
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
class="text-lg font-medium text-gray-300 hover:text-white"
|
||||
>
|
||||
{{ t('nav.done') }}
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
class="text-lg font-medium text-gray-300 hover:text-white"
|
||||
>
|
||||
{{ t('nav.about') }}
|
||||
</a>
|
||||
<button
|
||||
@click="$router.push('czhome')"
|
||||
class="w-full text-center py-3 text-black bg-white rounded-full font-bold cursor-pointer"
|
||||
>
|
||||
{{ t('hero.start') }}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
<!-- Hero Section -->
|
||||
<div ref="containerRef" class="relative h-[300vh] w-full ">
|
||||
<!-- Sticky Container -->
|
||||
<div class="sticky top-0 h-[100vh] w-full overflow-hidden" :style="{ height: viewportHeight + 'px', position: supportsSticky ? 'sticky' : 'relative' }">
|
||||
|
||||
<!-- Layer 1: Background Animation (Grid) -->
|
||||
<div class="absolute inset-0 flex items-center justify-center overflow-hidden">
|
||||
|
||||
<div
|
||||
|
||||
class="origin-center flex items-center justify-center"
|
||||
>
|
||||
<!-- Grid Layout -->
|
||||
<div v-if="!isMobile" class="grid grid-cols-3 md:grid-cols-5 gap-2 md:gap-3 w-full h-auto p-4" >
|
||||
<!-- --- ROW 1 --- -->
|
||||
<div class="aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center1" class="w-full h-full object-cover" alt="Robot Companion" /></div>
|
||||
<div class="aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center3" class="w-full h-full object-cover" alt="Electronics" /></div>
|
||||
<div :style="{ scale: scale*1.5<=1?1:scale*1.5, zIndex:20 }" class="aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50">
|
||||
<img :src="center" class="w-full h-full object-cover" alt="Main Hero Robot" />
|
||||
<!-- <div style="position: absolute;bottom: 0%;width: 100vw;left: 50%;transform: translateX(-50%);">
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center4" class="w-full h-full object-cover" alt="Retro Bot" /></div>
|
||||
<div class=" md:block aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center2" class="w-full h-full object-cover" alt="Toy Bot" /></div>
|
||||
<div class=" md:block aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center7" class="w-full h-full object-cover" alt="Cyberpunk" /></div>
|
||||
|
||||
<!-- --- ROW 2 (Middle) --- -->
|
||||
<div class="aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center5" class="w-full h-full object-cover" alt="Interactive" /></div>
|
||||
<div class=" md:block aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center6" class="w-full h-full object-cover" alt="Small Bot" /></div>
|
||||
|
||||
<!-- --- CENTER HERO IMAGE (Always Visible) --- -->
|
||||
<!-- :style="{ scale: scale*1.2 }" -->
|
||||
<div class="aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center9" class="w-full h-full object-cover" alt="Glowing Eye" /></div>
|
||||
<div class="hidden md:block aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center8" class="w-full h-full object-cover" alt="3D Print" /></div>
|
||||
<!-- --- ROW 3 --- -->
|
||||
<!-- <div class="hidden md:block aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center1" class="w-full h-full object-cover" alt="Tech Texture" /></div>
|
||||
<div class="aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center2" class="w-full h-full object-cover" alt="Robot Hand" /></div>
|
||||
<div class="aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center6" class="w-full h-full object-cover" alt="Circuit" /></div>
|
||||
<div class="hidden md:block aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center4" class="w-full h-full object-cover" alt="Display" /></div>
|
||||
<div class=" aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center5" class="w-full h-full object-cover" alt="Robotics" /></div> -->
|
||||
</div>
|
||||
<div v-else class="grid grid-cols-3 md:grid-cols-5 gap-2 md:gap-3 w-full h-auto p-4" >
|
||||
<!-- --- ROW 1 --- -->
|
||||
<div class="aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center1" class="w-full h-full object-cover" alt="Robot Companion" /></div>
|
||||
<div class="aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center3" class="w-full h-full object-cover" alt="Electronics" /></div>
|
||||
|
||||
<div class="aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center2" class="w-full h-full object-cover" alt="Retro Bot" /></div>
|
||||
<div class=" md:block aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center4" class="w-full h-full object-cover" alt="Toy Bot" /></div>
|
||||
<div :style="{ scale: scale*1.5<=1?1:scale*1.5, zIndex:20 }" class="aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50">
|
||||
<img :src="center" class="w-full h-full object-cover" alt="Main Hero Robot" />
|
||||
<!-- <div style="position: absolute;bottom: 0%;width: 100vw;left: 50%;transform: translateX(-50%);">
|
||||
</div> -->
|
||||
</div>
|
||||
<div class=" md:block aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center5" class="w-full h-full object-cover" alt="Cyberpunk" /></div>
|
||||
<!-- --- ROW 2 (Middle) --- -->
|
||||
<div class="aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center7" class="w-full h-full object-cover" alt="Interactive" /></div>
|
||||
<div class=" md:block aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center9" class="w-full h-full object-cover" alt="Small Bot" /></div>
|
||||
|
||||
<!-- --- CENTER HERO IMAGE (Always Visible) --- -->
|
||||
<!-- :style="{ scale: scale*1.2 }" -->
|
||||
<div class="aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center8" class="w-full h-full object-cover" alt="Glowing Eye" /></div>
|
||||
<!-- --- ROW 3 --- -->
|
||||
<!-- <div class="hidden md:block aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center1" class="w-full h-full object-cover" alt="Tech Texture" /></div>
|
||||
<div class="aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center2" class="w-full h-full object-cover" alt="Robot Hand" /></div>
|
||||
<div class="aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center6" class="w-full h-full object-cover" alt="Circuit" /></div>
|
||||
<div class="hidden md:block aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center4" class="w-full h-full object-cover" alt="Display" /></div>
|
||||
<div class=" aspect-[9/16] md:aspect-[1/1] rounded-xl overflow-hidden bg-gray-900/50"><img :src="center5" class="w-full h-full object-cover" alt="Robotics" /></div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Layer 2: Static Dark Overlay -->
|
||||
|
||||
<div class="absolute inset-0 z-10 pointer-events-none" />
|
||||
|
||||
<!-- Layer 3: Static Content Layer -->
|
||||
<div class="absolute inset-0 z-20 flex flex-col items-center justify-center pointer-events-none">
|
||||
|
||||
<MotionCom>
|
||||
<div class="pointer-events-auto flex flex-col items-center justify-center text-center px-4 w-full max-w-5xl mx-auto">
|
||||
<h1 class="text-5xl md:text-7xl lg:text-8xl font-bold text-white mb-6 tracking-tighter drop-shadow-2xl">
|
||||
{{ t('hero.title') }}
|
||||
</h1>
|
||||
<p class="text-xl md:text-2xl text-gray-200 mb-10 drop-shadow-lg max-w-2xl font-light">
|
||||
{{ t('hero.subtitle') }}
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-5">
|
||||
<a
|
||||
href="https://deotaland.com"
|
||||
class="px-9 py-4 rounded-full border border-white/50 bg-black/20 text-white font-semibold hover:bg-white hover:text-black transition-all text-lg"
|
||||
:style="{ backdropFilter: supportsBackdropFilter ? 'blur(4px)' : 'none' }"
|
||||
>
|
||||
{{ t('hero.explore') }}
|
||||
</a>
|
||||
<button
|
||||
@click="$router.push('czhome')"
|
||||
class="px-9 py-4 rounded-full bg-white text-black font-semibold hover:bg-gray-200 transition-all text-lg shadow-[0_0_20px_rgba(255,255,255,0.3)] cursor-pointer"
|
||||
>
|
||||
{{ t('hero.start') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</MotionCom>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Creation Canvas Section -->
|
||||
<section class="py-0 md:py-24 relative overflow-hidden">
|
||||
<!-- Background gradient hint -->
|
||||
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[500px] bg-gray-900/50 blur-[120px] rounded-full pointer-events-none" />
|
||||
|
||||
<div class="container mx-auto px-6 relative z-10">
|
||||
<MotionCom>
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-4xl md:text-5xl font-bold mb-4 text-white">
|
||||
{{ t('canvas.title') }}
|
||||
</h2>
|
||||
<p class="text-gray-400 text-lg md:text-xl">
|
||||
{{ t('canvas.subtitle') }}
|
||||
</p>
|
||||
</div>
|
||||
</MotionCom>
|
||||
<MotionCom>
|
||||
<!-- Workflow Visualization Container -->
|
||||
<div class="relative w-full max-w-6xl mx-auto bg-gray-900/40 border border-gray-800 rounded-3xl p-8 md:p-12"
|
||||
:style="{ backdropFilter: supportsBackdropFilter ? 'blur(4px)' : 'none' }"
|
||||
>
|
||||
|
||||
<!-- Responsive Flex Layout: Stack on mobile, Row on desktop -->
|
||||
<div class="flex flex-col md:flex-row items-center justify-between gap-8 md:gap-4">
|
||||
|
||||
<!-- LEVEL 1: INPUTS (Stacked Vertically) -->
|
||||
<div class="flex flex-col gap-6 w-full md:w-auto">
|
||||
<!-- Input A: Prompt -->
|
||||
<div class="flex items-center gap-4 bg-black/80 p-4 rounded-xl border border-gray-800 hover:border-gray-600 transition-colors w-full md:w-64 shadow-lg">
|
||||
<div class="w-12 h-12 rounded-full bg-gray-800 flex items-center justify-center text-purple-400 shrink-0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 7 4 4 20 4 20 7"></polyline><line x1="9" y1="20" x2="15" y2="20"></line><line x1="12" y1="4" x2="12" y2="20"></line></svg>
|
||||
</div>
|
||||
<div>
|
||||
<span class="block text-sm font-bold text-gray-200">{{ t('canvas.prompt') }}</span>
|
||||
<span class="text-xs text-gray-500">"A youthful and lovely girl"</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input B: Reference -->
|
||||
<div class="flex items-center gap-4 bg-black/80 p-4 rounded-xl border border-gray-800 hover:border-gray-600 transition-colors w-full md:w-64 shadow-lg">
|
||||
<div class="w-12 h-12 rounded-full bg-gray-800 flex items-center justify-center text-blue-400 shrink-0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="block text-sm font-bold text-gray-200 mb-1">{{ t('canvas.reference') }}</span>
|
||||
<div class="w-full h-34 rounded-lg overflow-hidden border border-gray-700 relative group">
|
||||
<img :src="refImage" alt="Dog Reference" class="w-full h-full object-cover opacity-80 group-hover:scale-110 transition-transform duration-500" />
|
||||
<div class="absolute bottom-1 right-1 bg-black/60 px-1 rounded text-[10px] text-white">{{ t('canvas.referenceText') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Connector Arrow 1 -->
|
||||
<div class="flex flex-col items-center justify-center text-gray-600">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="rotate-90 md:rotate-0"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg>
|
||||
</div>
|
||||
|
||||
<!-- LEVEL 2: 3D MODEL -->
|
||||
<div class="flex flex-col gap-4 bg-black/80 p-5 rounded-2xl border border-gray-800 w-full md:w-64 shadow-lg hover:border-gray-600 transition-colors">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<div class="w-10 h-10 rounded-full bg-gray-800 flex items-center justify-center text-green-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"></rect><line x1="3" y1="9" x2="21" y2="9"></line><line x1="9" y1="21" x2="9" y2="9"></line></svg>
|
||||
</div>
|
||||
<span class="text-sm font-bold text-gray-200">{{ t('canvas.model3d') }}</span>
|
||||
</div>
|
||||
<div class="w-full aspect-square rounded-xl overflow-hidden border border-gray-700 relative bg-gray-900 group">
|
||||
<img :src="model3dImage" alt="3D Pumpkin Dog" class="w-full h-full object-cover opacity-90 group-hover:scale-110 transition-transform duration-500" />
|
||||
<div class="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/grid-me.png')] opacity-30 pointer-events-none"></div>
|
||||
<div class="absolute bottom-2 left-2 bg-green-900/80 px-2 py-0.5 rounded text-[10px] text-green-100 font-mono">.OBJ Generated</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 text-center">{{ t('canvas.autoRigged') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Connector Arrow 2 -->
|
||||
<div class="flex flex-col items-center justify-center text-gray-600">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="rotate-90 md:rotate-0"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg>
|
||||
</div>
|
||||
|
||||
<!-- LEVEL 3: REAL ROBOT -->
|
||||
<div class="flex flex-col gap-4 bg-gradient-to-b from-purple-900/20 to-blue-900/20 p-5 rounded-2xl border border-purple-500/40 w-full md:w-72 shadow-[0_0_30px_rgba(147,51,234,0.1)]">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<div class="w-10 h-10 rounded-full bg-purple-600 flex items-center justify-center text-white shadow-lg shadow-purple-600/50">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"></rect><circle cx="12" cy="12" r="2"></circle><path d="M7 7h10v10"></path></svg>
|
||||
</div>
|
||||
<span class="text-sm font-bold text-white">{{ t('canvas.realRobot') }}</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-yellow-400 fill-yellow-400 ml-auto animate-pulse"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>
|
||||
</div>
|
||||
<div class="w-full aspect-square rounded-xl overflow-hidden border border-purple-500/30 relative group bg-gray-900">
|
||||
<img :src="realRobotImage" alt="Real Robot Toy" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700" />
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-purple-900/40 to-transparent"></div>
|
||||
</div>
|
||||
<p class="text-xs text-purple-200 text-center font-medium">{{ t('canvas.alive') }}</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</MotionCom>
|
||||
</div>
|
||||
</section>
|
||||
<MotionCom>
|
||||
<!-- Companionship Section -->
|
||||
<section class="py-24 border-t border-gray-900 ">
|
||||
<div class="container mx-auto px-6 flex flex-col items-center text-center">
|
||||
|
||||
<h2 class="text-4xl md:text-6xl font-bold mb-6 tracking-tight">
|
||||
{{ t('companionship.title') }}
|
||||
</h2>
|
||||
|
||||
<h3 class="text-2xl md:text-3xl text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-400 font-semibold mb-8">
|
||||
{{ t('companionship.powered') }}
|
||||
</h3>
|
||||
|
||||
<p class="text-xl text-gray-400 max-w-2xl mb-12 leading-relaxed">
|
||||
{{ t('companionship.subtitle') }}
|
||||
</p>
|
||||
|
||||
<a
|
||||
href="https://deotaland.com"
|
||||
class="pointer-events-auto inline-flex items-center justify-center px-8 py-4 text-base font-bold text-white bg-purple-600 rounded-full hover:scale-105 transition-transform duration-200"
|
||||
>
|
||||
{{ t('companionship.examples') }}
|
||||
</a>
|
||||
|
||||
<!-- Optional Visual Element below -->
|
||||
<div class="mt-16 w-full max-w-4xl h-64 md:h-96 rounded-3xl overflow-hidden relative">
|
||||
<img
|
||||
src="https://draft-user.s3.us-east-2.amazonaws.com/images/41d42aa6-9ada-49b8-8a54-0d2595c0816a.webp"
|
||||
alt="Robot Companion Context"
|
||||
class="w-full h-full object-cover opacity-90 hover:opacity-100 transition-opacity duration-500"
|
||||
/>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/10 via-transparent to-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</MotionCom>
|
||||
<MotionCom>
|
||||
<!-- Middle Text Section -->
|
||||
<section class="py-12 text-center">
|
||||
<div class="container mx-auto px-6 max-w-3xl">
|
||||
<p class="text-lg md:text-xl text-gray-400 leading-relaxed">
|
||||
Explore our community creations and get inspired to build your own unique AI robot companion.
|
||||
Each robot has its own personality and story, waiting to be discovered and loved.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</MotionCom>
|
||||
<MotionCom>
|
||||
<!-- Robot Cards Section -->
|
||||
<section class="py-20 overflow-hidden">
|
||||
<div class="container mx-auto px-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-6xl mx-auto">
|
||||
<div
|
||||
v-for="card in cards"
|
||||
:key="card.id"
|
||||
class="group relative rounded-3xl overflow-hidden aspect-[3/4] cursor-pointer"
|
||||
>
|
||||
<img
|
||||
:src="card.img"
|
||||
:alt="card.title"
|
||||
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
|
||||
/>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-90" />
|
||||
<div class="absolute bottom-0 left-0 p-6">
|
||||
<h3 class="text-xl font-bold text-white mb-1">{{ card.title }}</h3>
|
||||
<p class="text-sm font-medium text-gray-300">{{ card.user }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</MotionCom>
|
||||
<!-- Features Section -->
|
||||
<section class="py-24 bg-gray-900/30">
|
||||
<div class="container mx-auto px-6">
|
||||
|
||||
<div class="mb-16 text-center md:text-left">
|
||||
<h2 class="text-4xl md:text-5xl font-bold text-white mb-4">
|
||||
{{ t('features.title') }}
|
||||
</h2>
|
||||
<p class="text-xl text-gray-400">
|
||||
{{ t('features.subtitle') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-16">
|
||||
<div
|
||||
v-for="(item, idx) in t('features.list')"
|
||||
:key="idx"
|
||||
class="flex flex-col items-start gap-4"
|
||||
>
|
||||
<div class="p-3 bg-gray-800 rounded-xl mb-2">
|
||||
<svg v-if="idx === 0" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-purple-400"><circle cx="12" cy="12" r="10"></circle><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path><path d="M2 12h20"></path></svg>
|
||||
<svg v-else-if="idx === 1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-blue-400"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"></rect><line x1="3" y1="9" x2="21" y2="9"></line><line x1="9" y1="21" x2="9" y2="9"></line></svg>
|
||||
<svg v-else-if="idx === 2" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-yellow-400"><circle cx="12" cy="12" r="10"></circle><path d="M8 14s1.5 2 4 2 4-2 4-2"></path><line x1="9" y1="9" x2="9.01" y2="9"></line><line x1="15" y1="9" x2="15.01" y2="9"></line></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-red-400"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-white">{{ item.title }}</h3>
|
||||
<p class="text-gray-400 leading-relaxed text-lg">
|
||||
{{ item.desc }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Strong Engine Section -->
|
||||
<section class="py-24 ">
|
||||
<div class="container mx-auto px-6 text-center">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
|
||||
{{ t('engine.title') }}
|
||||
</h2>
|
||||
<p class="text-lg text-gray-500 mb-16 max-w-2xl mx-auto">
|
||||
{{ t('engine.subtitle') }}
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 max-w-4xl mx-auto">
|
||||
<div
|
||||
v-for="(name, i) in logos"
|
||||
:key="i"
|
||||
class="h-24 bg-gray-900 rounded-lg flex items-center justify-center border border-gray-800 hover:border-gray-600 transition-colors"
|
||||
>
|
||||
<span class="text-gray-500 font-semibold text-sm">{{ name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Community Call Section -->
|
||||
<section class="py-32 bg-gradient-to-b from-black to-gray-900 border-b border-gray-800">
|
||||
<div class="container mx-auto px-6 text-center">
|
||||
<h2 class="text-5xl md:text-7xl font-bold text-white mb-12 tracking-tighter">
|
||||
{{ t('community.title') }}
|
||||
</h2>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
class="inline-block px-10 py-4 text-lg font-bold text-black bg-white rounded-full hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
{{ t('community.join') }}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<!-- Footer -->
|
||||
<footer class="pointer-events-auto text-white py-16 border-t border-gray-900">
|
||||
<div class="container mx-auto px-6">
|
||||
<div class="flex flex-col md:flex-row justify-between items-start gap-12">
|
||||
|
||||
<!-- Logo -->
|
||||
<div class="mb-4 md:mb-0">
|
||||
<span class="text-2xl font-bold tracking-tighter">Deotaland</span>
|
||||
</div>
|
||||
|
||||
<!-- Links Columns -->
|
||||
<div class="flex flex-wrap gap-12 md:gap-24">
|
||||
|
||||
<!-- Socials -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<h4 class="font-semibold text-gray-500 text-sm uppercase tracking-wider">{{ t('footer.socials') }}</h4>
|
||||
<div class="flex gap-4">
|
||||
<a href="https://x.com/deotaland" class="text-gray-400 hover:text-white"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"></path></svg></a>
|
||||
<a href="https://www.youtube.com/@Deotaland" class="text-gray-400 hover:text-white"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 17a24.12 24.12 0 0 1 0-10 2 2 0 0 1 1.4-1.4 49.56 49.56 0 0 1 16.2 0A2 2 0 0 1 21.5 7a24.12 24.12 0 0 1 0 10 2 2 0 0 1-1.4 1.4 49.55 49.55 0 0 1-16.2 0A2 2 0 0 1 2.5 17"></path><path d="M9.75 15.02v-4.04l5.5 2.02-5.5 2.02Z"></path></svg></a>
|
||||
<a href="https://www.instagram.com/deotaland/" class="text-gray-400 hover:text-white"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="20" x="2" y="2" rx="5" ry="5"></rect><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path><line x1="17.5" x2="17.51" y1="6.5" y2="6.5"></line></svg></a>
|
||||
<a href="https://www.tiktok.com/@deotalandofficial" class="text-gray-400 hover:text-white font-bold text-sm flex items-center h-[20px]">TikTok</a>
|
||||
<a href="https://discord.gg/feuFGPpY" class="text-gray-400 hover:text-white font-bold text-sm flex items-center h-[20px]">Discord</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Language -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<h4 class="font-semibold text-gray-500 text-sm uppercase tracking-wider">{{ t('footer.language') }}</h4>
|
||||
<div class="flex flex-col gap-2">
|
||||
<a href="#" @click.prevent="switchLanguage('en')" class="text-gray-300 hover:text-white text-sm">English</a>
|
||||
<a href="#" @click.prevent="switchLanguage('zh')" class="text-gray-300 hover:text-white text-sm">中文</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Company -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<h4 class="font-semibold text-gray-500 text-sm uppercase tracking-wider">{{ t('footer.company') }}</h4>
|
||||
<div class="flex flex-col gap-2">
|
||||
<a href="#" class="text-gray-300 hover:text-white text-sm">{{ t('footer.about') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<button
|
||||
@click="scrollToTop"
|
||||
class="text-gray-300 hover:text-white text-sm text-left"
|
||||
>
|
||||
{{ t('footer.backTop') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div >
|
||||
|
||||
</div>
|
||||
<div class="mt-16 pt-8 border-t border-gray-900 text-center md:text-left text-sm text-gray-600">
|
||||
{{ t('footer.copyright') }}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</Bg>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MotionCom from './motion.vue'
|
||||
import spline from './spline.vue';
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||
import Bg from './bg.vue'
|
||||
|
||||
const center = 'https://draft-user.s3.us-east-2.amazonaws.com/images/c175585a-20c2-48b3-8939-32bbdb25814b.webp'
|
||||
const center1 = 'https://draft-user.s3.us-east-2.amazonaws.com/images/ecf39871-52c5-45ad-9f9e-6eafd838ce54.webp'
|
||||
const center2 = 'https://draft-user.s3.us-east-2.amazonaws.com/images/f7c4454e-1781-448e-9c70-b087b64f380e.webp'
|
||||
const center3 = 'https://draft-user.s3.us-east-2.amazonaws.com/images/1e74bf93-6be3-46ae-a6d5-5ad02d0b7712.webp'
|
||||
const center4 = 'https://draft-user.s3.us-east-2.amazonaws.com/images/64672da8-a034-4168-b9e7-cda985558f7e.webp'
|
||||
const center5 = 'https://draft-user.s3.us-east-2.amazonaws.com/images/a77f66c4-e7c0-43a6-8e75-0eac49402c06.webp'
|
||||
const center6 = 'https://draft-user.s3.us-east-2.amazonaws.com/images/de3cc66c-909b-4b03-9e73-5180df2bc374.webp'
|
||||
const center7 = 'https://draft-user.s3.us-east-2.amazonaws.com/images/bc62a209-9a54-4d1e-926a-b3ef66fdbd29.webp'
|
||||
const center8 = 'https://draft-user.s3.us-east-2.amazonaws.com/images/9e012193-5576-4a9e-9f38-eecb8705d8a4.webp'
|
||||
const center9 = 'https://draft-user.s3.us-east-2.amazonaws.com/images/65bb1613-0ff9-43c4-a5b9-978c2507ca91.webp'
|
||||
|
||||
const viewportHeight = ref(window.innerHeight);
|
||||
const isMobile = ref(window.innerWidth < 768);
|
||||
|
||||
const supportsSticky = ref(true);
|
||||
const supportsBackdropFilter = ref(true);
|
||||
|
||||
const checkStickySupport = () => {
|
||||
const testEl = document.createElement('div');
|
||||
testEl.style.position = 'sticky';
|
||||
testEl.style.top = '0';
|
||||
supportsSticky.value = testEl.style.position === 'sticky';
|
||||
};
|
||||
|
||||
const checkBackdropFilterSupport = () => {
|
||||
const testEl = document.createElement('div');
|
||||
testEl.style.backdropFilter = 'blur(10px)';
|
||||
supportsBackdropFilter.value = testEl.style.backdropFilter !== '';
|
||||
};
|
||||
// 中英文库
|
||||
const i18n = {
|
||||
en: {
|
||||
nav: {
|
||||
// creator: 'Creator',
|
||||
// land: 'Land',
|
||||
// pricing: 'Pricing',
|
||||
creator: 'Creator',
|
||||
done: 'D one',
|
||||
about: 'About us'
|
||||
},
|
||||
hero: {
|
||||
title: 'Create with Deotaland',
|
||||
subtitle: 'Bring your own AI robot companion to life',
|
||||
explore: 'Explore More',
|
||||
start: 'Start Now'
|
||||
},
|
||||
canvas: {
|
||||
title: 'Your Creation canvas – DeotaBoard',
|
||||
subtitle: 'Build your robot in an infinite canvas, together with AI.',
|
||||
prompt: 'Prompt / Idea',
|
||||
reference: 'Reference',
|
||||
model3d: '3D Model',
|
||||
realRobot: 'Real Robot',
|
||||
autoRigged: 'Auto-rigged mesh ready for core',
|
||||
alive: 'Alive on your desktop',
|
||||
referenceText: 'Reference'
|
||||
},
|
||||
companionship: {
|
||||
title: 'Born for Personal Companionship',
|
||||
powered: 'Powered by AI',
|
||||
subtitle: 'Cute companions, emotional partners, desktop friends... All in one Deotaland DIY robot.',
|
||||
examples: 'See Robot Examples'
|
||||
},
|
||||
features: {
|
||||
title: 'From Zero to Your Own Robot Friend',
|
||||
subtitle: 'Simple enough for beginners, powerful enough for makers.',
|
||||
list: [
|
||||
{ title: 'Stunning Custom Looks', desc: 'Generate unique robot designs from your ideas or images.' },
|
||||
{ title: 'From Idea to 3D Model', desc: 'Turn concepts into printable 3D shells ready for your robot core.' },
|
||||
{ title: 'One-Click Expression Packs', desc: 'Create and apply DIY eye styles and animated expressions.' },
|
||||
{ title: 'Always-On AI Companion', desc: 'Long-term memory, emotional responses, and daily interactions.' }
|
||||
]
|
||||
},
|
||||
engine: {
|
||||
title: 'Strong Engine Behind',
|
||||
subtitle: 'We combine leading AI and 3D technologies to power your DIY robots.'
|
||||
},
|
||||
community: {
|
||||
title: 'Deotaland is for All Creators',
|
||||
join: 'Join the Creator Community'
|
||||
},
|
||||
footer: {
|
||||
socials: 'Socials',
|
||||
language: 'Language',
|
||||
company: 'Company',
|
||||
about: 'About Us',
|
||||
backTop: 'Back to top',
|
||||
copyright: '©2025 Deotaland limited. All rights reserved.'
|
||||
}
|
||||
},
|
||||
zh: {
|
||||
nav: {
|
||||
// creator: '创作者',
|
||||
// land: '社区',
|
||||
// pricing: '价格',
|
||||
creator: 'Creator',
|
||||
done: 'D1',
|
||||
about: 'About'
|
||||
},
|
||||
hero: {
|
||||
title: '使用 Deotaland 创作',
|
||||
subtitle: '让你的 AI 机器人伴侣成为现实',
|
||||
explore: '探索更多',
|
||||
start: '立即开始'
|
||||
},
|
||||
canvas: {
|
||||
title: '你的创作画布 – DeotaBoard',
|
||||
subtitle: '在无限画布中,与 AI 一起打造你的机器人。',
|
||||
prompt: '提示 / 创意',
|
||||
reference: '参考',
|
||||
model3d: '3D 模型',
|
||||
realRobot: '真实机器人',
|
||||
autoRigged: '自动绑定网格,随时准备安装核心',
|
||||
alive: '活跃在你的桌面上',
|
||||
referenceText: '参考'
|
||||
},
|
||||
companionship: {
|
||||
title: '为个人陪伴而生',
|
||||
powered: '由 AI 驱动',
|
||||
subtitle: '可爱的伴侣、情感伙伴、桌面朋友... 尽在 Deotaland DIY 机器人中。',
|
||||
examples: '查看机器人示例'
|
||||
},
|
||||
features: {
|
||||
title: '从零到属于你的机器人朋友',
|
||||
subtitle: '对初学者足够简单,对创客足够强大。',
|
||||
list: [
|
||||
{ title: '惊艳的自定义外观', desc: '从你的想法或图像生成独特的机器人设计。' },
|
||||
{ title: '从想法到 3D 模型', desc: '将概念转化为可打印的 3D 外壳,随时准备安装你的机器人核心。' },
|
||||
{ title: '一键表情包', desc: '创建并应用 DIY 眼睛样式和动画表情。' },
|
||||
{ title: '全天候 AI 伴侣', desc: '长期记忆、情感反应和日常互动。' }
|
||||
]
|
||||
},
|
||||
engine: {
|
||||
title: '强大的引擎支持',
|
||||
subtitle: '我们结合领先的 AI 和 3D 技术,为你的 DIY 机器人提供动力。'
|
||||
},
|
||||
community: {
|
||||
title: 'Deotaland 面向所有创作者',
|
||||
join: '加入创作者社区'
|
||||
},
|
||||
footer: {
|
||||
socials: '社交',
|
||||
language: '语言',
|
||||
company: '公司',
|
||||
about: '关于我们',
|
||||
backTop: '返回顶部',
|
||||
copyright: '©2025 Deotaland 有限公司。保留所有权利。'
|
||||
}
|
||||
}
|
||||
};
|
||||
// Navbar state
|
||||
const isScrolled = ref(false);
|
||||
const isMobileMenuOpen = ref(false);
|
||||
|
||||
// 语言切换逻辑
|
||||
const currentLang = ref(localStorage.getItem('lang') || 'en');
|
||||
|
||||
// 切换语言
|
||||
const switchLanguage = (lang) => {
|
||||
currentLang.value = lang;
|
||||
localStorage.setItem('lang', lang);
|
||||
};
|
||||
|
||||
// 获取当前语言的文本
|
||||
const t = (key) => {
|
||||
const keys = key.split('.');
|
||||
let result = i18n[currentLang.value];
|
||||
for (const k of keys) {
|
||||
if (result && result[k] !== undefined) {
|
||||
result = result[k];
|
||||
} else {
|
||||
return key; // 如果找不到对应的键,返回原始键
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// Hero section state
|
||||
const containerRef = ref(null);
|
||||
const scrollYProgress = ref(0);
|
||||
|
||||
// Scroll event handler
|
||||
const handleScroll = () => {
|
||||
isScrolled.value = window.scrollY > 50;
|
||||
|
||||
if (containerRef.value) {
|
||||
const viewportHeight = window.innerHeight;
|
||||
const scrollPosition = window.scrollY;
|
||||
const containerTop = containerRef.value.offsetTop;
|
||||
const containerHeight = containerRef.value.offsetHeight;
|
||||
|
||||
const progress = Math.max(0, Math.min(1,
|
||||
(scrollPosition - containerTop + viewportHeight) /
|
||||
(containerHeight + viewportHeight)
|
||||
));
|
||||
|
||||
scrollYProgress.value = progress;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle window resize
|
||||
const handleResize = () => {
|
||||
viewportHeight.value = window.innerHeight;
|
||||
isMobile.value = window.innerWidth < 768;
|
||||
};
|
||||
|
||||
// Scroll to top function
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
// Scale transformation based on scroll progress
|
||||
const scale = computed(() => {
|
||||
// Zoom out more aggressively to ensure all 15 images are visible
|
||||
// Final scale is 0.2 for mobile and 0.3 for desktop
|
||||
const mappedProgress = Math.min(scrollYProgress.value / 0.8, 1);
|
||||
let initNum = window.innerWidth < 768 ? 3.5 : 5.5;
|
||||
let outNum = window.innerWidth < 768 ? 3.2 : 5.0;
|
||||
return initNum - (mappedProgress * outNum);
|
||||
});
|
||||
|
||||
// Nav links
|
||||
const navLinks = [
|
||||
{ name: 'Creator', href: '#' },
|
||||
{ name: 'Land', href: '#' },
|
||||
{ name: 'Pricing', href: '#' },
|
||||
];
|
||||
// Creation Canvas Images
|
||||
const refImage = 'https://draft-user.s3.us-east-2.amazonaws.com/images/0185a1f7-563a-4af9-9569-65d81f710c52.webp';
|
||||
const model3dImage = 'https://draft-user.s3.us-east-2.amazonaws.com/images/e5a1408b-b695-431f-b03e-0b9b06f1b82f.webp';
|
||||
const realRobotImage = 'https://draft-user.s3.us-east-2.amazonaws.com/images/ce2f6979-4b3c-499f-b179-80abbf4d7431.webp';
|
||||
|
||||
// Robot Cards
|
||||
const cards = [
|
||||
{ id: 1, title: 'Custom Robot', user: '@Wownny wolf', img:'https://draft-user.s3.us-east-2.amazonaws.com/images/8301d540-a4b2-4346-ac3d-1f9ad8b34bad.webp' },
|
||||
{ id: 2, title: 'Custom Robot', user: '@Lil Moods', img: 'https://draft-user.s3.us-east-2.amazonaws.com/images/c6253d79-47dd-4ced-8806-ded34b7ee184.webp' },
|
||||
{ id: 3, title: 'Custom Robot', user: '@Deo Monkey', img:'https://draft-user.s3.us-east-2.amazonaws.com/images/eb61f9e9-94dd-4920-813d-aa635eb73e24.webp' },
|
||||
];
|
||||
// Strong Engine Logos
|
||||
const logos = [
|
||||
"AI Engine 1", "3D Engine 2", "Voice Engine 3", "Memory Engine 4",
|
||||
"Physics Core", "Render Tech", "Motion API", "Vision OS"
|
||||
];
|
||||
|
||||
// Mount and unmount scroll event
|
||||
onMounted(() => {
|
||||
checkStickySupport();
|
||||
checkBackdropFilterSupport();
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
window.addEventListener('resize', handleResize);
|
||||
handleScroll();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Additional custom styles can be added here */
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -10,8 +10,9 @@
|
|||
<header
|
||||
:class="[
|
||||
'fixed top-0 left-0 right-0 z-900 transition-all duration-300 pointer-events-auto',
|
||||
isScrolled ? 'bg-black/90 backdrop-blur-md py-4' : 'bg-transparent py-6'
|
||||
isScrolled ? 'bg-black/90 py-4' : 'bg-transparent py-6'
|
||||
]"
|
||||
:style="{ backdropFilter: isScrolled && supportsBackdropFilter ? 'blur(12px)' : 'none' }"
|
||||
>
|
||||
<div class="container mx-auto px-6 flex items-center justify-between">
|
||||
<!-- Logo -->
|
||||
|
|
@ -131,7 +132,7 @@
|
|||
<!-- Hero Section -->
|
||||
<div ref="containerRef" class="relative h-[300vh] w-full ">
|
||||
<!-- Sticky Container -->
|
||||
<div class="sticky top-0 h-[100dvh] w-full overflow-hidden">
|
||||
<div class="sticky top-0 h-[100vh] w-full overflow-hidden" :style="{ height: viewportHeight + 'px', position: supportsSticky ? 'sticky' : 'relative' }">
|
||||
|
||||
<!-- Layer 1: Background Animation (Grid) -->
|
||||
<div class="absolute inset-0 flex items-center justify-center overflow-hidden">
|
||||
|
|
@ -218,7 +219,8 @@
|
|||
<div class="flex flex-col sm:flex-row gap-5">
|
||||
<a
|
||||
href="https://deotaland.com"
|
||||
class="px-9 py-4 rounded-full border border-white/50 bg-black/20 backdrop-blur-sm text-white font-semibold hover:bg-white hover:text-black transition-all text-lg"
|
||||
class="px-9 py-4 rounded-full border border-white/50 bg-black/20 text-white font-semibold hover:bg-white hover:text-black transition-all text-lg"
|
||||
:style="{ backdropFilter: supportsBackdropFilter ? 'blur(4px)' : 'none' }"
|
||||
>
|
||||
{{ t('hero.explore') }}
|
||||
</a>
|
||||
|
|
@ -255,7 +257,9 @@
|
|||
</MotionCom>
|
||||
<MotionCom>
|
||||
<!-- Workflow Visualization Container -->
|
||||
<div class="relative w-full max-w-6xl mx-auto bg-gray-900/40 border border-gray-800 rounded-3xl p-8 md:p-12 backdrop-blur-sm">
|
||||
<div class="relative w-full max-w-6xl mx-auto bg-gray-900/40 border border-gray-800 rounded-3xl p-8 md:p-12"
|
||||
:style="{ backdropFilter: supportsBackdropFilter ? 'blur(4px)' : 'none' }"
|
||||
>
|
||||
|
||||
<!-- Responsive Flex Layout: Stack on mobile, Row on desktop -->
|
||||
<div class="flex flex-col md:flex-row items-center justify-between gap-8 md:gap-4">
|
||||
|
|
@ -551,8 +555,7 @@ import MotionCom from './motion.vue'
|
|||
import spline from './spline.vue';
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||
import Bg from './bg.vue'
|
||||
// import dog from '@/assets/home/dog.webp'
|
||||
// import qdog from '@/assets/home/qdog.webp'
|
||||
|
||||
const center = 'https://draft-user.s3.us-east-2.amazonaws.com/images/c175585a-20c2-48b3-8939-32bbdb25814b.webp'
|
||||
const center1 = 'https://draft-user.s3.us-east-2.amazonaws.com/images/ecf39871-52c5-45ad-9f9e-6eafd838ce54.webp'
|
||||
const center2 = 'https://draft-user.s3.us-east-2.amazonaws.com/images/f7c4454e-1781-448e-9c70-b087b64f380e.webp'
|
||||
|
|
@ -563,18 +566,25 @@ const center6 = 'https://draft-user.s3.us-east-2.amazonaws.com/images/de3cc66c-9
|
|||
const center7 = 'https://draft-user.s3.us-east-2.amazonaws.com/images/bc62a209-9a54-4d1e-926a-b3ef66fdbd29.webp'
|
||||
const center8 = 'https://draft-user.s3.us-east-2.amazonaws.com/images/9e012193-5576-4a9e-9f38-eecb8705d8a4.webp'
|
||||
const center9 = 'https://draft-user.s3.us-east-2.amazonaws.com/images/65bb1613-0ff9-43c4-a5b9-978c2507ca91.webp'
|
||||
// Window size reactive state
|
||||
const getResponsiveWidthStyle = () => {
|
||||
const isMobile = window.innerWidth < 768;
|
||||
return {
|
||||
position: 'fixed',
|
||||
width: isMobile ? '300vw' : '100vw',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
transform: 'translate(-50%, -50%)'
|
||||
};
|
||||
};
|
||||
|
||||
const viewportHeight = ref(window.innerHeight);
|
||||
const isMobile = ref(window.innerWidth < 768);
|
||||
|
||||
const supportsSticky = ref(true);
|
||||
const supportsBackdropFilter = ref(true);
|
||||
|
||||
const checkStickySupport = () => {
|
||||
const testEl = document.createElement('div');
|
||||
testEl.style.position = 'sticky';
|
||||
testEl.style.top = '0';
|
||||
supportsSticky.value = testEl.style.position === 'sticky';
|
||||
};
|
||||
|
||||
const checkBackdropFilterSupport = () => {
|
||||
const testEl = document.createElement('div');
|
||||
testEl.style.backdropFilter = 'blur(10px)';
|
||||
supportsBackdropFilter.value = testEl.style.backdropFilter !== '';
|
||||
};
|
||||
// 中英文库
|
||||
const i18n = {
|
||||
en: {
|
||||
|
|
@ -729,17 +739,14 @@ const scrollYProgress = ref(0);
|
|||
|
||||
// Scroll event handler
|
||||
const handleScroll = () => {
|
||||
// isScrolled.value = window.scrollY > 50;
|
||||
isScrolled.value = false;
|
||||
isScrolled.value = window.scrollY > 50;
|
||||
|
||||
// Calculate scroll progress for hero section
|
||||
if (containerRef.value) {
|
||||
const viewportHeight = window.innerHeight;
|
||||
const scrollPosition = window.scrollY;
|
||||
const containerTop = containerRef.value.offsetTop;
|
||||
const containerHeight = containerRef.value.offsetHeight;
|
||||
|
||||
// Calculate progress from 0 to 1 as we scroll through the container
|
||||
const progress = Math.max(0, Math.min(1,
|
||||
(scrollPosition - containerTop + viewportHeight) /
|
||||
(containerHeight + viewportHeight)
|
||||
|
|
@ -749,6 +756,12 @@ const handleScroll = () => {
|
|||
}
|
||||
};
|
||||
|
||||
// Handle window resize
|
||||
const handleResize = () => {
|
||||
viewportHeight.value = window.innerHeight;
|
||||
isMobile.value = window.innerWidth < 768;
|
||||
};
|
||||
|
||||
// Scroll to top function
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
|
|
@ -789,13 +802,16 @@ const logos = [
|
|||
|
||||
// Mount and unmount scroll event
|
||||
onMounted(() => {
|
||||
checkStickySupport();
|
||||
checkBackdropFilterSupport();
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
// Initial call to set initial state
|
||||
window.addEventListener('resize', handleResize);
|
||||
handleScroll();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ const detectEnvironment = async () => {
|
|||
})
|
||||
environmentInfo.value = result
|
||||
isDetecting.value = false
|
||||
if (result.isDomestic) {
|
||||
if (result.isDomestic) {//如果是国内环境
|
||||
handleDomesticRedirect()
|
||||
} else {
|
||||
handleInternationalRedirect()
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@
|
|||
<div class="points-section" >
|
||||
<h2>{{ $t('userCenter.points.title') }}</h2>
|
||||
<!-- 积分明细和规则并排容器 -->
|
||||
|
||||
<div class="points-content-container">
|
||||
<div class="points-list">
|
||||
<div class="total-points">
|
||||
|
|
@ -121,33 +120,10 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- 优惠券区域 -->
|
||||
<div class="voucher-section">
|
||||
<div class="voucher-section" v-if="userData.voucherList.length > 0">
|
||||
<h2>{{ $t('userCenter.voucher.title') }}</h2>
|
||||
|
||||
<!-- 优惠券数量统计 -->
|
||||
<!-- <div class="voucher-count-container">
|
||||
<div class="voucher-count-stats">
|
||||
<div class="count-item">
|
||||
<span class="count-label">{{ $t('userCenter.voucher.availableCount') }}</span>
|
||||
<span class="count-value available">{{ userData.voucherCount.availableCount }}</span>
|
||||
</div>
|
||||
<div class="count-item">
|
||||
<span class="count-label">{{ $t('userCenter.voucher.usedCount') }}</span>
|
||||
<span class="count-value used">{{ userData.voucherCount.usedCount }}</span>
|
||||
</div>
|
||||
<div class="count-item">
|
||||
<span class="count-label">{{ $t('userCenter.voucher.expiredCount') }}</span>
|
||||
<span class="count-value expired">{{ userData.voucherCount.expiredCount }}</span>
|
||||
</div>
|
||||
<div class="count-item">
|
||||
<span class="count-label">{{ $t('userCenter.voucher.totalCount') }}</span>
|
||||
<span class="count-value total">{{ userData.voucherCount.totalCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- 优惠券列表 -->
|
||||
<div class="voucher-list-container">
|
||||
<div class="voucher-list-container" >
|
||||
<div v-if="loading" class="loading-container">
|
||||
<el-skeleton :rows="3" animated />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
# Dockerfile.simple
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /dist
|
||||
|
||||
COPY . .
|
||||
RUN npm install && npm run build
|
||||
|
||||
# 挂载点提示
|
||||
CMD ["echo", "Dist files are ready in /build/dist"]
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# Copyright (c) 1993-2009 Microsoft Corp.
|
||||
#
|
||||
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
|
||||
#
|
||||
# This file contains the mappings of IP addresses to host names. Each
|
||||
# entry should be kept on an individual line. The IP address should
|
||||
# be placed in the first column followed by the corresponding host name.
|
||||
# The IP address and the host name should be separated by at least one
|
||||
# space.
|
||||
#
|
||||
# Additionally, comments (such as these) may be inserted on individual
|
||||
# lines or following the machine name denoted by a '#' symbol.
|
||||
#
|
||||
# For example:
|
||||
#
|
||||
# 102.54.94.97 rhino.acme.com # source server
|
||||
# 38.25.63.10 x.acme.com # x client host
|
||||
|
||||
# localhost name resolution is handled within DNS itself.
|
||||
# 127.0.0.1 localhost
|
||||
# ::1 localhost
|
||||
|
||||
192.168.0.190 admin.deotaland.local
|
||||
192.168.0.190 www.deotaland.local
|
||||
192.168.0.190 api.deotaland.local
|
||||
192.168.0.190 git.deotaland.local
|
||||
|
|
@ -6,5 +6,11 @@ const login = {
|
|||
OAUTH_GOOGLE:{url:'/api-base/user/oauth/google',method:'POST',isLoading:true},// google弹窗授权
|
||||
FORGOT_PASSWORD:{url:'/api-base/user/forgot-password',method:'POST',isLoading:true},// 修改密码
|
||||
REFRESH_TOKEN:{url:'/api-base/user/oauth/google/refresh',method:'POST'},// googleRefreshToken刷新
|
||||
SEND_SMS_CODE:{url:'/api-base/user/send-sms-code',method:'POST'},// 发送短信验证码
|
||||
REGISTER_PHONE:{url:'/api-base/user/register-phone',method:'POST'},// 使用手机号和短信验证码注册新账号
|
||||
LOGIN_PHONE:{url:'/api-base/user/login-phone',method:'POST'},// 使用手机号和密码登录
|
||||
RESET_PASSWORD_PHONE:{url:'/api-base/user/reset-password-phone',method:'POST'},// 使用手机号和短信验证码重置密码
|
||||
LOGIN_PHONE_CODE:{url:'/api-base/user/login-phone-code',method:'POST'},// 使用手机号和短信验证码登录
|
||||
|
||||
}
|
||||
export default login;
|
||||
|
|
|
|||
Loading…
Reference in New Issue