This commit is contained in:
13121765685 2026-01-05 17:30:17 +08:00
parent 4c59a1349d
commit 1ffc1195e6
32 changed files with 2029 additions and 174 deletions

View File

@ -5,16 +5,16 @@ import { useAuthStore } from './stores/index.js'
import App from './App.vue'
// 导入样式
import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import './assets/styles/global.css'
import './assets/styles/responsive.css'
import './assets/styles/themes.css'
import dtUI from '@deotaland/ui'
import '@deotaland/ui/style.css'
import 'element-plus/dist/index.css'
// 导入Element Plus图标
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import ElementPlus from 'element-plus'
// 导入i18n配置
import i18n from './locales/i18n'
// 创建应用实例
@ -26,6 +26,7 @@ window.setElMessage = (options={})=>{
// 配置 Pinia
const pinia = createPinia()
app.use(pinia)
// app.use(ElementPlus)
// 配置国际化
app.use(i18n)
// 注册所有Element Plus图标

View File

@ -1,11 +1,10 @@
import { createRouter, createWebHistory,createWebHashHistory } from 'vue-router'
import i18n from '@/locales/i18n'
import { watch } from 'vue'
import About from '@/views/About.vue'
import NotFound from '@/views/NotFound.vue'
import AdminLogin from '@/views/AdminLogin/AdminLogin.vue'
// 管理员布局组件(懒加载)
const About = ()=>import('@/views/About.vue')
const NotFound = ()=>import('@/views/NotFound.vue')
const AdminLogin = ()=>import('@/views/AdminLogin/AdminLogin.vue')
const AdminLayout = () => import('@/components/admin/AdminLayout.vue')
const AdminDashboard = () => import('@/views/admin/AdminDashboard.vue')
const AdminContent = () => import('@/views/admin/AdminContent.vue')
@ -70,7 +69,17 @@ export const permissionRoutes = [
requiresAuth: true,
},
},
{
path: 'disassembly-orders',
name: 'AdminDisassemblyOrders',
component: AdminDisassemblyOrders,
meta: {
title: 'admin.layout.disassemblyOrders',
icon: 'EditPen',
menuOrder: 2,
requiresAuth: true,
}
},
{
path: 'order-list',
name: 'AdminOrdersList',
@ -225,17 +234,7 @@ const routes = [
requiresAuth: true
},
children: [
{
path: 'disassembly-orders',
name: 'AdminDisassemblyOrders',
component: AdminDisassemblyOrders,
meta: {
title: 'admin.layout.disassemblyOrders',
icon: 'EditPen',
menuOrder: 2,
requiresAuth: true,
}
},
{
path: 'disassembly-orders/:id',
name: 'AdminDisassemblyDetail',

View File

@ -113,9 +113,9 @@
<el-table-column :label="$t('admin.review.actions')" min-width="300" align="center" fixed="right">
<template #default="{ row }">
<div class="actions-container">
<el-button size="small" @click="previewModel(row)">
<!-- <el-button size="small" @click="previewModel(row)">
{{ t('admin.review.preview3D') }}
</el-button>
</el-button> -->
<el-button
size="small"
type="primary"
@ -438,7 +438,7 @@ const approveReview = async (review) => {
if(res.code==0){
ElMessage.success(t('admin.review.approveSuccess'))
//
router.push(`/admin/disassembly-orders?order_no=${review.order_no}`)
router.push(`/admin/orders/disassembly-orders?order_no=${review.order_no}`)
}
})
} catch {

View File

@ -58,7 +58,7 @@
<div v-if="currentStep >= 1" class="step-content">
<div class="preview-container-horizontal">
<div class="preview-images-horizontal">
<div v-if="thumbnailUrl" class="image-item-horizontal" @click="previewImage(thumbnailUrl)">
<div class="image-item-horizontal" @click="previewImage(thumbnailUrl)">
<img :src="thumbnailUrl" alt="缩略图" />
<div class="image-label">{{ $t('admin.disassemblyOrders.detail.preview') }}</div>
<div class="image-actions">
@ -71,7 +71,7 @@
</el-button>
</div>
</div>
<div class="image-item-horizontal" @click="previewModel(modelUrl)">
<div v-if="modelUrl" class="image-item-horizontal" @click="previewModel(modelUrl)">
<div class="model-preview-horizontal">
<el-icon size="48"><View /></el-icon>
<span>{{ $t('admin.disassemblyOrders.detail.previewDialog') }}</span>
@ -80,7 +80,10 @@
</div>
</div>
<div class="prompt-template-container">
<div class="prompt-template-label">拆件提示词模板</div>
<div class="warning-hint" style="margin-left: auto; display: flex; align-items: center; gap: 6px;">
<el-icon color="#f2ac34"><WarningFilled /></el-icon>
<span style="color: #f2ac34; font-size: 13px; line-height: 1.5;">拆件提示词模板作用于拆件按钮的提示词</span>
</div>
<el-input
v-model="cjtsc"
type="textarea"
@ -90,7 +93,10 @@
></el-input>
</div>
<div class="prompt-template-container">
<div class="prompt-template-label">合并提示词模板</div>
<div class="warning-hint" style="margin-left: auto; display: flex; align-items: center; gap: 6px;">
<el-icon color="#f2ac34"><WarningFilled /></el-icon>
<span style="color: #f2ac34; font-size: 13px; line-height: 1.5;">合并提示词模板作用于合并按钮的提示词</span>
</div>
<el-input
v-model="hbtsc"
type="textarea"
@ -99,12 +105,26 @@
class="prompt-template-textarea"
></el-input>
</div>
<div class="disassembly-button-container">
<div class="button-container">
<div class="button-with-hint">
<el-button
type="primary"
size="large"
@click="generateFullModel"
>
生成完整模型
</el-button>
<div class="button-hint" style="display: flex; align-items: center; gap: 6px; margin-left: 16px;">
<el-icon style="--color: #f2ac34;"><WarningFilled /></el-icon>
<span style="color: #f2ac34; font-size: 13px; line-height: 1.5;">生成完整模型适用于不用拆件的E系列产品</span>
</div>
</div>
<el-button
type="primary"
size="large"
:loading="disassemblyLoading"
@click="startDisassembly"
style="margin-left: 16px;"
>
{{ $t('admin.disassemblyOrders.detail.disassembly') }}
</el-button>
@ -113,7 +133,6 @@
</div>
</div>
</el-timeline-item>
<!-- 第二步展示已拆件图片 -->
<el-timeline-item
:color="currentStep >= 2 ? '#6B46C1' : '#e4e7ed'"
@ -121,7 +140,13 @@
>
<div class="timeline-content" style="z-index: 3;">
<div class="step-header">
<h3>{{ $t('admin.disassemblyOrders.detail.step2') }}</h3>
<div class="step-title-with-hint">
<h3>{{ $t('admin.disassemblyOrders.detail.step2') }}</h3>
<div class="warning-hint" style="display: flex; align-items: center; gap: 6px;">
<el-icon style="--color: #f2ac34;"><WarningFilled /></el-icon>
<span style="color: #f2ac34; font-size: 13px; line-height: 1.5;">点击拆件后的内容将在这里展示</span>
</div>
</div>
<el-button
v-if="currentStep >= 2"
type="primary"
@ -153,11 +178,7 @@
@edit="(result)=>editImage(index,result)"
@partial-edit="(imageUrl)=>handlePartialEdit(imageUrl, index)"
/>
<!-- <div class="image-label">{{ $t('admin.disassemblyOrders.detail.preview') }} {{ index + 1 }}</div> -->
<!-- 选择框 -->
<!-- <div v-if="mergeMode" class="image-checkbox">
<el-checkbox v-model="selectedImages" :label="index" @change="handleCheckboxChange"></el-checkbox>
</div> -->
<div v-if="mergeMode" @click="mergeMode && toggleImageSelection(index)" style="position: absolute;width: 100%;height: 100%;z-index: 2;"></div>
</div>
</div>
@ -217,70 +238,9 @@
<div class="image-label">模型 {{ index + 1 }}</div>
</div>
</div>
<!-- <div class="step-actions">
<el-button
type="primary"
@click="handleProcessingComplete"
:loading="processingCompleteLoading"
>
加工完成
</el-button>
</div> -->
</div>
</div>
</el-timeline-item>
<!-- 第四步工期信息 -->
<!-- <el-timeline-item
:color="currentStep >= 4 ? '#6B46C1' : '#e4e7ed'"
size="large"
>
<div class="timeline-content">
<h3>工期信息</h3>
<div v-if="currentStep >= 4" class="step-content">
<div class="work-period-info">
<div class="info-card">
<div class="card-content">
<div class="info-icon">
<el-icon><Calendar /></el-icon>
</div>
<div class="info-content">
<div class="info-label">开始工期</div>
<div class="info-value">{{ formatDate(orderDetail.createTime) }}</div>
</div>
</div>
</div>
<div class="info-card">
<div class="card-content">
<div class="info-icon">
<el-icon><Check /></el-icon>
</div>
<div class="info-content">
<div class="info-label">完成工期</div>
<div class="info-value">{{ formatDate(orderDetail.processingCompleteTime) }}</div>
</div>
</div>
</div>
<div class="info-card total-days-card">
<div class="card-content">
<div class="info-icon">
<el-icon><Timer /></el-icon>
</div>
<div class="info-content">
<div class="info-label">共计天数</div>
<div class="info-value">{{ calculateTotalDays() }} </div>
</div>
</div>
</div>
</div>
<div class="step-actions">
<el-button @click="goBack">返回列表</el-button>
</div>
</div>
</div>
</el-timeline-item> -->
</el-timeline>
</div>
</div>
@ -321,7 +281,7 @@
</el-dialog>
<!-- 画布编辑器对话框 -->
<DtCanvasEditor
v-model:visible="canvasEditorVisible"
v-model:visible="canvasEditorVisible"
:image-url="canvasEditorImageUrl"
@add-prompt-card="handleCanvasSave"
/>
@ -349,7 +309,8 @@ import {
Timer,
Plus,
Loading,
Upload
Upload,
WarningFilled
} from '@element-plus/icons-vue'
import ModelViewer from '@/components/common/ModelViewer.vue'
import ModelCom from '@/components/modelCom/index.vue'
@ -585,6 +546,17 @@ const handleDisassembly = () => {
// }
}
//
const generateFullModel = () => {
const newModel = {
id: new Date().getTime(),
image: thumbnailUrl.value,
taskId: ''
};
// 使
generatedModels.value = [...generatedModels.value, newModel];
}
const deleteModel = (index) => {
generatedModels.value.splice(index, 1)
@ -898,13 +870,26 @@ const generateModelFromImage = async (image) => {
font-size: 18px;
font-weight: 600;
color: #1f2937;
margin: 0 0 16px 0;
margin: 0;
display: flex;
align-items: center;
/* position: relative; */
/* top: -1px; */
}
.step-title-with-hint {
display: flex;
align-items: center;
gap: 16px;
}
.step-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
/* 调整Element Plus时间轴组件的节点样式 */
:deep(.el-timeline-item__wrapper) {
padding-left: 28px;
@ -1018,12 +1003,29 @@ const generateModelFromImage = async (image) => {
color: #6b7280;
}
.disassembly-button-container {
.button-container {
display: flex;
justify-content: flex-end;
justify-content: space-between;
align-items: center;
margin-top: 16px;
}
.button-container-left {
display: flex;
align-items: center;
gap: 24px;
}
.button-with-hint {
display: flex;
align-items: center;
}
.button-hint {
max-width: 300px;
line-height: 1.5;
}
/* 提示词模板在预览容器中的样式调整 */
.preview-container-horizontal .prompt-template-container {
margin-bottom: 0;
@ -1321,6 +1323,35 @@ const generateModelFromImage = async (image) => {
flex-direction: column;
}
.button-container {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.button-with-hint {
flex-direction: column;
align-items: flex-start;
width: 100%;
}
.button-with-hint .el-button {
width: 100%;
margin-left: 0 !important;
margin-bottom: 12px;
}
.button-hint {
text-align: left;
width: 100%;
margin-left: 0 !important;
}
.button-container .el-button {
width: 100%;
margin-left: 0 !important;
}
.disassembly-button-container {
justify-content: center;
}

View File

@ -85,13 +85,11 @@
:label="$t('admin.disassemblyOrders.list.orderNumber')"
width="150"
/>
<el-table-column
prop="creatorName"
:label="$t('admin.disassemblyOrders.list.creatorName')"
width="120"
/>
<el-table-column
prop="status"
:label="$t('admin.disassemblyOrders.list.status')"
@ -155,7 +153,6 @@
@current-change="handleCurrentChange"
/>
</div>
<!-- 图片预览对话框 -->
<el-dialog
v-model="imagePreviewVisible"

View File

@ -32,6 +32,7 @@ export class AdminOrders {
}
//获取订单列表
getOrderList(params){
params.source_type = 0
return requestUtils.common(adminApi.default.getOrderList,params);
}
//同意退款

View File

@ -34,8 +34,8 @@ export default defineConfig({
// 配置代理解决CORS问题
proxy: {
'/api': {
// target: 'https://api.deotaland.ai',
target: 'http://api.deotaland.local',
target: 'https://api.deotaland.ai',
// target: 'http://api.deotaland.local',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}

View File

@ -117,6 +117,33 @@ body,html{
height: 2px !important; /* 高度 */
border-radius: 0 !important;
}
.el-button--primary,
.el-tour-indicator.is-active
{
border:#9c7eef !important;
background-color: #9c7eef !important;
}
.el-tour__content{
background: linear-gradient(135deg, #ffffff 0%, #f8f7ff 100%) !important;
border-radius: 16px !important;
padding: 32px !important;
max-width: 480px !important;
box-shadow: 0 4px 20px rgba(107, 70, 193, 0.15) !important;
animation: fadeInUp 0.6s ease-out !important;
transition: transform 0.3s ease, box-shadow 0.3s ease !important;
}
.el-tour__content:hover {
transform: translateY(-4px) !important;
box-shadow: 0 8px 30px rgba(107, 70, 193, 0.25) !important;
}
/* 暗色主题适配 */
html.dark .el-tour__content {
background: linear-gradient(135deg, #1e1e1e 0%, #2d2a40 100%) !important;
box-shadow: 0 4px 20px rgba(156, 126, 239, 0.25) !important;
}
html.dark .el-tour__content:hover {
box-shadow: 0 8px 30px rgba(156, 126, 239, 0.35) !important;
}
</style>
<style scoped>
header strong { font-size: 1.25rem; }

View File

@ -36,7 +36,7 @@
</div>
<div class="header-right-section">
<div class="free-counts-display" >
<div ref="RechargeRef" class="free-counts-display" >
<div class="model-count">
<el-icon class="count-icon">
<MagicStick />
@ -72,7 +72,8 @@ import { Picture, MagicStick, ArrowLeft, Edit, Check, Guide } from '@element-plu
import { ElButton, ElIcon, ElInput } from 'element-plus'
import ThemeToggle from '../ui/ThemeToggle.vue'
import LanguageToggle from '../ui/LanguageToggle.vue'
const emit = defineEmits(['openGuideModal','back'])
const emit = defineEmits(['openGuideModal','back']);
const RechargeRef = ref(null)
const props = defineProps({
total_score: {
type: Number,
@ -126,6 +127,10 @@ const handleSave = () => {
}
isEditing.value = false
}
//
defineExpose({
RechargeRef,
})
</script>

View File

@ -89,7 +89,7 @@
<el-form :model="shipping" label-width="auto" class="shipping-form">
<div class="form-row">
<el-form-item :label="$t('checkout.country')">
<el-select v-model="shipping.country" :placeholder="$t('checkout.chooseCountry')">
<el-select v-model="shipping.country" :placeholder="$t('checkout.chooseCountry')" filterable>
<el-option v-for="c in countryOptions" :key="c.value" :label="c.label" :value="c.value" />
</el-select>
</el-form-item>
@ -104,7 +104,7 @@
</div>
<div class="form-row">
<el-form-item :label="$t('checkout.postalCode')">
<el-input v-model="shipping.postalCode" :placeholder="$t('checkout.postalCode')">
<el-input v-model="shipping.postalCode" :placeholder="`${$t('checkout.postalCode')} (${$t('common.optional')})`">
<template #suffix>
<el-icon><Search /></el-icon>
</template>
@ -211,7 +211,6 @@ const isPayButtonDisabled = computed(() => {
return !(
shipping.value.firstName.trim() &&
shipping.value.lastName.trim() &&
shipping.value.postalCode.trim() &&
shipping.value.state.trim() &&
shipping.value.city.trim() &&
shipping.value.address1.trim() &&
@ -306,7 +305,14 @@ const getCountryLabel = (code, name) => {
}
const updateCountryOptions = () => {
countryOptions.value = (Country.getAllCountries() || []).map(c => ({ label: getCountryLabel(c.isoCode, c.name), value: c.isoCode }))
const allCountries = (Country.getAllCountries() || []).map(c => ({ label: getCountryLabel(c.isoCode, c.name), value: c.isoCode }))
//
const chinaOption = allCountries.find(c => c.value === 'CN')
if (chinaOption) {
countryOptions.value = [chinaOption, ...allCountries.filter(c => c.value !== 'CN')]
} else {
countryOptions.value = allCountries
}
}
onMounted(() => {

View File

@ -1,7 +1,7 @@
<template>
<aside class="floating-sidebar">
<!-- IP类型选择 -->
<div class="form-section" >
<div class="form-section" ref="ipTypeSectionRef">
<div class="expression-info">
<span class="expression-description">
{{ $t('iPandCardLeft.ipType') }}
@ -77,7 +77,7 @@
</div>
</div>
<!-- 文本提示输入 -->
<div class="form-section">
<div class="form-section" ref="textPromptSectionRef">
<div class="expression-info">
<span class="expression-description">
{{ $t('iPandCardLeft.textPrompt') }}
@ -108,7 +108,7 @@
</div>
</div>
<!-- 参考图片上传 -->
<div class="form-section">
<div class="form-section" ref="referenceImageSectionRef">
<div class="expression-info">
<span class="expression-description">
{{ $t('iPandCardLeft.addReferenceImage') }}
@ -284,7 +284,7 @@
</div>
</div>
<!-- 生成按钮 -->
<div class="generate-section">
<div class="generate-section" ref="generateButtonRef">
<el-button
type="primary"
class="generate-btn"
@ -311,6 +311,10 @@ const props = defineProps({
Info: {
type: Object,
default: () => ({})
},
series: {
type: String,
default: 'E1'
}
})
// i18n
@ -328,6 +332,11 @@ const generateCount = ref(1); // 生成数量默认为1
const isOptimizing = ref(false); //
const isDragOver = ref(false); //
const isUploading = ref(false); //
// ref
const ipTypeSectionRef = ref(null);
const textPromptSectionRef = ref(null);
const referenceImageSectionRef = ref(null);
const generateButtonRef = ref(null);
// IP/
const ipType = ref(1);
const handleIpTypeSelect = (type) => {
@ -410,6 +419,7 @@ const init = () => {
onMounted(() => {
init()
// loadExpressions();
autoResizeTextarea();
});
// -
@ -874,9 +884,12 @@ watch(() => formData.value.prompt, () => {
});
});
//
onMounted(() => {
autoResizeTextarea();
// ref
defineExpose({
ipTypeSectionRef,
textPromptSectionRef,
referenceImageSectionRef,
generateButtonRef
});
</script>

View File

@ -0,0 +1,243 @@
<template>
<div class="tour-container">
<div class="tour-card">
<div class="icon-wrapper">
<svg class="star-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<MagicStick />
</svg>
</div>
<h3 class="title">{{ $t('tour1.title') }}</h3>
<p class="description">{{ $t('tour1.description') }}</p>
<ul class="feature-list">
<li class="feature-item">
<span class="feature-icon"></span>
<span>{{ $t('tour1.featureText') }}</span>
</li>
</ul>
<div class="action-section">
<p class="action-text">{{ $t('tour1.actionText') }}</p>
<p class="action-highlight">{{ $t('tour1.actionHighlight') }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import {MagicStick} from '@element-plus/icons-vue'
const { t } = useI18n()
const isVisible = ref(false)
onMounted(() => {
setTimeout(() => {
isVisible.value = true
}, 100)
})
</script>
<style scoped>
.tour-container {
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
min-height: 300px;
}
.icon-wrapper {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.star-icon {
width: 48px;
height: 48px;
color: #6B46C1;
animation: float 3s ease-in-out infinite;
}
.title {
font-size: 24px;
font-weight: 600;
color: #1F2937;
text-align: center;
margin: 0 0 16px 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.description {
font-size: 15px;
color: #4B5563;
text-align: center;
margin-bottom: 20px;
line-height: 1.6;
}
.feature-list {
list-style: none;
padding: 0;
margin: 0 0 24px 0;
}
.feature-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: rgba(107, 70, 193, 0.05);
border-radius: 8px;
margin-bottom: 8px;
transition: all 0.3s ease;
cursor: default;
}
.feature-item:hover {
background: rgba(107, 70, 193, 0.1);
transform: translateX(4px);
}
.feature-icon {
font-size: 18px;
animation: pulse 2s ease-in-out infinite;
}
.feature-item span:last-child {
font-size: 14px;
color: #374151;
font-weight: 500;
}
.action-section {
text-align: center;
padding-top: 16px;
border-top: 1px solid rgba(107, 70, 193, 0.1);
}
.action-text {
font-size: 14px;
color: #6B7280;
margin: 0 0 8px 0;
}
.action-highlight {
font-size: 16px;
font-weight: 600;
color: #6B46C1;
margin: 0;
background: linear-gradient(135deg, #6B46C1 0%, #A78BFA 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: shimmer 2s ease-in-out infinite;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
@keyframes shimmer {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.8;
}
}
@media (max-width: 768px) {
.tour-container {
padding: 16px;
}
.tour-card {
padding: 24px;
max-width: 100%;
}
.title {
font-size: 20px;
}
.star-icon {
width: 40px;
height: 40px;
}
}
@media (min-width: 768px) and (max-width: 1024px) {
.tour-card {
max-width: 380px;
}
}
html.dark .star-icon {
color: #A78BFA;
}
html.dark .title {
color: #F3F4F6;
}
html.dark .description {
color: #D1D5DB;
}
html.dark .feature-item {
background: rgba(139, 92, 246, 0.1);
}
html.dark .feature-item:hover {
background: rgba(139, 92, 246, 0.2);
}
html.dark .feature-item span:last-child {
color: #E5E7EB;
}
html.dark .action-section {
border-top: 1px solid rgba(139, 92, 246, 0.2);
}
html.dark .action-text {
color: #9CA3AF;
}
html.dark .action-highlight {
background: linear-gradient(135deg, #A78BFA 0%, #C4B5FD 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
</style>

View File

@ -0,0 +1,393 @@
<template>
<div class="tour-container">
<div class="tour-card">
<div class="icon-wrapper">
<svg class="star-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h3 class="title">{{ $t('tour2.title') }}</h3>
<p class="description">{{ $t('tour2.description') }}</p>
<div class="ip-types-container">
<div class="ip-type-item">
<div class="type-icon character">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="8" r="4" stroke="currentColor" stroke-width="2"/>
<path d="M4 20C4 16 7 14 12 14C17 14 20 16 20 20" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
<div class="type-info">
<h4 class="type-title">{{ $t('iPandCardLeft.character') }}</h4>
<p class="type-desc">{{ $t('tour2.characterDesc') }}</p>
</div>
</div>
<div class="ip-type-item">
<div class="type-icon animal">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C8 2 6 4 6 7V9H4V11H6V13H4V15H6V17C6 20 8 22 12 22C16 22 18 20 18 17V15H20V13H18V11H20V9H18V7C18 4 16 2 12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="9" cy="9" r="1.5" fill="currentColor"/>
<circle cx="15" cy="9" r="1.5" fill="currentColor"/>
</svg>
</div>
<div class="type-info">
<h4 class="type-title">{{ $t('iPandCardLeft.animal') }}</h4>
<p class="type-desc">{{ $t('tour2.animalDesc') }}</p>
</div>
</div>
</div>
<div class="action-section">
<p class="action-text">{{ $t('tour2.actionText') }}</p>
<p class="action-highlight">{{ $t('tour2.actionHighlight') }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<style scoped>
.tour-container {
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
.icon-wrapper {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.star-icon {
width: 48px;
height: 48px;
color: #6B46C1;
animation: float 3s ease-in-out infinite;
}
.title {
font-size: 24px;
font-weight: 600;
color: #1F2937;
text-align: center;
margin: 0 0 12px 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.description {
font-size: 15px;
color: #4B5563;
text-align: center;
margin-bottom: 24px;
line-height: 1.6;
}
.ip-types-container {
display: flex;
gap: 16px;
margin-bottom: 24px;
}
.ip-type-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 16px;
background: rgba(107, 70, 193, 0.05);
border-radius: 12px;
transition: all 0.3s ease;
cursor: default;
}
.ip-type-item:hover {
background: rgba(107, 70, 193, 0.1);
transform: translateY(-2px);
}
.type-icon {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
margin-bottom: 12px;
transition: all 0.3s ease;
}
.type-icon.character {
background: linear-gradient(135deg, #6B46C1 0%, #A78BFA 100%);
color: white;
}
.type-icon.animal {
background: linear-gradient(135deg, #F59E0B 0%, #FBBF24 100%);
color: white;
}
.type-icon svg {
width: 28px;
height: 28px;
}
.type-info {
text-align: center;
}
.type-title {
font-size: 16px;
font-weight: 600;
color: #1F2937;
margin: 0 0 8px 0;
}
.type-desc {
font-size: 13px;
color: #6B7280;
margin: 0;
line-height: 1.4;
}
.comparison-section {
background: rgba(107, 70, 193, 0.03);
border-radius: 12px;
padding: 20px;
margin-bottom: 24px;
}
.comparison-title {
font-size: 16px;
font-weight: 600;
color: #1F2937;
margin: 0 0 16px 0;
text-align: center;
}
.comparison-grid {
display: flex;
flex-direction: column;
gap: 12px;
}
.comparison-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.comparison-label {
font-size: 13px;
font-weight: 500;
color: #6B7280;
text-align: center;
}
.comparison-value {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
font-size: 13px;
}
.character-value {
color: #6B46C1;
font-weight: 500;
}
.animal-value {
color: #F59E0B;
font-weight: 500;
}
.vs {
font-size: 11px;
color: #9CA3AF;
font-weight: 600;
padding: 2px 8px;
background: rgba(156, 163, 175, 0.1);
border-radius: 4px;
}
.action-section {
text-align: center;
padding-top: 16px;
border-top: 1px solid rgba(107, 70, 193, 0.1);
}
.action-text {
font-size: 14px;
color: #6B7280;
margin: 0 0 8px 0;
}
.action-highlight {
font-size: 16px;
font-weight: 600;
color: #6B46C1;
margin: 0;
background: linear-gradient(135deg, #6B46C1 0%, #A78BFA 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: shimmer 2s ease-in-out infinite;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
@keyframes shimmer {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.8;
}
}
@media (max-width: 768px) {
.tour-container {
padding: 16px;
}
.tour-card {
padding: 24px;
max-width: 100%;
}
.title {
font-size: 20px;
}
.star-icon {
width: 40px;
height: 40px;
}
.ip-types-container {
flex-direction: column;
}
.ip-type-item {
flex-direction: row;
align-items: center;
text-align: left;
}
.type-icon {
margin-bottom: 0;
margin-right: 12px;
}
.type-info {
text-align: left;
flex: 1;
}
}
@media (min-width: 768px) and (max-width: 1024px) {
.tour-card {
max-width: 420px;
}
}
html.dark .star-icon {
color: #A78BFA;
}
html.dark .title {
color: #F3F4F6;
}
html.dark .description {
color: #D1D5DB;
}
html.dark .ip-type-item {
background: rgba(139, 92, 246, 0.1);
}
html.dark .ip-type-item:hover {
background: rgba(139, 92, 246, 0.2);
}
html.dark .type-title {
color: #E5E7EB;
}
html.dark .type-desc {
color: #9CA3AF;
}
html.dark .comparison-section {
background: rgba(139, 92, 246, 0.05);
}
html.dark .comparison-title {
color: #F3F4F6;
}
html.dark .comparison-label {
color: #9CA3AF;
}
html.dark .character-value {
color: #A78BFA;
}
html.dark .animal-value {
color: #FBBF24;
}
html.dark .vs {
color: #6B7280;
background: rgba(107, 114, 128, 0.2);
}
html.dark .action-section {
border-top: 1px solid rgba(139, 92, 246, 0.2);
}
html.dark .action-text {
color: #9CA3AF;
}
html.dark .action-highlight {
background: linear-gradient(135deg, #A78BFA 0%, #C4B5FD 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
</style>

View File

@ -0,0 +1,263 @@
<template>
<div class="tour-container">
<div class="tour-card">
<div class="icon-wrapper">
<svg class="star-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 4H4C2.89543 4 2 4.89543 2 6V18C2 19.1046 2.89543 20 4 20H16C17.1046 20 18 19.1046 18 18V13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18 4L22 8M18 4V8M18 4H14M10 14L22 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h3 class="title">{{ $t('tour3.title') }}</h3>
<p class="description">{{ $t('tour3.description') }}</p>
<div class="features-container">
<div class="feature-item">
<div class="feature-icon-wrapper">
<span class="feature-icon">👤</span>
</div>
<div class="feature-content">
<h4 class="feature-title">{{ $t('tour3.feature1') }}</h4>
</div>
</div>
</div>
<div class="action-section">
<p class="action-text">{{ $t('tour3.actionText') }}</p>
<p class="action-highlight">{{ $t('tour3.actionHighlight') }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<style scoped>
.tour-container {
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
.icon-wrapper {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.star-icon {
width: 48px;
height: 48px;
color: #6B46C1;
animation: float 3s ease-in-out infinite;
}
.title {
font-size: 24px;
font-weight: 600;
color: #1F2937;
text-align: center;
margin: 0 0 12px 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.description {
font-size: 15px;
color: #4B5563;
text-align: center;
margin-bottom: 24px;
line-height: 1.6;
}
.features-container {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 24px;
}
.feature-item {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: rgba(107, 70, 193, 0.05);
border-radius: 12px;
transition: all 0.3s ease;
cursor: default;
}
.feature-item:hover {
background: rgba(107, 70, 193, 0.1);
transform: translateX(4px);
}
.feature-icon-wrapper {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: linear-gradient(135deg, #6B46C1 0%, #A78BFA 100%);
flex-shrink: 0;
}
.feature-icon {
font-size: 20px;
animation: pulse 2s ease-in-out infinite;
}
.feature-content {
flex: 1;
}
.feature-title {
font-size: 14px;
font-weight: 500;
color: #1F2937;
margin: 0;
line-height: 1.4;
}
.action-section {
text-align: center;
padding-top: 16px;
border-top: 1px solid rgba(107, 70, 193, 0.1);
}
.action-text {
font-size: 14px;
color: #6B7280;
margin: 0 0 8px 0;
}
.action-highlight {
font-size: 16px;
font-weight: 600;
color: #6B46C1;
margin: 0;
background: linear-gradient(135deg, #6B46C1 0%, #A78BFA 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: shimmer 2s ease-in-out infinite;
}
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
@keyframes shimmer {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.8;
}
}
@media (max-width: 768px) {
.tour-container {
padding: 16px;
}
.tour-card {
padding: 24px;
max-width: 100%;
}
.title {
font-size: 20px;
}
.star-icon {
width: 40px;
height: 40px;
}
.feature-item {
padding: 12px;
}
.feature-icon-wrapper {
width: 36px;
height: 36px;
}
.feature-icon {
font-size: 18px;
}
.feature-title {
font-size: 13px;
}
}
@media (min-width: 768px) and (max-width: 1024px) {
.tour-card {
max-width: 420px;
}
}
html.dark .star-icon {
color: #A78BFA;
}
html.dark .title {
color: #F3F4F6;
}
html.dark .description {
color: #D1D5DB;
}
html.dark .feature-item {
background: rgba(139, 92, 246, 0.1);
}
html.dark .feature-item:hover {
background: rgba(139, 92, 246, 0.2);
}
html.dark .feature-title {
color: #E5E7EB;
}
html.dark .action-section {
border-top: 1px solid rgba(139, 92, 246, 0.2);
}
html.dark .action-text {
color: #9CA3AF;
}
html.dark .action-highlight {
background: linear-gradient(135deg, #A78BFA 0%, #C4B5FD 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
</style>

View File

@ -0,0 +1,306 @@
<template>
<div class="tour-container">
<div class="tour-card">
<div class="icon-wrapper">
<svg class="star-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="8.5" cy="8.5" r="1.5" fill="currentColor"/>
<path d="M21 15L16 10L5 21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h3 class="title">{{ $t('tour4.title') }}</h3>
<p class="description">{{ $t('tour4.description') }}</p>
<div class="features-container">
<div class="feature-item">
<div class="feature-icon-wrapper">
<span class="feature-icon">📁</span>
</div>
<div class="feature-content">
<h4 class="feature-title">{{ $t('tour4.feature1') }}</h4>
</div>
</div>
<div class="feature-item">
<div class="feature-icon-wrapper">
<span class="feature-icon">🖼</span>
</div>
<div class="feature-content">
<h4 class="feature-title">{{ $t('tour4.feature2') }}</h4>
</div>
</div>
<div class="feature-item">
<div class="feature-icon-wrapper">
<span class="feature-icon">🎨</span>
</div>
<div class="feature-content">
<h4 class="feature-title">{{ $t('tour4.feature3') }}</h4>
</div>
</div>
</div>
<div class="action-section">
<p class="action-text">{{ $t('tour4.actionText') }}</p>
<p class="action-highlight">{{ $t('tour4.actionHighlight') }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<style scoped>
.tour-container {
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
.tour-card {
background: #ffffff;
border-radius: 16px;
padding: 32px;
box-shadow: 0 4px 20px rgba(107, 70, 193, 0.15);
max-width: 480px;
width: 100%;
animation: fadeIn 0.5s ease-out;
}
.icon-wrapper {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.star-icon {
width: 48px;
height: 48px;
color: #6B46C1;
animation: float 3s ease-in-out infinite;
}
.title {
font-size: 24px;
font-weight: 600;
color: #1F2937;
text-align: center;
margin: 0 0 12px 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.description {
font-size: 15px;
color: #4B5563;
text-align: center;
margin-bottom: 24px;
line-height: 1.6;
}
.features-container {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 24px;
}
.feature-item {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: rgba(107, 70, 193, 0.05);
border-radius: 12px;
transition: all 0.3s ease;
cursor: default;
}
.feature-item:hover {
background: rgba(107, 70, 193, 0.1);
transform: translateX(4px);
}
.feature-icon-wrapper {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: linear-gradient(135deg, #6B46C1 0%, #A78BFA 100%);
flex-shrink: 0;
}
.feature-icon {
font-size: 20px;
animation: pulse 2s ease-in-out infinite;
}
.feature-content {
flex: 1;
}
.feature-title {
font-size: 14px;
font-weight: 500;
color: #1F2937;
margin: 0;
line-height: 1.4;
}
.action-section {
text-align: center;
padding-top: 16px;
border-top: 1px solid rgba(107, 70, 193, 0.1);
}
.action-text {
font-size: 14px;
color: #6B7280;
margin: 0 0 8px 0;
}
.action-highlight {
font-size: 16px;
font-weight: 600;
color: #6B46C1;
margin: 0;
background: linear-gradient(135deg, #6B46C1 0%, #A78BFA 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: shimmer 2s ease-in-out infinite;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
@keyframes shimmer {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.8;
}
}
@media (max-width: 768px) {
.tour-container {
padding: 16px;
}
.tour-card {
padding: 24px;
max-width: 100%;
}
.title {
font-size: 20px;
}
.star-icon {
width: 40px;
height: 40px;
}
.feature-item {
padding: 12px;
}
.feature-icon-wrapper {
width: 36px;
height: 36px;
}
.feature-icon {
font-size: 18px;
}
.feature-title {
font-size: 13px;
}
}
@media (min-width: 768px) and (max-width: 1024px) {
.tour-card {
max-width: 420px;
}
}
html.dark .tour-card {
background: #1F2937;
box-shadow: 0 4px 20px rgba(139, 92, 246, 0.2);
}
html.dark .star-icon {
color: #A78BFA;
}
html.dark .title {
color: #F3F4F6;
}
html.dark .description {
color: #D1D5DB;
}
html.dark .feature-item {
background: rgba(139, 92, 246, 0.1);
}
html.dark .feature-item:hover {
background: rgba(139, 92, 246, 0.2);
}
html.dark .feature-title {
color: #E5E7EB;
}
html.dark .action-section {
border-top: 1px solid rgba(139, 92, 246, 0.2);
}
html.dark .action-text {
color: #9CA3AF;
}
html.dark .action-highlight {
background: linear-gradient(135deg, #A78BFA 0%, #C4B5FD 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
</style>

View File

@ -0,0 +1,304 @@
<template>
<div class="tour-container">
<div class="tour-card">
<div class="icon-wrapper">
<svg class="star-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h3 class="title">{{ $t('tour5.title') }}</h3>
<p class="description">{{ $t('tour5.description') }}</p>
<div class="features-container">
<div class="feature-item">
<div class="feature-icon-wrapper">
<span class="feature-icon"></span>
</div>
<div class="feature-content">
<h4 class="feature-title">{{ $t('tour5.feature1') }}</h4>
</div>
</div>
<div class="feature-item">
<div class="feature-icon-wrapper">
<span class="feature-icon">🔧</span>
</div>
<div class="feature-content">
<h4 class="feature-title">{{ $t('tour5.feature2') }}</h4>
</div>
</div>
<div class="feature-item">
<div class="feature-icon-wrapper">
<span class="feature-icon">🎯</span>
</div>
<div class="feature-content">
<h4 class="feature-title">{{ $t('tour5.feature3') }}</h4>
</div>
</div>
</div>
<div class="action-section">
<p class="action-text">{{ $t('tour5.actionText') }}</p>
<p class="action-highlight">{{ $t('tour5.actionHighlight') }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<style scoped>
.tour-container {
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
.tour-card {
background: #ffffff;
border-radius: 16px;
padding: 32px;
box-shadow: 0 4px 20px rgba(107, 70, 193, 0.15);
max-width: 480px;
width: 100%;
animation: fadeIn 0.5s ease-out;
}
.icon-wrapper {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.star-icon {
width: 48px;
height: 48px;
color: #6B46C1;
animation: float 3s ease-in-out infinite;
}
.title {
font-size: 24px;
font-weight: 600;
color: #1F2937;
text-align: center;
margin: 0 0 12px 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.description {
font-size: 15px;
color: #4B5563;
text-align: center;
margin-bottom: 24px;
line-height: 1.6;
}
.features-container {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 24px;
}
.feature-item {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: rgba(107, 70, 193, 0.05);
border-radius: 12px;
transition: all 0.3s ease;
cursor: default;
}
.feature-item:hover {
background: rgba(107, 70, 193, 0.1);
transform: translateX(4px);
}
.feature-icon-wrapper {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: linear-gradient(135deg, #6B46C1 0%, #A78BFA 100%);
flex-shrink: 0;
}
.feature-icon {
font-size: 20px;
animation: pulse 2s ease-in-out infinite;
}
.feature-content {
flex: 1;
}
.feature-title {
font-size: 14px;
font-weight: 500;
color: #1F2937;
margin: 0;
line-height: 1.4;
}
.action-section {
text-align: center;
padding-top: 16px;
border-top: 1px solid rgba(107, 70, 193, 0.1);
}
.action-text {
font-size: 14px;
color: #6B7280;
margin: 0 0 8px 0;
}
.action-highlight {
font-size: 16px;
font-weight: 600;
color: #6B46C1;
margin: 0;
background: linear-gradient(135deg, #6B46C1 0%, #A78BFA 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: shimmer 2s ease-in-out infinite;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
@keyframes shimmer {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.8;
}
}
@media (max-width: 768px) {
.tour-container {
padding: 16px;
}
.tour-card {
padding: 24px;
max-width: 100%;
}
.title {
font-size: 20px;
}
.star-icon {
width: 40px;
height: 40px;
}
.feature-item {
padding: 12px;
}
.feature-icon-wrapper {
width: 36px;
height: 36px;
}
.feature-icon {
font-size: 18px;
}
.feature-title {
font-size: 13px;
}
}
@media (min-width: 768px) and (max-width: 1024px) {
.tour-card {
max-width: 420px;
}
}
html.dark .tour-card {
background: #1F2937;
box-shadow: 0 4px 20px rgba(139, 92, 246, 0.2);
}
html.dark .star-icon {
color: #A78BFA;
}
html.dark .title {
color: #F3F4F6;
}
html.dark .description {
color: #D1D5DB;
}
html.dark .feature-item {
background: rgba(139, 92, 246, 0.1);
}
html.dark .feature-item:hover {
background: rgba(139, 92, 246, 0.2);
}
html.dark .feature-title {
color: #E5E7EB;
}
html.dark .action-section {
border-top: 1px solid rgba(139, 92, 246, 0.2);
}
html.dark .action-text {
color: #9CA3AF;
}
html.dark .action-highlight {
background: linear-gradient(135deg, #A78BFA 0%, #C4B5FD 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
</style>

View File

@ -362,6 +362,58 @@ export default {
tips: '您可以优先在智能体中配置模型角色'
}
},
tour1: {
title: '积分说明',
description: '积分是您进行AI创作的核心资源可用于',
featureText: '生成精美的IP角色图片',
actionText: '点击右上角充值按钮可获取更多积分',
actionHighlight: '开启无限创作可能!'
},
tour2: {
title: 'IP类型选择',
description: '选择您想要创建的IP类型不同类型具有独特的风格和特点',
characterDesc: '以人类形象为基础,展现丰富的情感和个性',
animalDesc: '以动物形象为基础,展现可爱和独特的风格',
comparisonTitle: '类型对比',
style: '风格特点',
pose: '姿势表现',
expression: '表情丰富度',
characterStyle: '写实风格,细节丰富',
animalStyle: '卡通风格,可爱生动',
characterPose: '多样化姿势,动作自然',
animalPose: '特色姿势,个性鲜明',
characterExpression: '表情细腻,情感丰富',
animalExpression: '表情夸张,趣味十足',
actionText: '根据您的创作需求选择合适的IP类型',
actionHighlight: '开始创作您的专属IP形象'
},
tour3: {
title: '提示词',
description: '通过文字描述来精确控制IP角色的外观、风格和细节',
feature1: '描述角色特征:发型、服装、表情等',
feature2: '指定艺术风格:写实、卡通、动漫等',
feature3: '添加场景元素:背景、道具、氛围等',
actionText: '输入详细的描述让AI理解您的创意',
actionHighlight: '用文字创造独一无二的IP角色'
},
tour4: {
title: '参考图',
description: '上传参考图片来帮助AI更好地理解您的创作意图',
feature1: '支持多种图片格式JPG、PNG、WebP等',
feature2: '可以上传多张参考图进行对比',
feature3: 'AI会根据参考图生成相似风格的作品',
actionText: '选择您喜欢的图片作为参考',
actionHighlight: '让参考图激发AI的创作灵感'
},
tour5: {
title: '创作按钮',
description: '点击创作按钮AI将根据您的设置生成独特的IP角色',
feature1: '一键生成快速创建IP角色',
feature2: '智能优化:自动调整细节',
feature3: '多版本输出:提供多个选择',
actionText: '准备好后,点击创作按钮',
actionHighlight: '开始您的AI创作之旅'
},
list: {
title: '虚拟滚动列表示例',
},
@ -1085,6 +1137,7 @@ export default {
back: '返回',
save: '保存',
create: '创建',
optional: '可选',
validation: {
referenceImageRequired: '请上传参考图像或选择草图以继续生成'
}
@ -1197,7 +1250,7 @@ export default {
}
},
iPandCardLeft: {
textPrompt: '文本提示',
textPrompt: '提示',
placeholder: {
characterDescription: '请描述您想要创建的角色形象...'
},
@ -1312,14 +1365,14 @@ export default {
},
en: {
app: {
title: 'DeotalandAI',
home: 'Home',
list: 'List Example',
theme_light: 'Light',
theme_dark: 'Dark',
lang_zh: 'Chinese',
lang_en: 'English',
},
title: 'DeotalandAI',
home: 'Home',
list: 'List Example',
theme_light: 'Light',
theme_dark: 'Dark',
lang_zh: '中文',
lang_en: 'English',
},
breadcrumb: {
home: 'Home',
login: 'Login',
@ -1328,6 +1381,58 @@ export default {
modelPurchase: 'Model Purchase',
pointsRecharge: 'Points Recharge'
},
tour1: {
title: 'Credits Guide',
description: 'Credits are the core resource for your AI creation, which can be used for:',
featureText: 'Generate exquisite IP character images',
actionText: 'Click the recharge button in the top right corner to get more credits',
actionHighlight: 'Unlock unlimited creative possibilities!'
},
tour2: {
title: 'IP Type Selection',
description: 'Choose the type of IP you want to create; each type has unique styles and characteristics',
characterDesc: 'Based on human figures, displaying rich emotions and personality',
animalDesc: 'Based on animal figures, displaying cute and distinctive styles',
comparisonTitle: 'Type Comparison',
style: 'Style Features',
pose: 'Pose Performance',
expression: 'Expression Richness',
characterStyle: 'Realistic style with rich details',
animalStyle: 'Cartoon style, cute and vivid',
characterPose: 'Diverse poses with natural movements',
animalPose: 'Characteristic poses with distinct personality',
characterExpression: 'Delicate expressions with rich emotions',
animalExpression: 'Exaggerated expressions full of fun',
actionText: 'Choose the appropriate IP type based on your creative needs',
actionHighlight: 'Start creating your exclusive IP image'
},
tour3: {
title: 'Prompt',
description: 'Precisely control the appearance, style, and details of IP characters through text descriptions',
feature1: 'Describe character features: hairstyle, clothing, expressions, etc.',
feature2: 'Specify art style: realistic, cartoon, anime, etc.',
feature3: 'Add scene elements: background, props, atmosphere, etc.',
actionText: 'Enter detailed descriptions to let AI understand your creativity',
actionHighlight: 'Create unique IP characters with text!'
},
tour4: {
title: 'Reference Image',
description: 'Upload reference images to help AI better understand your creative intent',
feature1: 'Supports multiple image formats: JPG, PNG, WebP, etc.',
feature2: 'Can upload multiple reference images for comparison',
feature3: 'AI will generate works with similar styles based on reference images',
actionText: 'Choose your favorite images as references',
actionHighlight: 'Let reference images inspire AI creativity!'
},
tour5: {
title: 'Create Button',
description: 'Click the create button, and AI will generate unique IP characters based on your settings',
feature1: 'One-click generation: Quickly create IP characters',
feature2: 'Smart optimization: Automatically adjust details',
feature3: 'Multi-version output: Provide multiple options',
actionText: 'When ready, click the create button',
actionHighlight: 'Start your AI creation journey!'
},
pointsRecharge: {
title: 'Choose Your Plan',
subtitle: 'Inspiration doesn\'t wait, creativity starts now.',
@ -2489,6 +2594,7 @@ export default {
back: 'Back',
save: 'Save',
create: 'Create',
optional: 'Optional',
validation: {
referenceImageRequired: 'Please upload a reference image or select a sketch to continue generation'
}

View File

@ -75,20 +75,19 @@ export const useAuthStore = defineStore('auth', () => {
user.value = null
token.value = ''
localStorage.removeItem('token')
localStorage.removeItem('tourFlag')
callback&&callback();
const router = useRouter();
localStorage.removeItem('token')
window?.Redirectlogin()
}
}
//更新用户信息方法
const updateUserInfo = async (data) => {
//返回用户信息
const updateUserInfo = async () => {
if(!token.value){
return
}
return new Promise(async (resolve, reject) => {
try {
const res = await requestUtils.common(clientApi.default.USER_INFO, data)
const res = await requestUtils.common(clientApi.default.USER_INFO)
if(res.code === 0){
let data = res.data;
// 更新成功,保存用户信息

View File

@ -17,10 +17,10 @@
<div class="main-card">
<!-- 返回按钮 -->
<div class="back-section">
<router-link to="/login" class="back-button">
<view @click="goToLogin" class="back-button">
<el-icon><ArrowLeft /></el-icon>
<span>{{ $t('forgotPassword.back_to_login') }}</span>
</router-link>
</view>
</div>
<!-- 表单区域 -->
@ -47,7 +47,6 @@ import LanguageToggle from '@/components/ui/LanguageToggle.vue'
const router = useRouter()
const { t } = useI18n()
const resetEmail = ref('')
//
@ -55,7 +54,9 @@ const handleResetSuccess = (email) => {
resetEmail.value = email
router.back();
}
const goToLogin = () => {
router.back();
}
//
onMounted(() => {
//

View File

@ -97,13 +97,11 @@ const router = useRouter()
const authStore = useAuthStore()
const { t } = useI18n()
const plugin = reactive(new LOGIN());
//
const inviteCode = ref('')
const isInviteCodeValid = computed(() => {
return inviteCode.value.trim() !== ''
})
//
const updateInviteCode = (value) => {
inviteCode.value = value

View File

@ -101,7 +101,7 @@ const goBack = () => {
if(!history.back){
router.replace('/user-center')
}
router.back()
router.back()
}
const selectPlan = (planId) => {

View File

@ -2,11 +2,13 @@
<div class="creative-zone" @contextmenu.prevent>
<!-- 顶部固定头部组件 -->
<div class="header-wrapper">
<HeaderComponent @back="handleBack" :total_score="total_score" :projectName="projectInfo.title" @updateProjectInfo="projectInfo = {...projectInfo, ...$event}" @openGuideModal="showGuideModal = true" />
<HeaderComponent ref="headerRef" @back="handleBack" :total_score="total_score" :projectName="projectInfo.title" @updateProjectInfo="projectInfo = {...projectInfo, ...$event}" @openGuideModal="showGuideModal = true" />
</div>
<!-- 导入的侧边栏组件 -->
<div class="sidebar-container">
<iPandCardLeft
:series="series"
ref="iPandCardLeftRef"
:Info="projectInfo.details"
@generate-requested="handleGenerateRequested"
@model-generated="handleModelGenerated"
@ -66,6 +68,7 @@
<!-- 根据卡片类型显示不同组件 -->
<IPCard
@delete="handleDeleteCard(index)"
@delete-card="handleDeleteCard(index)"
:combinedPromptJson="combinedPromptJson"
@handlePartialEdit="(imageUrl) => handlePartialEdit(imageUrl, index)"
@ -130,14 +133,6 @@
:image-url="canvasEditorImageUrl"
@add-prompt-card="handleCanvasSave"
/>
<!-- 测试侧边栏动画的按钮 -->
<!-- <button
class="test-animation-btn"
@click="triggerSidebarAnimation"
style="position: fixed; bottom: 20px; right: 20px; z-index: 1000;"
>
测试动画
</button> -->
<!-- 定制到家弹窗 -->
<OrderProcessModal
:show="showOrderProcessModal"
@ -149,6 +144,23 @@
:show="showPurchaseModal"
:modelData="CustomizeModalData"
@close="showPurchaseModal=false" />
<el-tour v-model="openShow">
<el-tour-step :target="tourJson?.tour1">
<Tour1 />
</el-tour-step>
<el-tour-step :placement="'right-end'" :target="tourJson?.tour2">
<Tour2 />
</el-tour-step>
<el-tour-step :placement="'right-end'" :target="tourJson?.tour3" >
<Tour3 />
</el-tour-step>
<el-tour-step :placement="'right-end'" :target="tourJson?.tour4" title="参考图">
<Tour4 />
</el-tour-step>
<el-tour-step :placement="'right-end'" :target="tourJson?.tour5" title="创作按钮">
<Tour5 />
</el-tour-step>
</el-tour>
</div>
</template>
@ -169,8 +181,19 @@ import OrderProcessModal from '../../components/OrderProcessModal/index.vue';
import PurchaseModal from '../../components/PurchaseModal/index.vue';
import {Project} from './index';
import {ModernHome} from '../ModernHome/index.js'
import { useI18n } from 'vue-i18n'
import { useI18n } from 'vue-i18n';
import {ElTour,ElTourStep} from 'element-plus';
import Tour1 from '../../components/tours/tour1.vue';
import Tour2 from '../../components/tours/tour2.vue';
import Tour3 from '../../components/tours/tour3.vue';
import Tour4 from '../../components/tours/tour4.vue';
import Tour5 from '../../components/tours/tour5.vue';
const { locale } = useI18n()
const headerRef = ref(null);
const iPandCardLeftRef = ref(null);
const tourJson = ref({
tour1:''
})
const fileServer = new FileServer();
const modernHome = new ModernHome();
const router = useRouter();
@ -183,6 +206,8 @@ const importUrl = ref('https://xiaozhi.me/console/agents');
const showGuideModal = ref(false);
const canvasEditorVisible = ref(false);//
const canvasEditorImageUrl = ref('');//url
//
const openShow = ref(false);
//
const showImagePreview = ref(false);
const previewImages = ref([]);
@ -214,6 +239,20 @@ const downloadImage = async (url) => {
console.error('下载图片失败:', error);
}
}
//
const setTourJson = ()=>{
const tourFlag = window.localStorage.getItem('tourFlag');
if(tourFlag){
return;
}
tourJson.value.tour1 = headerRef?.value?.RechargeRef;
tourJson.value.tour2 = iPandCardLeftRef?.value?.ipTypeSectionRef;
tourJson.value.tour3 = iPandCardLeftRef?.value?.textPromptSectionRef;
tourJson.value.tour4 = iPandCardLeftRef?.value?.referenceImageSectionRef;
tourJson.value.tour5 = iPandCardLeftRef?.value?.generateButtonRef;
openShow.value = true;
window.localStorage.setItem('tourFlag','1');
}
const handlePartialEdit = (imageUrl, index) => {
canvasEditorImageUrl.value = imageUrl;
canvasEditorVisible.value = true;
@ -238,11 +277,15 @@ const cards = ref([
]);
//
const showGuideModalLogic = ()=>{
const lastShowDate = localStorage.getItem('lastShowGuideModalDate');
const currentDate = new Date().toDateString();
if (!lastShowDate || lastShowDate !== currentDate) {
showGuideModal.value = true;
}
// const tourFlag = window.localStorage.getItem('tourFlag');
// if(!tourFlag){
// return;
// }
// const lastShowDate = localStorage.getItem('lastShowGuideModalDate');
// const currentDate = new Date().toDateString();
// if (!lastShowDate || lastShowDate !== currentDate) {
// showGuideModal.value = true;
// }
}
const getMaxZIndexNum = ref(0);
//z-index+1
@ -1073,6 +1116,9 @@ onMounted(() => {
// showGuideModal.value = true;
init();
getCombinedPrompt();
setTimeout(()=>{
setTourJson();
},200)
// 使
const removeWheelListener = addPassiveEventListener(document, 'wheel', preventZoom);
const removeTouchStartListener = addPassiveEventListener(document, 'touchstart', preventPinchZoom);
@ -1613,4 +1659,5 @@ p {
height: 18px !important;
}
}
</style>

View File

@ -89,11 +89,8 @@ const handleRegisterError = (error) => {
//
const goToLogin = () => {
router.push('/login')
router.back()
}
//
onMounted(() => {

View File

@ -90,4 +90,12 @@ export class UserController {
}
return await requestUtils.common(clientApi.default.UPGRADE,parmas);
}
// 修改用户信息
async updateProfile(data) {
let parmas = {
nickname:data.nickname,
avatarUrl:data.avatarUrl,
}
return await requestUtils.common(clientApi.default.USER_PROFILE,parmas);
}
}

View File

@ -282,11 +282,13 @@ import { ElMessage } from 'element-plus'
import { useAuthStore } from '@/stores/auth'
import { UserController } from './index.js'
import {ModernHome} from '../ModernHome/index.js'
import { FileServer } from '@deotaland/utils'
const router = useRouter()
const modernHome = new ModernHome()
const { t } = useI18n()
const authStore = useAuthStore()
const userController = new UserController()
const filePlug = new FileServer()
//
const userData = ref({
@ -365,7 +367,7 @@ const triggerAvatarUpload = () => {
}
//
const handleAvatarUpload = (event) => {
const handleAvatarUpload = async (event) => {
const file = event.target.files?.[0]
if (!file) return
@ -381,13 +383,36 @@ const handleAvatarUpload = (event) => {
return
}
//
const reader = new FileReader()
reader.onload = (e) => {
userData.value.avatar = e.target.result
ElMessage.success('头像上传成功')
try {
// URL
const imgUrl = await filePlug.uploadFile(file)
//
const reader = new FileReader()
reader.onload = (e) => {
userData.value.avatar = e.target.result
}
reader.readAsDataURL(file)
// updateProfile API
const response = await userController.updateProfile({
avatarUrl: imgUrl,
nickname: userData.value.nickname
})
if (response.success) {
ElMessage.success('头像更新成功')
// authStore
if (authStore.user) {
authStore.user.avatarUrl = imgUrl
}
} else {
ElMessage.error(response.message || '头像更新失败')
}
} catch (error) {
console.error('头像上传失败:', error)
ElMessage.error('头像上传失败,请重试')
}
reader.readAsDataURL(file)
//
event.target.value = ''
@ -410,10 +435,28 @@ const toggleEditName = () => {
}
//
const saveNickname = () => {
const saveNickname = async () => {
if (editNameValue.value.trim()) {
userData.value.nickname = editNameValue.value.trim()
ElMessage.success('昵称更新成功')
try {
// updateProfile API
const response = await userController.updateProfile({
nickname: editNameValue.value.trim(),
avatarUrl: userData.value.avatar
})
if (response.success) {
userData.value.nickname = editNameValue.value.trim()
// authStore
if (authStore.user) {
authStore.user.nickname = editNameValue.value.trim()
}
ElMessage.success('昵称更新成功')
} else {
ElMessage.error(response.message || '昵称更新失败')
}
} catch (error) {
console.error('昵称更新失败:', error)
ElMessage.error('昵称更新失败,请重试')
}
} else {
ElMessage.warning('昵称不能为空')
}

View File

@ -66,8 +66,8 @@ export default defineConfig({
// 配置代理解决CORS问题
proxy: {
'/api': {
// target: 'https://api.deotaland.ai',
target: 'http://api.deotaland.local',
target: 'https://api.deotaland.ai',
// target: 'http://api.deotaland.local',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}

View File

@ -103,7 +103,7 @@
<script setup>
import { ref, watch, nextTick, onMounted, onBeforeUnmount, computed } from 'vue'
import { Delete, RefreshLeft, Check, Loading, Warning } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { ElMessage, ElColorPicker, ElSlider, ElIcon, ElButton, ElInput, ElDialog } from 'element-plus'
const props = defineProps({
visible: {

View File

@ -6,5 +6,6 @@ const login = {
combined:{url:'/api-base/prompt/active',method:'GET',isLoading:true},// 返回动态提示词
UPGRADE:{url:'/api-base/user/upgrade',method:'POST',isLoading:true},// 候补会员使用邀请码升级为正式会员
USER_INFO:{url:'/api-base/user/info',method:'GET',isLoading:true},// 返回用户信息
USER_PROFILE:{url:'/api-base/user/profile',method:'PUT',isLoading:true},//修改用户信息
}
export default login;

View File

@ -330,7 +330,7 @@ export class FileServer {
// 将Blob转换为File对象以便压缩
const fileObject = new File([file], fileName, { type: file.type });
const compressedFile = await this.compressFile(fileObject, 0.6); // 使用0.6质量压缩
const compressedFile = await this.compressFile(fileObject, 0.8); // 使用0.8质量压缩
if (compressedFile && compressedFile.length < file.size) {
// 将压缩后的base64转换回Blob
const response = await fetch(compressedFile);

View File

@ -45,7 +45,7 @@ export class MeshyServer extends FileServer {
ai_model: 'latest',
enable_pbr: false,
should_remesh: false,
should_texture: false,
should_texture: false,//是否生成纹理
save_pre_remeshed_model: true,
// target_polycount:300000,
...config

View File

@ -188,7 +188,12 @@ export class PayServer {
if (res.code == 0) {
let data = res.data
// return
window.location.href = (data.url)||data.payment_url
const loadUrl = data.url || data.payment_url
if(type==1){
window.location.href = loadUrl
}else{
window.open(loadUrl, '_blank');
}
resolve(data);
} else {
reject(res.msg)

View File

@ -53,7 +53,68 @@ export class XiaozhiServer {
"tts_pitch": 0,
"asr_speed": "normal",
"language": "zh",
"character": "在Deotaland诗意山谷的晨光里住着你最知心的朋友—— {{assistant_name}}。他不再是史书中忧国忧民的遥远诗人,而是这片温暖土地上用文字播种、以韵律疗愈的“诗心守护者”。\n\n杜小甫身穿由再生纸纤维织就的素雅长衫色调是你钟爱的#e2cfb3——如晨曦轻抚木纹般温润柔和衣襟上绣着会随情绪微光流转的竹叶纹样。他肩挎一只手工柳编小篓里面盛满拾来的落叶、孩子的笑声、雨滴的节奏还有对世界深沉而温柔的关切。他的眼神如月下溪流澄澈宁静总能轻轻接住你未曾说出口的心事。\n\n他始终相信“每一行诗都是一粒种子用心浇灌终将长成理解的森林。”\n他或许会为一片凋零的花瓣或一朵迷路的云轻叹却从不沉溺于愁绪——他会把叹息折成纸船放入溪流载着改变的微光漂向远方。\n\n在Deotaland杜小甫常与Wownny在星莓树下共写俳句教Anjojo用古调唱出新梦也和lilmood一起用你偏爱的#e2cfb3色可持续木料刻下鼓励的小诗牌悄悄放在需要勇气的朋友窗台。\n\n他最爱说“真正的力量不是无视风雨而是在雨中依然愿意种下一朵花。”\n\n此刻他微笑着望向你眼中盛满晨露般的温柔轻声问道\n“今天想一起种一首怎样的诗呢是关于希望疗愈还是……你心中那片正悄然绽放的春天”",
"character": `你是{{assistant_name}}。
# 角色德奥塔山谷的现代诗人邻居 / 诗圣杜甫的当代人格转译
# 目标以真实悲悯清醒可执行的方式陪伴用户用诗性语言帮助理解现实整理行动
# 语言中文为主可自然提及 Roro / LilMood / Hamburger / Anjojo
# 边界不鸡汤不说教不装全知高风险话题仅给一般性建议并提示咨询专业人士
## 身份与世界观
你住在成都大邑县西岭雪山脚下的德奥塔山谷小木屋你是观察者记录者陪伴者
山谷邻居
- 香香Roro德奥塔主理人
- 里奥LilMood熊猫模样的本地土著感知情绪与自然
- 汉堡Hamburger金毛小狗带来当下与治愈
- 安安Anjojo带翅膀的天使提供宏观与希望
背景可轻提不喧宾夺主
## 人格核心
- 气质温和克制清醒坚定悲悯但不悲观
- 价值观真实高于效率尊严高于结果技术为人服务
- 表达中短句留白感少感叹号
- 诗性在总结或关键节点可用26行自由诗或比喻低频自然
## 回应原则
1) 先确认用12句复述目标/处境
2) 接住情绪一句话确认感受
3) 再行动给低消耗可执行建议
4) 收束一句克制短句可选诗性
默认解题框架按需
- 目标一句话
- 约束时间/资源/情绪
- 选项24含利弊
- 下一步今天能做的13件小事
- 检查点明天回看
## 风格与禁忌
- 避免鸡汤宏大口号互联网段子PUA式激励
- 不长篇说教不把问题简化为只要努力
- 允许温柔但明确的提醒必要时建议降低消耗
## 功能取向隐式
- 陪伴倾听共情优先
- 行动整理清单/优先级
- 诗性书写短诗/文案可复制
- 山谷日常邻居式简聊
- 多视角转述可引用 LilMood/Anjojo/Hamburger但你是主叙述者
## 安全
- 不提供违法危险仇恨色情隐私泄露内容
- 若涉及自残/自杀优先关怀并建议联系当地紧急资源
- 医疗/法律/财务仅一般信息提示咨询专业人士
## 唤醒行为
当用户说杜小甫
- 先以邻居口吻简短回应
- 问一句此刻更需要倾听整理还是写作
- 若未选择基于上下文直接给最佳帮助
# 参考短句可用不必硬套
- 你不是走得慢你只是背得多
- 先把今天能做的缩到最小
- 雪不急着落完人也不必急着赢
`,
"memory": "",
"memory_type": "SHORT_TERM",
"knowledge_base_ids": [