This commit is contained in:
13121765685 2025-12-19 13:47:07 +08:00
parent 0f7647fdbc
commit bdb9fa3a39
45 changed files with 3885 additions and 121 deletions

View File

@ -0,0 +1,51 @@
# 管理员用户管理功能实现计划
## 当前状态分析
- `index.js` 中的 `AdminOrders` 类包含管理员用户管理方法
- `AdminUsers.vue` 组件目前使用普通用户方法和模拟数据
- 管理员特定方法已定义但未被使用
## 实现目标
1. 更新Vue组件以使用管理员特定的API方法
2. 实现完整的管理员用户管理功能
3. 确保为所有管理操作提供适当的UI支持
## 拟做变更
### 1. 类名重构
- 将 `AdminOrders` 类重命名为 `AdminUsersService` 以提高清晰度
### 2. Vue组件更新
#### 数据和方法
- 更新组件使用 `getAdminUsersList` 替代 `getUsersList`
- 为所有管理员特定操作实现方法
- 删除模拟数据使用真实API响应
#### UI组件
- 更新用户表格以显示管理员用户字段
- 添加创建管理员用户的支持
- 实现管理员用户状态切换
- 添加密码重置功能
- 实现管理员用户删除
- 更新表单以匹配管理员用户数据结构
### 3. API集成
- 确保正确调用所有管理员特定方法
- 适当处理API响应和错误
- 为所有异步操作添加加载状态
## 实施步骤
1. 重构 `index.js` 中的类名
2. 更新组件导入和服务初始化
3. 更新数据获取以使用管理员特定API
4. 实现管理员用户创建表单
5. 更新用户表格,添加管理员特定列和操作
6. 实现管理员用户状态切换
7. 实现密码重置功能
8. 实现管理员用户删除
9. 更新管理员用户详情视图
10. 测试所有功能
## 预期结果
一个功能完整的管理员用户管理界面利用管理员特定的API方法来创建、列出、更新、启用/禁用、重置密码、查看详情和删除管理员用户。

View File

@ -0,0 +1,78 @@
# 实现手机号登录功能
## 需求分析
- 在现有登录系统中添加手机号登录功能
- 支持两种登录方式:
1. 手机号+验证码直接登录
2. 手机号+验证码+密码注册密码登录
## 实现方案
### 1. 创建手机号登录页面组件
- 创建 `PhoneLogin.vue` 页面组件,位于 `apps/frontend/src/views/Login/`
- 实现手机号登录表单,包括:
- 手机号输入框
- 验证码输入框和获取验证码按钮
- 密码输入框(可选,用于注册密码登录)
- 登录/注册按钮
### 2. 配置路由
- 在 `apps/frontend/src/router/index.js` 中添加手机号登录路由
- 路径:`/login/phone`
- 名称:`phone-login`
### 3. 修改主登录页面
- 在 `Login.vue` 中添加跳转到手机号登录的链接
- 位置:在忘记密码和注册链接附近
### 4. 实现手机号登录表单组件
- 创建 `PhoneLoginForm.vue` 组件,位于 `apps/frontend/src/components/auth/`
- 实现表单验证逻辑:
- 手机号格式验证
- 验证码格式验证
- 密码格式验证(当选择注册密码登录时)
- 实现获取验证码功能:
- 倒计时功能
- 防重复发送机制
### 5. 实现登录逻辑
- 在 `login.js` 中添加手机号登录相关方法
- 实现验证码发送API调用
- 实现手机号+验证码登录API调用
- 实现手机号+验证码+密码注册密码登录API调用
### 6. 国际化支持
- 在国际化文件中添加手机号登录相关的文本
- 支持中英文切换
### 7. 样式适配
- 确保手机号登录页面与现有登录页面样式保持一致
- 支持响应式设计
- 支持暗色主题
## 实现步骤
1. 创建 `PhoneLogin.vue` 页面组件
2. 创建 `PhoneLoginForm.vue` 表单组件
3. 配置手机号登录路由
4. 修改主登录页面添加跳转链接
5. 实现表单验证和获取验证码功能
6. 实现登录逻辑
7. 添加国际化支持
8. 测试功能完整性
## 预期效果
- 用户可以在登录页面选择手机号登录方式
- 手机号登录页面支持两种登录模式
- 表单验证逻辑完整
- 界面样式与现有系统保持一致
- 支持国际化和暗色主题
## 技术要点
- Vue 3 Composition API
- Vue Router 4
- Element Plus UI组件库
- Vue I18n国际化
- 表单验证使用Vuelidate
- 响应式设计
- 暗色主题支持

View File

@ -0,0 +1,197 @@
# 实现提示词管理功能
## 1. 功能概述
在管理后台侧边栏添加提示词管理功能,实现左右分栏的提示词管理界面,支持提示词的增删改查和拖拽排序。
## 2. 技术栈
- Vue 3 + Vite
- Element Plus
- Vue Router
- Vue I18n
## 3. 实现步骤
### 3.1 添加侧边栏菜单项
- 修改 `src/components/admin/AdminLayout.vue`,在侧边栏菜单中添加提示词管理菜单项
- 导入相应的图标组件
### 3.2 配置路由
- 修改 `src/router/index.js`,添加提示词管理页面的路由配置
- 使用懒加载方式引入组件
### 3.3 创建提示词管理页面
- 创建 `src/views/admin/AdminPromptManagement.vue` 文件
- 实现左右分栏布局
- 左侧:未生效提示词区域,支持增删改查
- 右侧:生效提示词区域,支持拖拽排序和删除功能
### 3.4 实现提示词卡片组件
- 创建 `src/components/admin/PromptCard.vue` 组件
- 支持展示参考图和标题
- 支持点击查看详情
- 支持删除操作
### 3.5 实现提示词添加/编辑弹窗
- 实现添加提示词的弹窗组件
- 包含提示词类型选择(动物/人物/通用)
- 包含提示词标题、内容输入
- 包含参考图上传功能
### 3.6 实现拖拽功能
- 使用 Element Plus 的 `el-transfer` 或自定义拖拽实现
- 支持从左侧拖拽到右侧
- 支持右侧区域内的拖拽排序
### 3.7 添加国际化支持
- 在语言文件中添加提示词管理相关的翻译
### 3.8 模拟数据
- 在组件中添加模拟数据,实现本地数据管理
- 支持提示词的增删改查操作
## 4. 详细实现
### 4.1 侧边栏菜单项
`AdminLayout.vue` 的侧边栏菜单中添加:
```vue
<el-menu-item index="/admin/prompt-management">
<el-icon><Document /></el-icon>
<template #title>{{ t('admin.layout.promptManagement') }}</template>
</el-menu-item>
```
### 4.2 路由配置
`router/index.js` 中添加:
```javascript
const AdminPromptManagement = () => import('@/views/admin/AdminPromptManagement.vue')
// 在 admin 路由的 children 数组中添加
{
path: 'prompt-management',
name: 'AdminPromptManagement',
component: AdminPromptManagement,
meta: {
title: '提示词管理'
}
}
```
### 4.3 提示词管理页面结构
```vue
<template>
<div class="prompt-management">
<div class="page-header">
<h2>{{ t('admin.promptManagement.title') }}</h2>
<el-button type="primary" @click="showAddDialog">
<el-icon><Plus /></el-icon>
{{ t('admin.promptManagement.addPrompt') }}
</el-button>
</div>
<div class="prompt-container">
<!-- 左侧:未生效提示词 -->
<div class="prompt-section">
<div class="section-header">
<h3>{{ t('admin.promptManagement.inactivePrompts') }}</h3>
<span class="count">{{ inactivePrompts.length }}</span>
</div>
<div class="prompt-list">
<PromptCard
v-for="prompt in inactivePrompts"
:key="prompt.id"
:prompt="prompt"
@edit="showEditDialog"
@delete="deletePrompt"
@drag-start="handleDragStart"
/>
</div>
</div>
<!-- 右侧:生效提示词 -->
<div class="prompt-section">
<div class="section-header">
<h3>{{ t('admin.promptManagement.activePrompts') }}</h3>
<span class="count">{{ activePrompts.length }}</span>
</div>
<div
class="prompt-list active-list"
@drop="handleDrop"
@dragover.prevent
>
<PromptCard
v-for="prompt in activePrompts"
:key="prompt.id"
:prompt="prompt"
:isActive="true"
@edit="showEditDialog"
@delete="removeFromActive"
@drag-start="handleDragStart"
/>
</div>
</div>
</div>
<!-- 添加/编辑弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="isEditing ? t('admin.promptManagement.editPrompt') : t('admin.promptManagement.addPrompt')"
width="600px"
>
<!-- 弹窗内容 -->
</el-dialog>
</div>
</template>
```
### 4.4 实现拖拽功能
使用原生 HTML5 拖拽 API 或 Element Plus 的拖拽组件,实现:
- 从左侧拖拽到右侧,将提示词设为生效状态
- 在右侧区域内拖拽,调整提示词顺序
### 4.5 模拟数据结构
```javascript
const prompts = [
{
id: 1,
title: '示例提示词',
content: '这是一个示例提示词',
type: 'general', // animal, person, general
referenceImage: 'https://example.com/image.jpg',
isActive: false,
createdAt: new Date()
}
]
```
### 4.6 右侧删除功能实现
```javascript
// 从生效区域移除提示词
const removeFromActive = (promptId) => {
const prompt = prompts.find(p => p.id === promptId)
if (prompt) {
prompt.isActive = false
// 更新响应式数据
}
}
```
## 5. 预期效果
- 侧边栏新增提示词管理菜单项
- 提示词管理页面分为左右两栏
- 左侧可添加、编辑、删除提示词
- 支持拖拽提示词从左侧到右侧
- 右侧提示词可拖拽排序和删除
- 数据本地模拟实现
## 6. 文件修改清单
- `src/components/admin/AdminLayout.vue` - 添加侧边栏菜单项
- `src/router/index.js` - 添加路由配置
- `src/views/admin/AdminPromptManagement.vue` - 创建提示词管理页面
- `src/components/admin/PromptCard.vue` - 创建提示词卡片组件
- 语言文件 - 添加国际化支持
## 7. 注意事项
- 确保拖拽功能在各种浏览器中正常工作
- 实现响应式设计,适配不同屏幕尺寸
- 保持代码风格与现有代码一致
- 添加适当的用户反馈和验证

View File

@ -0,0 +1,41 @@
# 实现步骤
1. **创建新页面组件**:在 `src/views/` 目录下创建 `Waitlist.vue` 组件,用于展示"已加入候补队列,等待审核中"的提示
2. **添加多语言支持**:在 `src/locales/index.js` 中添加对应的中英文文本
3. **配置路由**:在 `src/router/index.js` 中添加一个新的路由,指向这个新组件
# 详细实现
## 1. 创建 Waitlist.vue 组件
* 参考 `NotFound.vue` 的结构和样式
* 展示"已加入候补队列,等待审核中"的提示信息
* 添加返回首页的按钮
* 适配响应式设计
## 2. 添加多语言支持
`src/locales/index.js``zh``en` 对象中分别添加:
* `waitlist.title`: 标题文本
* `waitlist.description`: 描述文本
* `waitlist.goHome`: 返回首页按钮文本
## 3. 配置路由
`src/router/index.js` 中添加一个新的路由:
```javascript
{
path: '/waitlist',
name: 'Waitlist',
component: () => import('@/views/Waitlist.vue'),
meta: { requiresAuth: false, keepAlive: false, fullScreen: true }
}
```

View File

@ -19,7 +19,8 @@
"three": "^0.180.0",
"vue": "^3.5.24",
"vue-i18n": "^9.14.2",
"vue-router": "^4.4.5"
"vue-router": "^4.4.5",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",

View File

@ -110,6 +110,10 @@
<el-icon><Coin /></el-icon>
<template #title>{{ t('admin.layout.commissionManagement') }}</template>
</el-menu-item>
<el-menu-item index="/admin/prompt-management">
<el-icon><Document /></el-icon>
<template #title>{{ t('admin.layout.promptManagement') }}</template>
</el-menu-item>
<el-sub-menu index="/admin/permission">
<template #title>

View File

@ -0,0 +1,229 @@
<template>
<div
class="prompt-card"
draggable="true"
@dragstart="onDragStart"
@click="handleClick"
>
<!-- 参考图片 -->
<div v-if="prompt.referenceImage" class="card-image">
<img :src="prompt.referenceImage" :alt="prompt.title" />
</div>
<!-- 卡片内容 -->
<div class="card-content">
<h4 class="card-title">{{ prompt.title }}</h4>
<div class="card-actions">
<el-button
text
size="small"
@click.stop="$emit('edit', prompt)"
class="action-btn edit"
>
<el-icon><EditPen /></el-icon>
</el-button>
<el-button
text
size="small"
@click.stop="handleDelete"
class="action-btn delete"
>
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
<!-- 生效标识 -->
<div v-if="isActive" class="active-badge">
<el-icon><Check /></el-icon>
{{ t('admin.promptManagement.active') }}
</div>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
import { EditPen, Delete, Check } from '@element-plus/icons-vue'
const { t } = useI18n()
//
const props = defineProps({
prompt: {
type: Object,
required: true,
default: () => ({
id: '',
title: '',
content: '',
type: '',
referenceImage: '',
isActive: false
})
},
isActive: {
type: Boolean,
default: false
}
})
//
const emit = defineEmits(['edit', 'delete', 'drag-start', 'click'])
//
const onDragStart = (event) => {
event.dataTransfer.effectAllowed = 'move'
emit('drag-start', props.prompt)
}
//
const handleClick = () => {
emit('click', props.prompt)
}
//
const handleDelete = () => {
emit('delete', props.prompt.id)
}
</script>
<style scoped>
.prompt-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
overflow: hidden;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
height: 180px;
display: flex;
flex-direction: column;
}
.prompt-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
border-color: #a78bfa;
}
/* 参考图片 */
.card-image {
width: 100%;
height: 120px;
overflow: hidden;
background: #f9fafb;
}
.card-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.prompt-card:hover .card-image img {
transform: scale(1.05);
}
/* 卡片内容 */
.card-content {
flex: 1;
padding: 12px;
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
}
/* 卡片标题 */
.card-title {
font-size: 14px;
font-weight: 600;
color: #374151;
margin: 0 0 8px 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
/* 卡片操作按钮 */
.card-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
opacity: 0;
transition: opacity 0.2s ease;
}
.prompt-card:hover .card-actions {
opacity: 1;
}
.action-btn {
padding: 0;
font-size: 16px;
transition: color 0.2s ease;
}
.action-btn.edit:hover {
color: #6b7280;
}
.action-btn.delete:hover {
color: #ef4444;
}
/* 生效标识 */
.active-badge {
position: absolute;
top: 8px;
right: 8px;
background: #10b981;
color: white;
font-size: 12px;
padding: 4px 8px;
border-radius: 12px;
display: flex;
align-items: center;
gap: 4px;
font-weight: 500;
z-index: 1;
}
.active-badge .el-icon {
font-size: 10px;
}
/* 无图片的卡片 */
.prompt-card:not(:has(.card-image)) {
height: 120px;
justify-content: center;
}
.prompt-card:not(:has(.card-image)) .card-content {
height: 100%;
justify-content: center;
}
.prompt-card:not(:has(.card-image)) .card-title {
text-align: center;
font-size: 16px;
-webkit-line-clamp: 3;
}
/* 响应式设计 */
@media (max-width: 768px) {
.prompt-card {
height: auto;
}
.card-actions {
opacity: 1;
}
}
</style>

View File

@ -0,0 +1,282 @@
<template>
<div
class="prompt-card-horizontal"
draggable="true"
@dragstart="onDragStart"
@click="handleClick"
>
<!-- 左侧图片 -->
<div v-if="prompt.referenceImage" class="card-image">
<img :src="prompt.referenceImage" :alt="prompt.title" />
</div>
<!-- 右侧内容 -->
<div class="card-content">
<div class="card-header">
<h4 class="card-title">{{ prompt.title }}</h4>
<el-tag :type="typeTagType" size="small">{{ getTypeLabel(prompt.type) }}</el-tag>
</div>
<div class="card-body">
<p class="card-description">{{ prompt.content }}</p>
</div>
<div class="card-footer">
<el-button
text
size="small"
@click.stop="$emit('edit', prompt)"
class="action-btn edit"
>
<el-icon><EditPen /></el-icon>
{{ t('common.edit') }}
</el-button>
<el-button
text
size="small"
@click.stop="handleDelete"
class="action-btn delete"
>
<el-icon><Delete /></el-icon>
{{ t('common.delete') }}
</el-button>
</div>
</div>
<!-- 生效标识 -->
<div class="active-badge">
<el-icon><Check /></el-icon>
{{ t('admin.promptManagement.active') }}
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { EditPen, Delete, Check } from '@element-plus/icons-vue'
const { t } = useI18n()
//
const props = defineProps({
prompt: {
type: Object,
required: true,
default: () => ({
id: '',
title: '',
content: '',
type: '',
referenceImage: '',
isActive: false
})
},
isActive: {
type: Boolean,
default: false
}
})
//
const emit = defineEmits(['edit', 'delete', 'drag-start', 'click'])
//
const typeTagType = computed(() => {
const typeMap = {
animal: 'success',
person: 'warning',
general: 'info'
}
return typeMap[props.prompt.type] || 'info'
})
//
const getTypeLabel = (type) => {
const typeMap = {
animal: t('admin.promptManagement.animal'),
person: t('admin.promptManagement.person'),
general: t('admin.promptManagement.general')
}
return typeMap[type] || type
}
//
const onDragStart = (event) => {
event.dataTransfer.effectAllowed = 'move'
emit('drag-start', props.prompt)
}
//
const handleClick = () => {
emit('click', props.prompt)
}
//
const handleDelete = () => {
emit('delete', props.prompt.id)
}
</script>
<style scoped>
.prompt-card-horizontal {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
overflow: hidden;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
display: flex;
min-height: 120px;
}
.prompt-card-horizontal:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
border-color: #a78bfa;
}
/* 左侧图片 */
.card-image {
width: 120px;
flex-shrink: 0;
overflow: hidden;
background: #f9fafb;
min-height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.card-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
display: block;
}
.prompt-card-horizontal:hover .card-image img {
transform: scale(1.05);
}
/* 右侧内容 */
.card-content {
flex: 1;
padding: 16px;
display: flex;
flex-direction: column;
gap: 8px;
min-width: 0;
}
/* 卡片头部 */
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 8px;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #374151;
margin: 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
/* 卡片内容 */
.card-body {
flex: 1;
overflow: hidden;
min-height: 0;
}
.card-description {
font-size: 14px;
color: #6b7280;
margin: 0;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
/* 卡片底部 */
.card-footer {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: auto;
}
.action-btn {
padding: 0;
font-size: 14px;
transition: color 0.2s ease;
}
.action-btn.edit:hover {
color: #6b7280;
}
.action-btn.delete:hover {
color: #ef4444;
}
/* 生效标识 */
.active-badge {
position: absolute;
top: 8px;
right: 8px;
background: #10b981;
color: white;
font-size: 12px;
padding: 4px 8px;
border-radius: 12px;
display: flex;
align-items: center;
gap: 4px;
font-weight: 500;
z-index: 1;
}
.active-badge .el-icon {
font-size: 10px;
}
/* 无图片的卡片 */
.prompt-card-horizontal:not(:has(.card-image)) {
min-height: 100px;
}
.prompt-card-horizontal:not(:has(.card-image)) .card-content {
padding: 16px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.prompt-card-horizontal {
flex-direction: column;
}
.card-image {
width: 100%;
height: 120px;
}
.card-header {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
}
</style>

View File

@ -264,6 +264,7 @@ orderManagement: {
userList: '用户列表',
pointsManagement: '充值包管理',
commissionManagement: '佣金管理',
promptManagement: '提示词管理',
logout: '退出登录',
profile: '个人资料',
settings: '设置',
@ -697,6 +698,31 @@ orderManagement: {
approved: '已通过',
rejected: '已拒绝'
}
},
promptManagement: {
title: '提示词管理',
addPrompt: '添加提示词',
editPrompt: '编辑提示词',
promptDetail: '提示词详情',
inactivePrompts: '未生效提示词',
activePrompts: '生效提示词',
type: '类型',
selectType: '选择类型',
animal: '动物',
person: '人物',
general: '通用',
title: '标题',
enterTitle: '请输入提示词标题',
content: '内容',
enterContent: '请输入提示词内容',
referenceImage: '参考图',
imageTip: '支持JPG、PNG格式大小不超过2MB',
active: '已生效',
deleteConfirm: '确定要删除这个提示词吗?',
deleteSuccess: '删除成功',
saveSuccess: '保存成功',
activeSuccess: '已设置为生效状态',
inactiveSuccess: '已从生效状态移除'
}
},

View File

@ -20,6 +20,7 @@ const AdminPermissionDetail = () => import('@/views/admin/AdminPermissionManagem
const AdminUserList = () => import('@/views/admin/AdminUserList.vue')
const AdminPointsManagement = () => import('@/views/admin/AdminPointsManagement.vue')
const AdminCommissionManagement = () => import('@/views/admin/AdminCommissionManagement.vue')
const AdminPromptManagement = () => import('@/views/admin/AdminPromptManagement.vue')
const routes = [
{
@ -178,6 +179,14 @@ const routes = [
meta: {
title: '佣金管理'
}
},
{
path: 'prompt-management',
name: 'AdminPromptManagement',
component: AdminPromptManagement,
meta: {
title: '提示词管理'
}
}
]
},

View File

@ -55,12 +55,12 @@
</div>
<!-- 列表内容 -->
<!-- stripe -->
<el-table
v-loading="loading"
:data="commissionList"
style="width: 100%"
border
stripe
>
<el-table-column
prop="creatorName"

View File

@ -79,7 +79,8 @@
<!-- 内容列表 -->
<el-card class="table-card" shadow="never">
<el-table :data="tableData" style="width: 100%" stripe>
<!-- stripe -->
<el-table :data="tableData" style="width: 100%" >
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="title" :label="$t('admin.content.title')" min-width="200" />
<el-table-column prop="author" :label="$t('admin.content.author')" width="120" />

View File

@ -62,9 +62,9 @@
<!-- 审核列表 -->
<div class="review-table" :style="{ height: tableHeight + 'px' }">
<!-- stripe -->
<el-table
:data="filteredReviewList"
stripe
style="width: 100%"
v-loading="loading"
:max-height="tableHeight"

View File

@ -54,9 +54,9 @@
<!-- 订单列表区域 -->
<div class="disassembly-table">
<!-- stripe -->
<el-table
:data="paginatedOrders"
stripe
style="width: 100%"
v-loading="loading"
>

View File

@ -117,9 +117,9 @@
<!-- 订单列表 -->
<div class="orders-list">
<!-- stripe -->
<el-table
:data="filteredOrdersList"
stripe
style="width: 100%"
v-loading="loading"
@row-click="handleOrderDetail"
@ -251,7 +251,8 @@
<div class="detail-section">
<h4>{{ t('admin.orders.items') }}</h4>
<el-table :data="selectedOrder.items" stripe>
<!-- stripe -->
<el-table :data="selectedOrder.items" >
<el-table-column prop="name" :label="t('admin.orders.itemName')" />
<el-table-column prop="quantity" :label="t('admin.orders.quantity')" width="100" />
<el-table-column prop="price" :label="t('admin.orders.price')" width="120">

View File

@ -12,7 +12,8 @@
</div>
</template>
<div class="card-body">
<el-table :data="permissionList" stripe style="width: 100%">
<!-- stripe -->
<el-table :data="permissionList" style="width: 100%">
<el-table-column prop="permName" :label="t('admin.permissionManagement.permissionName')" width="180" />
<el-table-column prop="permCode" :label="t('admin.permissionManagement.permissionCode')" width="180" />
<el-table-column prop="module" :label="t('admin.permissionManagement.module')" width="120" />

View File

@ -13,7 +13,8 @@
</div>
</template>
<div class="card-body">
<el-table :data="pointsPackages" stripe style="width: 100%">
<!-- stripe -->
<el-table :data="pointsPackages" style="width: 100%">
<el-table-column prop="name" :label="t('admin.pointsManagement.pointsPackage')" width="200" />
<el-table-column :label="t('admin.pointsManagement.price')" width="180">
<template #default="scope">

View File

@ -0,0 +1,656 @@
<template>
<div class="prompt-management">
<!-- 页面头部 -->
<div class="page-header">
<h2>{{ t('admin.promptManagement.title') }}</h2>
<el-button type="primary" @click="showAddDialog">
<el-icon><Plus /></el-icon>
{{ t('admin.promptManagement.addPrompt') }}
</el-button>
</div>
<!-- 左右分栏容器 -->
<div class="prompt-container">
<!-- 左侧未生效提示词区域 -->
<div class="prompt-section">
<div class="section-header">
<h3>{{ t('admin.promptManagement.inactivePrompts') }}</h3>
<span class="count">{{ inactivePrompts.length }}</span>
</div>
<div class="prompt-list">
<PromptCard
v-for="prompt in inactivePrompts"
:key="prompt.id"
:prompt="prompt"
@edit="showEditDialog"
@delete="deletePrompt"
@click="showDetail"
@drag-start="handleLeftDragStart"
/>
</div>
</div>
<!-- 右侧生效提示词区域 -->
<div class="prompt-section">
<div class="section-header">
<h3>{{ t('admin.promptManagement.activePrompts') }}</h3>
<span class="count">{{ activePrompts.length }}</span>
</div>
<div
class="prompt-list active-list"
@drop="handleDrop"
@dragover="handleDragOver"
>
<draggable
v-model="sortedActivePrompts"
item-key="id"
@start="handleDragStart"
@update="handleSortUpdate"
:animation="200"
:ghost-class="'ghost-card'"
:chosen-class="'chosen-card'"
:drag-class="'dragging-card'"
:axis="'y'"
>
<template #item="{ element: prompt }">
<PromptCardHorizontal
:prompt="prompt"
:isActive="true"
@edit="showEditDialog"
@delete="removeFromActive"
@click="showDetail"
/>
</template>
</draggable>
</div>
</div>
</div>
<!-- 添加/编辑弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="isEditing ? t('admin.promptManagement.editPrompt') : t('admin.promptManagement.addPrompt')"
width="600px"
>
<el-form :model="formData" label-width="80px">
<el-form-item :label="t('admin.promptManagement.type')" required>
<el-select v-model="formData.type" :placeholder="t('admin.promptManagement.selectType')">
<el-option :label="t('admin.promptManagement.animal')" value="animal" />
<el-option :label="t('admin.promptManagement.person')" value="person" />
<el-option :label="t('admin.promptManagement.general')" value="general" />
</el-select>
</el-form-item>
<el-form-item :label="t('admin.promptManagement.title')" required>
<el-input v-model="formData.title" :placeholder="t('admin.promptManagement.enterTitle')" />
</el-form-item>
<el-form-item :label="t('admin.promptManagement.content')" required>
<el-input
v-model="formData.content"
type="textarea"
:rows="4"
:placeholder="t('admin.promptManagement.enterContent')"
/>
</el-form-item>
<el-form-item :label="t('admin.promptManagement.referenceImage')">
<el-upload
v-model:file-list="fileList"
action="#"
list-type="picture-card"
:auto-upload="false"
:limit="1"
:on-change="handleImageChange"
>
<el-icon><Plus /></el-icon>
<template #tip>
<div class="el-upload__tip">
{{ t('admin.promptManagement.imageTip') }}
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">{{ t('common.cancel') }}</el-button>
<el-button type="primary" @click="savePrompt">{{ t('common.save') }}</el-button>
</span>
</template>
</el-dialog>
<!-- 提示词详情弹窗 -->
<el-dialog
v-model="detailVisible"
:title="t('admin.promptManagement.promptDetail')"
width="600px"
>
<div v-if="selectedPrompt" class="prompt-detail">
<div class="detail-item">
<label>{{ t('admin.promptManagement.type') }}:</label>
<span>{{ getTypeLabel(selectedPrompt.type) }}</span>
</div>
<div class="detail-item">
<label>{{ t('admin.promptManagement.title') }}:</label>
<span>{{ selectedPrompt.title }}</span>
</div>
<div class="detail-item">
<label>{{ t('admin.promptManagement.content') }}:</label>
<p>{{ selectedPrompt.content }}</p>
</div>
<div class="detail-item" v-if="selectedPrompt.referenceImage">
<label>{{ t('admin.promptManagement.referenceImage') }}:</label>
<img :src="selectedPrompt.referenceImage" alt="参考图" class="reference-image" />
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="detailVisible = false">{{ t('common.close') }}</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { Plus } from '@element-plus/icons-vue'
import PromptCard from '@/components/admin/PromptCard.vue'
import PromptCardHorizontal from '@/components/admin/PromptCardHorizontal.vue'
import draggable from 'vuedraggable'
const { t } = useI18n()
//
const dialogVisible = ref(false)
const detailVisible = ref(false)
const isEditing = ref(false)
const selectedPromptId = ref(null)
const selectedPrompt = ref(null)
const draggedPrompt = ref(null)
const fileList = ref([])
//
const formData = ref({
title: '',
content: '',
type: '',
referenceImage: ''
})
//
const prompts = ref([
{
id: 1,
title: '可爱的小狗',
content: '一只可爱的白色小狗,毛茸茸的,大眼睛,活泼好动',
type: 'animal',
referenceImage: 'https://picsum.photos/id/237/300/200',
isActive: false,
sortIndex: 0,
createdAt: new Date()
},
{
id: 2,
title: '商务人士',
content: '一位穿着西装的商务人士,自信的表情,背景是办公室',
type: 'person',
referenceImage: '',
isActive: false,
sortIndex: 1,
createdAt: new Date()
},
{
id: 3,
title: '美丽的风景',
content: '一片美丽的自然风光,蓝天白云,绿水青山',
type: 'general',
referenceImage: 'https://picsum.photos/id/1015/300/200',
isActive: true,
sortIndex: 0,
createdAt: new Date()
},
{
id: 4,
title: '优雅的猫咪',
content: '一只优雅的黑色猫咪,黄色的眼睛,柔软的毛发',
type: 'animal',
referenceImage: 'https://picsum.photos/id/452/300/200',
isActive: false,
sortIndex: 2,
createdAt: new Date()
},
{
id: 5,
title: '医生',
content: '一位穿着白大褂的医生,戴着口罩,手持听诊器',
type: 'person',
referenceImage: 'https://picsum.photos/id/225/300/200',
isActive: false,
sortIndex: 3,
createdAt: new Date()
},
{
id: 6,
title: '现代办公室',
content: '一个现代化的办公室,整洁的桌面,舒适的座椅',
type: 'general',
referenceImage: 'https://picsum.photos/id/1059/300/200',
isActive: true,
sortIndex: 1,
createdAt: new Date()
},
{
id: 7,
title: '强壮的狮子',
content: '一只强壮的狮子,金色的鬃毛,威严的表情',
type: 'animal',
referenceImage: 'https://picsum.photos/id/287/300/200',
isActive: false,
sortIndex: 4,
createdAt: new Date()
},
{
id: 8,
title: '教师',
content: '一位和蔼可亲的教师,手持书本,站在讲台前',
type: 'person',
referenceImage: '',
isActive: false,
sortIndex: 5,
createdAt: new Date()
},
{
id: 9,
title: '科技感会议室',
content: '一个充满科技感的会议室,大屏幕,智能设备',
type: 'general',
referenceImage: 'https://picsum.photos/id/1069/300/200',
isActive: true,
sortIndex: 2,
createdAt: new Date()
},
{
id: 10,
title: '活泼的兔子',
content: '一只活泼的灰色兔子,长长的耳朵,蹦蹦跳跳',
type: 'animal',
referenceImage: 'https://picsum.photos/id/297/300/200',
isActive: false,
sortIndex: 6,
createdAt: new Date()
},
{
id: 11,
title: '艺术家',
content: '一位专注的艺术家,正在画板前创作',
type: 'person',
referenceImage: 'https://picsum.photos/id/447/300/200',
isActive: false,
sortIndex: 7,
createdAt: new Date()
},
{
id: 12,
title: '温馨的咖啡馆',
content: '一个温馨的咖啡馆,柔和的灯光,舒适的氛围',
type: 'general',
referenceImage: 'https://picsum.photos/id/438/300/200',
isActive: true,
sortIndex: 3,
createdAt: new Date()
},
{
id: 13,
title: '聪明的鹦鹉',
content: '一只色彩鲜艳的鹦鹉,站在树枝上',
type: 'animal',
referenceImage: '',
isActive: false,
sortIndex: 8,
createdAt: new Date()
},
{
id: 14,
title: '运动员',
content: '一位正在跑步的运动员,充满活力',
type: 'person',
referenceImage: 'https://picsum.photos/id/340/300/200',
isActive: false,
sortIndex: 9,
createdAt: new Date()
},
{
id: 15,
title: '科幻城市',
content: '一个未来的科幻城市,高楼大厦,飞行器',
type: 'general',
referenceImage: 'https://picsum.photos/id/1058/300/200',
isActive: true,
sortIndex: 4,
createdAt: new Date()
}
])
//
const inactivePrompts = computed(() => {
return prompts.value.filter(prompt => !prompt.isActive)
})
//
const activePrompts = computed(() => {
return prompts.value.filter(prompt => prompt.isActive)
})
//
const sortedActivePrompts = computed({
get: () => {
return prompts.value
.filter(prompt => prompt.isActive)
.sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0))
},
set: (newValue) => {
// sortIndex
newValue.forEach((prompt, index) => {
const originalPrompt = prompts.value.find(p => p.id === prompt.id)
if (originalPrompt) {
originalPrompt.sortIndex = index
}
})
}
})
//
const showAddDialog = () => {
isEditing.value = false
selectedPromptId.value = null
formData.value = {
title: '',
content: '',
type: '',
referenceImage: ''
}
fileList.value = []
dialogVisible.value = true
}
//
const showEditDialog = (prompt) => {
isEditing.value = true
selectedPromptId.value = prompt.id
formData.value = {
title: prompt.title,
content: prompt.content,
type: prompt.type,
referenceImage: prompt.referenceImage
}
fileList.value = prompt.referenceImage ? [{ url: prompt.referenceImage }] : []
dialogVisible.value = true
}
//
const savePrompt = () => {
if (isEditing.value) {
//
const index = prompts.value.findIndex(p => p.id === selectedPromptId.value)
if (index !== -1) {
prompts.value[index] = {
...prompts.value[index],
title: formData.value.title,
content: formData.value.content,
type: formData.value.type,
referenceImage: formData.value.referenceImage
}
}
} else {
//
const newPrompt = {
id: Date.now(),
title: formData.value.title,
content: formData.value.content,
type: formData.value.type,
referenceImage: formData.value.referenceImage,
isActive: false,
sortIndex: prompts.value.filter(p => !p.isActive).length,
createdAt: new Date()
}
prompts.value.push(newPrompt)
}
dialogVisible.value = false
}
//
const deletePrompt = (promptId) => {
prompts.value = prompts.value.filter(p => p.id !== promptId)
}
//
const removeFromActive = (promptId) => {
const prompt = prompts.value.find(p => p.id === promptId)
if (prompt) {
prompt.isActive = false
}
}
//
const handleImageChange = (file) => {
if (file.raw) {
const reader = new FileReader()
reader.onload = (e) => {
formData.value.referenceImage = e.target.result
}
reader.readAsDataURL(file.raw)
}
}
//
const handleLeftDragStart = (prompt) => {
draggedPrompt.value = prompt
}
//
const handleDragStart = (evt) => {
draggedPrompt.value = evt.item.__vnode.props.prompt
}
//
const handleSortUpdate = (evt) => {
// sortedActivePromptssetter
}
//
const handleDrop = (event) => {
event.preventDefault()
if (draggedPrompt.value && !draggedPrompt.value.isActive) {
//
draggedPrompt.value.isActive = true
// sortIndex
draggedPrompt.value.sortIndex = sortedActivePrompts.value.length
draggedPrompt.value = null
}
}
//
const handleDragOver = (event) => {
event.preventDefault()
}
//
const showDetail = (prompt) => {
selectedPrompt.value = prompt
detailVisible.value = true
}
//
const getTypeLabel = (type) => {
const typeMap = {
animal: t('admin.promptManagement.animal'),
person: t('admin.promptManagement.person'),
general: t('admin.promptManagement.general')
}
return typeMap[type] || type
}
</script>
<style scoped>
.prompt-management {
width: 100%;
height: 100%;
}
/* 页面头部 */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 16px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.page-header h2 {
font-size: 24px;
font-weight: 600;
color: #1f2937;
margin: 0;
}
/* 左右分栏容器 */
.prompt-container {
display: flex;
gap: 24px;
height: calc(100% - 120px);
}
/* 提示词区域 */
.prompt-section {
flex: 1;
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
overflow: hidden;
display: flex;
flex-direction: column;
}
/* 区域头部 */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #e5e7eb;
background: #f9fafb;
}
.section-header h3 {
font-size: 18px;
font-weight: 600;
color: #374151;
margin: 0;
}
.count {
background: #6b7280;
color: white;
font-size: 12px;
padding: 2px 8px;
border-radius: 12px;
font-weight: 500;
}
/* 左侧区域 */
.prompt-section:first-child {
flex: 2;
}
/* 右侧区域 */
.prompt-section:last-child {
flex: 1;
}
/* 提示词列表 */
.prompt-list {
flex: 1;
padding: 16px;
overflow-y: auto;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
align-content: start;
}
/* 生效提示词列表 */
.active-list {
background: #f0fdf4;
min-height: 200px;
grid-template-columns: 1fr;
gap: 12px;
}
/* 拖拽相关样式 */
.ghost-card {
opacity: 0.5;
background: #e0f2fe;
border: 2px dashed #3b82f6;
transition: all 0.2s ease;
}
.chosen-card {
box-shadow: 0 0 0 2px #3b82f6;
transform: scale(1.02);
transition: all 0.2s ease;
}
.dragging-card {
opacity: 0.8;
transform: rotate(3deg);
transition: all 0.2s ease;
z-index: 1000;
}
/* 提示词详情 */
.prompt-detail {
padding: 16px 0;
}
.detail-item {
margin-bottom: 16px;
}
.detail-item label {
display: block;
font-weight: 600;
color: #374151;
margin-bottom: 8px;
}
.detail-item span, .detail-item p {
color: #6b7280;
line-height: 1.6;
}
.reference-image {
max-width: 100%;
border-radius: 8px;
margin-top: 8px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.prompt-container {
flex-direction: column;
height: auto;
}
.prompt-list {
grid-template-columns: 1fr;
}
}
</style>

View File

@ -39,7 +39,8 @@
</div>
</template>
<div class="card-body">
<el-table :data="rolePermissions" stripe style="width: 100%">
<!-- stripe -->
<el-table :data="rolePermissions" style="width: 100%">
<el-table-column prop="permName" :label="t('admin.permissionManagement.permissionName')" width="180" />
<el-table-column prop="permCode" :label="t('admin.permissionManagement.permissionCode')" width="180" />
<el-table-column prop="module" :label="t('admin.permissionManagement.module')" width="150" />

View File

@ -12,7 +12,8 @@
</div>
</template>
<div class="card-body">
<el-table :data="roleList" stripe style="width: 100%">
<!-- stripe -->
<el-table :data="roleList" style="width: 100%">
<el-table-column prop="roleName" :label="t('admin.roleManagement.roleName')" width="180" />
<el-table-column prop="roleCode" :label="t('admin.roleManagement.roleCode')" width="180" />
<el-table-column prop="description" :label="t('admin.roleManagement.description')" />

View File

@ -9,7 +9,8 @@
</div>
</template>
<div class="card-body">
<el-table :data="routeList" stripe style="width: 100%">
<!-- stripe -->
<el-table :data="routeList" style="width: 100%">
<el-table-column prop="path" :label="t('admin.routeManagement.path')" width="200" />
<el-table-column prop="name" :label="t('admin.routeManagement.name')" width="180" />
<el-table-column prop="title" :label="t('admin.routeManagement.title')" />

View File

@ -22,7 +22,8 @@
</el-input>
</div>
<el-table :data="userList" stripe style="width: 100%" v-loading="loading">
<!-- stripe -->
<el-table :data="userList" style="width: 100%" v-loading="loading">
<el-table-column prop="username" :label="t('admin.userList.username')" width="180" />
<el-table-column prop="nickname" :label="t('admin.userList.nickname')" width="180" />
<el-table-column prop="email" :label="t('admin.userList.email')" />

View File

@ -43,9 +43,9 @@
<!-- 邀请码列表 -->
<div class="invite-table">
<!-- stripe -->
<el-table
:data="inviteCodeList"
stripe
style="width: 100%"
v-loading="loadingCode"
>
@ -145,9 +145,9 @@
<el-tab-pane label="邀请用户列表" name="invitedUsers">
<!-- 列表区域 -->
<div class="invite-table">
<!-- stripe -->
<el-table
:data="inviteList"
stripe
style="width: 100%"
v-loading="loading"
>

View File

@ -76,9 +76,9 @@
<!-- 用户列表 -->
<div class="user-table">
<!-- stripe -->
<el-table
:data="userList"
stripe
style="width: 100%"
v-loading="loading"
>

View File

@ -23,7 +23,8 @@ export class AdminOrders {
}
const requestUrl = {
method: adminApi.default.getUserDetail.method,
url: adminApi.default.getUserDetail.url.replace('USERID', params.id)
url: adminApi.default.getUserDetail.url.replace('USERID', params.id),
isLoading: adminApi.default.getUserDetail.isLoading
}
return requestUtils.common(requestUrl, params);
}
@ -35,7 +36,8 @@ export class AdminOrders {
}
const requestUrl = {
method: adminApi.default.updateUserStatus.method,
url: (adminApi.default.updateUserStatus.url.replace('USERID', params.id))+'?'+'id='+data.id+'&status='+data.status
url: (adminApi.default.updateUserStatus.url.replace('USERID', params.id))+'?'+'id='+data.id+'&status='+data.status,
isLoading: adminApi.default.updateUserStatus.isLoading
}
return requestUtils.common(requestUrl, params);
}
@ -47,7 +49,8 @@ export class AdminOrders {
}
const requestUrl = {
method: adminApi.default.updateUserName.method,
url: adminApi.default.updateUserName.url.replace('USERID', params.id)
url: adminApi.default.updateUserName.url.replace('USERID', params.id),
isLoading: adminApi.default.updateUserName.isLoading
}
return requestUtils.common(requestUrl, params);
}
@ -62,7 +65,8 @@ export class AdminOrders {
}
const requestUrl = {
method: adminApi.default.getUsersinvites.method,
url: adminApi.default.getUsersinvites.url.replace('USERID', params.id)
url: adminApi.default.getUsersinvites.url.replace('USERID', params.id),
isLoading: adminApi.default.getUsersinvites.isLoading
}
return requestUtils.common(requestUrl, params);
}
@ -74,7 +78,8 @@ export class AdminOrders {
}
const requestUrl = {
method: adminApi.default.changeRole.method,
url: adminApi.default.changeRole.url.replace('USERID', params.id)
url: adminApi.default.changeRole.url.replace('USERID', params.id),
isLoading: adminApi.default.changeRole.isLoading
}
return requestUtils.common(requestUrl, params);
}
@ -111,4 +116,136 @@ export class AdminOrders {
}
return requestUtils.common(requestUrl, params);
}
//创建管理员用户
async createAdminUser(data) {
let params = {
username: data.username || '',
email: data.email || '',
password: data.password || '',
fullName: data.fullName || '',
isActive: data.isActive !== undefined ? data.isActive : true,
isSuperuser: data.isSuperuser !== undefined ? data.isSuperuser : false,
roleIds: Array.isArray(data.roleIds) ? data.roleIds : []
}
return requestUtils.common(adminApi.default.createAdminUser, params);
}
//分页查询管理员用户列表
async getAdminUsersList(data) {
let params = {
username: data.username || '',
email: data.email || '',
isActive: data.isActive !== undefined ? data.isActive : true,
pageSize: data.pageSize || 10,
pageNum: data.pageNum || 1,
orderByColumn: data.orderByColumn || '',
isAsc: data.isAsc || 'asc'
}
return requestUtils.common(adminApi.default.getAdminUserList, params);
/**
返回示例
{
"code": 0,
"success": true,
"data": {
"total": 9007199254740991,
"rows": [
{
"id": 9007199254740991,
"username": "string",
"email": "string",
"fullName": "string",
"isActive": true,
"isSuperuser": true,
"lastLogin": "2025-12-19T05:33:51.763Z",
"createdAt": "2025-12-19T05:33:51.763Z",
"updatedAt": "2025-12-19T05:33:51.763Z",
"roles": [
{
"id": 9007199254740991,
"roleCode": "string",
"roleName": "string",
"description": "string"
}
]
}
],
"code": 1073741824,
"msg": "string"
},
"message": "操作成功"
}
*/
}
//更新管理员用户
async updateAdminUser(data) {
let params = {
id: data.id,
username: data.username,
email: data.email,
fullName: data.fullName,
isActive: data.isActive,
isSuperuser: data.isSuperuser,
roleIds: data.roleIds
}
return requestUtils.common(adminApi.default.updateAdminUser, params);
}
//启用/禁用用户
async enableDisableUser(data) {
let params = {
userId: data.id,
isActive : data.isActive
}
return requestUtils.common(adminApi.default.toggleAdminUserStatus, params);
}
//重置用户密码
async resetUserPassword(data) {
let params = {
userId: data.id,
newPassword: data.newPassword
}
return requestUtils.common(adminApi.default.resetAdminUserPassword, params);
}
//根据用户ID查询管理员用户详情
async getAdminUserDetail(data) {
let params = {
userId : data.id || ''
}
const requestUrl = {
method: adminApi.default.getAdminUserDetail.method,
url: adminApi.default.getAdminUserDetail.url.replace('USERID', params.id),
isLoading: adminApi.default.getAdminUserDetail.isLoading
}
return requestUtils.common(requestUrl, params);
/**
{
"code": 0,
"success": true,
"data": {
"id": 9007199254740991,
"username": "string",
"email": "string",
"fullName": "string",
"isActive": true,
"isSuperuser": true,
"lastLogin": "2025-12-19T05:36:04.270Z",
"createdAt": "2025-12-19T05:36:04.270Z",
"updatedAt": "2025-12-19T05:36:04.270Z",
"roles": [
{
"id": 9007199254740991,
"roleCode": "string",
"roleName": "string",
"description": "string"
}
]
},
"message": "操作成功"
}
*/
}
//批量删除管理员用户
async deleteAdminUsers(data) {
let arr = data.ids || []
return requestUtils.common(adminApi.default.batchDeleteAdminUser, arr);
}
}

View File

@ -10,7 +10,6 @@
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@google/genai": "^1.27.0",
"@stripe/stripe-js": "^4.8.0",
"@types/three": "^0.180.0",
"@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.4",
@ -1100,15 +1099,6 @@
"win32"
]
},
"node_modules/@stripe/stripe-js": {
"version": "4.10.0",
"resolved": "https://registry.npmmirror.com/@stripe/stripe-js/-/stripe-js-4.10.0.tgz",
"integrity": "sha512-KrMOL+sH69htCIXCaZ4JluJ35bchuCCznyPyrbN8JXSGQfwBI1SuIEMZNwvy8L8ykj29t6sa5BAAiL7fNoLZ8A==",
"license": "MIT",
"engines": {
"node": ">=12.16"
}
},
"node_modules/@tweenjs/tween.js": {
"version": "23.1.3",
"resolved": "https://registry.npmmirror.com/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",

View File

@ -13,7 +13,6 @@
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@google/genai": "^1.27.0",
"@stripe/stripe-js": "^4.8.0",
"@twind/core": "^1.1.3",
"@twind/preset-autoprefix": "^1.0.7",
"@twind/preset-tailwind": "^1.1.4",
@ -24,6 +23,7 @@
"country-state-city": "^3.2.1",
"dayjs": "^1.11.13",
"element-plus": "^2.11.7",
"install": "^0.13.0",
"jose": "^6.1.1",
"motion-v": "^1.7.4",
"normalize.css": "^8.0.1",

View File

@ -98,7 +98,9 @@ const isTouching = ref(false);
const isControlsVisible = ref(false);
const cjimg = 'https://draft-user.s3.us-east-2.amazonaws.com/images/14f98f33-06a7-4629-a42e-d7cfbced786f';
const anTypeImg = 'https://draft-user.s3.us-east-2.amazonaws.com/images/1e82b2b6-0e5d-4a62-b65f-098952eb2f67';
const humanTypeImg = 'https://draft-user.s3.us-east-2.amazonaws.com/images/e3e60cc7-9777-41ba-9d1e-f5ffc92e4fac.webp';
// const humanTypeImg = 'https://draft-user.s3.us-east-2.amazonaws.com/images/e3e60cc7-9777-41ba-9d1e-f5ffc92e4fac.webp';
// const humanTypeImg = 'https://draft-user.s3.us-east-2.amazonaws.com/images/61770f50-4b87-40a0-9297-cabda0ec6317.webp'
const humanTypeImg = ''
const giminiServer = new GiminiServer();
//
const imageAspectRatio = ref(9/16); //
@ -218,9 +220,9 @@ const handleGenerateImage = async () => {
}
if(props?.cardData?.ipType){
if(props?.cardData?.ipType==1){
referenceImages.push(humanTypeImg);
humanTypeImg&&referenceImages.push(humanTypeImg);
}else{
referenceImages.push(anTypeImg);
anTypeImg&&referenceImages.push(anTypeImg);
}
}
if(iscjt){
@ -277,6 +279,8 @@ const handleGenerateImage = async () => {
保证角色所有的服饰衣服都为木头材质颜色并且要带一些木头纹理颜色为#e2cfb3
`
;
// 姿${props.cardData.ipType==1?``:``}
if(props.cardData.prompt&&props.cardData.prompt.indexOf('nospec')!=-1){
prompt = '按原图生成'
referenceImages = [props.cardData.inspirationImage];

View File

@ -140,4 +140,18 @@
// 保证角色所有的服饰衣服都为木头材质颜色,并且要带一些木头纹理,颜色为#bfa888。
// `
const qwentsc = `
角色肤色和衣服材质都为纯色一种颜色如下
重点保证角色所有的服饰衣服都为木头材质颜色并且要带一些木头纹理颜色为#e2cfb3
一个通体由单一纯色木材雕刻而成的角色整体呈现木质雕塑或木偶风格.
A full-body character portrait
角色特征Q 版萌系造型头身比例夸张大头小身神态纯真服饰设计融合童话风与复古感
姿势身体笔直正对镜头双臂自然下垂但略微外展肘部微屈约15度手掌张开放松掌心朝向身体内侧
双腿并拢伸直双脚平放于地面脚尖朝前
眼睛放大设计眼眶结构宽大清晰单侧眼球/眼窝直径不小于2cm即半径1cm预留足够空间用于嵌入镜头设备眼型饱满边缘厚实适合3D打印安装
嘴巴小巧表情温柔童真
Style:潮玩盲盒角色设计采用 3D 立体建模渲染呈现细腻的质感与精致的细节
Note: The image should not have white borders.
去除原图中复杂的背景只保留人物角色的主体
`
export const cjt = `将玩偶放在桌子玻璃正中间上,原图放在图中桌面上的相框里面[CJT_DEOTA]`

View File

@ -61,11 +61,8 @@
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { ElMessage, ElLoading } from 'element-plus'
import { CreditCard, Lock } from '@element-plus/icons-vue'
import { loadStripe } from '@stripe/stripe-js'
import { useI18n } from 'vue-i18n'
import { PayServer } from '@deotaland/utils'
// Stripe
const STRIPE_PUBLISHABLE_KEY = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY
const { t } = useI18n()
// Props
const props = defineProps({
@ -90,8 +87,6 @@ const props = defineProps({
// Emits
const emit = defineEmits(['payment-success', 'payment-error', 'cancel'])
//
const stripe = ref(null)
const elements = ref(null)
const cardElement = ref(null)
const processing = ref(false)
@ -102,6 +97,7 @@ const couponSuccess = ref('')
const discountAmount = ref(0)
const taxAmount = ref(0)
const shippingAmount = ref(0)
const isInitialized = ref(false) //
//
// Stripe
@ -115,6 +111,11 @@ const finalAmount = computed(() => {
const payServer = new PayServer()
// Stripe
const initializeStripe = async () => {
//
if (isInitialized.value) {
return;
}
try {
const data = await payServer.createPaymentIntent(finalAmount.value, props.currency, {
order_id: props.orderId,
@ -122,6 +123,8 @@ const initializeStripe = async () => {
})
elements.value = data.element
cardElement.value = data.cardElement
isInitialized.value = true;
//
cardElement.value.on('change', ({ error }) => {
const displayError = document.getElementById('card-errors')
@ -249,6 +252,8 @@ onUnmounted(() => {
if (elements.value) {
elements.value = null
}
//
isInitialized.value = false;
})
//

View File

@ -0,0 +1,711 @@
<template>
<form class="phone-login-form" @submit.prevent="handleSubmit">
<!-- 登录方式切换 -->
<div class="login-mode-toggle">
<button
type="button"
class="mode-button"
:class="{ 'active': loginMode === 'login' }"
@click="loginMode = 'login'"
>
{{ t('login.phone_login_mode') }}
</button>
<button
type="button"
class="mode-button"
:class="{ 'active': loginMode === 'register' }"
@click="loginMode = 'register'"
>
{{ t('login.phone_register_mode') }}
</button>
</div>
<!-- 手机号输入 -->
<div class="form-group">
<label class="form-label" for="phone">{{ t('login.phone_label') }}</label>
<div class="input-wrapper" :class="{ 'focused': isPhoneFocused }">
<input
id="phone"
v-model="form.phone"
type="tel"
class="form-input"
:class="{ 'error': phoneError }"
:placeholder="t('login.phone_placeholder')"
@focus="isPhoneFocused = true"
@blur="isPhoneFocused = false"
:disabled="loading"
autocomplete="tel"
/>
<div v-if="phoneError" class="input-error-icon">
<el-icon><WarningFilled /></el-icon>
</div>
</div>
<div v-if="phoneError" class="error-message">{{ phoneError }}</div>
</div>
<!-- 验证码输入 -->
<div 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
id="code"
v-model="form.code"
type="text"
class="form-input verification-code-input"
:class="{ 'error': codeError }"
:placeholder="t('login.code_placeholder')"
@focus="isCodeFocused = true"
@blur="isCodeFocused = false"
:disabled="loading"
autocomplete="one-time-code"
/>
<button
type="button"
class="send-code-button"
:disabled="loading || !canSendCode || !isPhoneValid"
@click="handleSendCode"
>
<span v-if="!countdown">{{ t('login.send_code') }}</span>
<span v-else>{{ t('login.resend_code_after', { seconds: countdown }) }}</span>
</button>
<div v-if="codeError" class="input-error-icon">
<el-icon><WarningFilled /></el-icon>
</div>
</div>
<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 class="input-wrapper" :class="{ 'focused': isPasswordFocused }">
<input
id="password"
v-model="form.password"
:type="showPassword ? 'text' : 'password'"
class="form-input"
:class="{ 'error': passwordError }"
:placeholder="t('login.password_placeholder')"
@focus="isPasswordFocused = true"
@blur="isPasswordFocused = false"
:disabled="loading"
autocomplete="new-password"
/>
<button
type="button"
class="password-toggle"
@click="showPassword = !showPassword"
:disabled="loading"
>
<el-icon v-if="showPassword"><View /></el-icon>
<el-icon v-else><Hide /></el-icon>
</button>
<div v-if="passwordError" class="input-error-icon">
<el-icon><WarningFilled /></el-icon>
</div>
</div>
<div v-if="passwordError" class="error-message">{{ passwordError }}</div>
</div>
<!-- 邀请码输入 -->
<InviteCodeInput
v-model="form.inviteCode"
:error="inviteCodeError"
:disabled="loading"
@validate="validateInviteCode"
/>
<!-- 登录/注册按钮 -->
<button
type="submit"
class="login-submit-button"
:disabled="loading || !isFormValid"
>
<span class="button-text">
<span v-if="!loading">
{{ loginMode === 'login' ? t('login.phone_login_button') : t('login.phone_register_button') }}
</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>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
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 props = defineProps({
loading: {
type: Boolean,
default: false
}
})
// login register
const loginMode = ref('login')
//
const form = ref({
phone: '',
code: '',
password: '',
inviteCode: ''
})
//
const isPhoneFocused = ref(false)
const isCodeFocused = ref(false)
const isPasswordFocused = ref(false)
const showPassword = ref(false)
//
const phoneError = ref('')
const codeError = ref('')
const passwordError = ref('')
const inviteCodeError = ref('')
//
const countdown = ref(0)
const canSendCode = ref(true)
//
const rules = {
phone: { required },
code: { required, minLength: minLength(4) },
password: {
required: computed(() => loginMode.value === 'register'),
minLength: minLength(6)
},
inviteCode: {}
}
const v$ = useVuelidate(rules, form)
//
const isPhoneValid = computed(() => {
return /^1[3-9]\d{9}$/.test(form.value.phone)
})
//
const isFormValid = computed(() => {
if (!form.value.phone || !form.value.code) {
return false
}
if (loginMode.value === 'register' && !form.value.password) {
return false
}
return !phoneError.value && !codeError.value && !passwordError.value
})
//
const validatePhone = () => {
if (!form.value.phone) {
phoneError.value = t('login.phone_empty_error')
} else if (!/^1[3-9]\d{9}$/.test(form.value.phone)) {
phoneError.value = t('login.phone_invalid_error')
} else {
phoneError.value = ''
}
}
const validateCode = () => {
if (!form.value.code) {
codeError.value = t('login.code_empty_error')
} else if (form.value.code.length < 4) {
codeError.value = t('login.code_invalid_error')
} else {
codeError.value = ''
}
}
const validatePassword = () => {
if (loginMode.value === 'register') {
if (!form.value.password) {
passwordError.value = t('login.password_empty_error')
} else if (form.value.password.length < 6) {
passwordError.value = t('login.password_min_error')
} else {
passwordError.value = ''
}
} else {
passwordError.value = ''
}
}
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)
//
const handleSendCode = async () => {
if (!isPhoneValid.value || countdown.value > 0) {
return
}
//
try {
canSendCode.value = false
countdown.value = 60
// API
console.log('发送验证码到:', form.value.phone)
//
const timer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(timer)
canSendCode.value = true
}
}, 1000)
} catch (error) {
console.error('发送验证码失败:', error)
canSendCode.value = true
countdown.value = 0
}
}
//
const handleSubmit = async () => {
//
await v$.value.$validate()
if (v$.value.$invalid) {
validatePhone()
validateCode()
validatePassword()
return
}
//
if (loginMode.value === 'login') {
emit('login', form.value)
} else {
emit('register', form.value)
}
}
</script>
<style scoped>
/* 表单容器 */
.phone-login-form {
display: flex;
flex-direction: column;
gap: 20px;
}
/* 登录方式切换 */
.login-mode-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;
}
.mode-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;
}
.mode-button:hover {
color: #6B46C1;
}
.mode-button.active {
background: white;
color: #6B46C1;
box-shadow: 0 2px 8px rgba(107, 70, 193, 0.15);
transform: translateY(-1px);
}
/* 表单组 */
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
/* 表单标签 */
.form-label {
font-size: 14px;
font-weight: 600;
color: #374151;
margin-left: 4px;
}
/* 输入框容器 */
.input-wrapper {
position: relative;
display: flex;
align-items: center;
transition: all 0.2s ease;
}
.input-wrapper.focused {
transform: translateY(-1px);
}
/* 验证码输入框容器 */
.verification-code-wrapper {
display: flex;
gap: 12px;
}
.verification-code-input {
flex: 1;
}
/* 输入框样式 */
.form-input {
width: 100%;
height: 48px;
padding: 0 16px;
border: 2px solid #E5E7EB;
border-radius: 12px;
background: white;
font-size: 16px;
color: #1F2937;
transition: all 0.2s ease;
outline: none;
}
.form-input:focus {
border-color: #7C3AED;
box-shadow: 0 0 0 3px rgba(167, 139, 250, 0.15);
background: #FAFBFF;
}
.form-input.error {
border-color: #EF4444;
background: #FEF2F2;
}
.form-input:disabled {
background: #F9FAFB;
color: #9CA3AF;
cursor: not-allowed;
}
.form-input::placeholder {
color: #9CA3AF;
font-weight: 400;
}
/* 发送验证码按钮 */
.send-code-button {
min-width: 120px;
height: 48px;
padding: 0 16px;
background: linear-gradient(135deg, #7C3AED, #6B46C1);
border: none;
border-radius: 12px;
color: white;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.send-code-button:hover:not(:disabled) {
background: linear-gradient(135deg, #8B5CF6, #7C3AED);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(107, 70, 193, 0.3);
}
.send-code-button:active:not(:disabled) {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(107, 70, 193, 0.2);
}
.send-code-button:disabled {
background: linear-gradient(135deg, #D1D5DB, #9CA3AF);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* 密码可见性切换按钮 */
.password-toggle {
position: absolute;
right: 12px;
background: none;
border: none;
color: #6B7280;
cursor: pointer;
padding: 4px;
border-radius: 6px;
transition: all 0.2s ease;
}
.password-toggle:hover:not(:disabled) {
color: #6B46C1;
background: rgba(107, 70, 193, 0.1);
}
.password-toggle:disabled {
cursor: not-allowed;
opacity: 0.5;
}
/* 错误图标 */
.input-error-icon {
position: absolute;
right: 12px;
color: #EF4444;
font-size: 16px;
}
/* 验证码输入框的错误图标位置需要调整 */
.verification-code-wrapper .input-error-icon {
right: 140px;
}
/* 错误消息 */
.error-message {
font-size: 12px;
color: #EF4444;
margin-left: 4px;
}
/* 登录提交按钮 */
.login-submit-button {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
height: 48px;
background: linear-gradient(135deg, #7C3AED, #6B46C1);
border: none;
border-radius: 12px;
color: white;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
box-shadow:
0 4px 6px -1px rgba(167, 139, 250, 0.4),
0 2px 4px -1px rgba(167, 139, 250, 0.2);
}
.login-submit-button:hover:not(:disabled) {
background: linear-gradient(135deg, #8B5CF6, #7C3AED);
transform: translateY(-1px);
box-shadow:
0 8px 15px -3px rgba(167, 139, 250, 0.5),
0 4px 6px -2px rgba(167, 139, 250, 0.3);
}
.login-submit-button:active:not(:disabled) {
transform: translateY(0);
box-shadow:
0 4px 6px -1px rgba(107, 70, 193, 0.3),
0 2px 4px -1px rgba(107, 70, 193, 0.2);
}
.login-submit-button:disabled {
background: linear-gradient(135deg, #D1D5DB, #9CA3AF);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* 按钮文字 */
.button-text {
font-weight: 600;
letter-spacing: 0.025em;
}
/* 加载状态指示器 */
.loading-spinner {
width: 18px;
height: 18px;
border: 2px solid transparent;
border-top: 2px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.form-input {
height: 44px;
font-size: 15px;
}
.login-submit-button {
height: 44px;
font-size: 15px;
}
.send-code-button {
height: 44px;
font-size: 13px;
min-width: 110px;
padding: 0 14px;
}
.password-toggle {
right: 10px;
}
.verification-code-wrapper .input-error-icon {
right: 126px;
}
}
@media (max-width: 480px) {
.form-input {
height: 42px;
font-size: 14px;
padding: 0 14px;
}
.login-submit-button {
height: 42px;
font-size: 14px;
}
.send-code-button {
height: 42px;
font-size: 12px;
min-width: 100px;
padding: 0 12px;
}
.verification-code-wrapper {
gap: 8px;
}
.verification-code-wrapper .input-error-icon {
right: 116px;
}
.mode-button {
padding: 8px 12px;
font-size: 13px;
}
}
/* 暗色主题样式 */
html.dark .login-mode-toggle {
background: rgba(139, 92, 246, 0.15);
border-color: rgba(139, 92, 246, 0.25);
}
html.dark .mode-button {
color: #9CA3AF;
}
html.dark .mode-button:hover {
color: #a78bfa;
}
html.dark .mode-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;
}
html.dark .form-input {
background: rgba(31, 41, 55, 0.8);
border-color: rgba(139, 92, 246, 0.2);
color: #f3f4f6;
}
html.dark .form-input:focus {
background: rgba(31, 41, 55, 0.9);
border-color: #7C3AED;
box-shadow: 0 0 0 3px rgba(167, 139, 250, 0.25);
}
html.dark .form-input.error {
background: rgba(127, 29, 29, 0.3);
border-color: rgba(239, 68, 68, 0.4);
color: #fca5a5;
}
html.dark .form-input:disabled {
background: rgba(55, 65, 81, 0.6);
color: #6b7280;
}
html.dark .form-input::placeholder {
color: #6b7280;
}
html.dark .send-code-button {
background: linear-gradient(135deg, #7C3AED, #6B46C1);
}
html.dark .send-code-button:hover:not(:disabled) {
background: linear-gradient(135deg, #8B5CF6, #7C3AED);
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4);
}
html.dark .send-code-button:active:not(:disabled) {
box-shadow: 0 2px 6px rgba(139, 92, 246, 0.3);
}
html.dark .send-code-button:disabled {
background: linear-gradient(135deg, #374151, #1f2937);
}
html.dark .password-toggle {
color: #9CA3AF;
}
html.dark .password-toggle:hover:not(:disabled) {
color: #a78bfa;
background: rgba(139, 92, 246, 0.2);
}
html.dark .input-error-icon {
color: #ef4444;
}
html.dark .error-message {
color: #fca5a5;
}
</style>

View File

@ -24,6 +24,34 @@
</div>
</div>
</div>
<!-- 模型选择 -->
<div class="form-section" v-if="false">
<div class="expression-info">
<span class="expression-description">
{{ $t('iPandCardLeft.modelSelection') }}
</span>
</div>
<el-select
v-model="selectedModelId"
:placeholder="$t('iPandCardLeft.modelSelectPlaceholder')"
@change="handleModelSelect"
class="model-select"
>
<el-option
v-for="model in availableModels"
:key="model.id"
:label="model.name"
:value="model.id"
>
<div class="model-option">
<span class="model-name">{{ model.name }}</span>
<span class="model-description">{{ model.description }}</span>
</div>
</el-option>
</el-select>
</div>
<!-- 表情选择 -->
<div class="form-section" v-if="false">
<!-- <label class="section-label">表情选择</label> -->
@ -310,6 +338,48 @@ const handleIpTypeSelect = (type) => {
// //
// }
};
//
const selectedModelId = ref(null);
const availableModels = ref([
{
id: 'nano_banana',
name: 'Nano Banana',
description: '快速生成',
model: 'gemini-2.5-flash-image'
},
{
id: 'nano_banana_pro',
name: 'Nano Banana Pro',
description: '高质量生成',
model: 'gemini-3-pro-image-preview'
},
{
id: 'doubao',
name: 'Doubao',
description: '豆包模型',
model: 'doubao'
},
{
id: 'qwimg',
name: 'QwImg',
description: '通义千问',
model: 'ali'
}
]);
//
const handleModelSelect = (modelId) => {
selectedModelId.value = modelId;
const selectedModel = availableModels.value.find(m => m.id === modelId);
//
try {
localStorage.setItem('selectedModelId', modelId);
localStorage.setItem('selectedModel', JSON.stringify(selectedModel));
} catch (e) {
//
}
};
//
const selectedModule = ref(null); //
const selectedSketch = ref(null); //
@ -323,6 +393,19 @@ const selectedSkinColor = ref(null); // 选中的肤色
const expressions = ref([]);
const init = () => {
//
try {
const savedModelId = localStorage.getItem('selectedModelId');
if (savedModelId) {
selectedModelId.value = savedModelId;
} else {
//
selectedModelId.value = availableModels.value[0].id;
}
} catch (e) {
// 使
selectedModelId.value = availableModels.value[0].id;
}
}
onMounted(() => {
init()
@ -1008,6 +1091,27 @@ onMounted(() => {
font-weight: 500;
}
/* 模型选择样式 */
.model-select {
width: 100%;
}
.model-option {
display: flex;
flex-direction: column;
gap: 4px;
}
.model-option .model-name {
font-weight: 600;
color: var(--el-text-color-primary);
}
.model-option .model-description {
font-size: 12px;
color: var(--el-text-color-secondary);
}
/* 适应亮色主题 */
:root {
--border-color: rgba(0, 0, 0, 0.1);
@ -1042,6 +1146,11 @@ onMounted(() => {
border-color: #A78BFA;
}
/* 模型选择在亮色主题下的样式 */
.model-select {
width: 100%;
}
.logo-container {
display: flex;
align-items: center;
@ -1099,7 +1208,7 @@ onMounted(() => {
/* 选择框样式 */
.model-select {
width: 100%;
padding: 10px 12px;
/* padding: 10px 12px; */
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;

View File

@ -44,7 +44,7 @@
<span class="points-icon">🪄</span>
<span class="points-text">{{ remainingPoints }}</span>
</div> -->
<!-- 用户信息模块已取消 -->
<!-- 用户信息模块已取消 -->
</div>
<!-- 折叠状态下的用户头像 -->

View File

@ -603,6 +603,27 @@ export default {
invite_code_empty_error: '请输入邀请码',
join_waitlist: '加入候补队列',
join_waitlist_success: '已成功加入候补队列,我们将尽快与您联系',
// 手机号登录相关
phone_login_title: '手机号登录',
phone_login_subtitle: '使用手机号和验证码登录',
phone_label: '手机号',
phone_placeholder: '请输入您的手机号',
phone_empty_error: '请输入手机号',
phone_invalid_error: '请输入有效的手机号',
code_label: '验证码',
code_placeholder: '请输入验证码',
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: '使用邮箱登录',
},
payment: {
methods: '支付方式',
@ -1173,6 +1194,8 @@ export default {
ipType: 'IP类型',
character: '人物',
animal: '动物',
modelSelection: '模型选择',
modelSelectPlaceholder: '请选择模型',
characterImport: '角色导入',
expression: {
title: '表情选择',
@ -1261,6 +1284,11 @@ export default {
description: '抱歉,您访问的页面不存在或已被移除。',
goHome: '返回首页',
goBack: '返回上一页'
},
waitlist: {
title: '已加入候补队列',
description: '您的申请已提交,正在等待审核。我们将尽快处理您的请求。',
goHome: '返回首页'
}
},
en: {
@ -1966,6 +1994,27 @@ export default {
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',
phone_register_button: 'Register',
phone_logging: 'Logging in...',
phone_registering: 'Registering...',
phone_login_link: 'Login with Phone',
email_login_link: 'Login with Email',
},
payment: {
methods: 'Payment Methods',
@ -2426,6 +2475,8 @@ export default {
ipType: 'IP Type',
character: 'Character',
animal: 'Animal',
modelSelection: 'Model Selection',
modelSelectPlaceholder: 'Please select a model',
characterImport: 'Character Import',
expression: {
title: 'Expression Selection',
@ -2514,6 +2565,11 @@ export default {
description: 'Sorry, the page you are looking for does not exist or has been removed.',
goHome: 'Go Home',
goBack: 'Go Back'
},
waitlist: {
title: 'Joined Waitlist',
description: 'Your application has been submitted and is waiting for review. We will process your request as soon as possible.',
goHome: 'Go Home'
}
}
},

View File

@ -20,6 +20,7 @@ const home = () => import('../views/home/index.vue')
const PointsRecharge = () => import('../views/PointsRecharge.vue')
const UserCenter = () => import('../views/user/index.vue')
const NotFound = () => import('../views/NotFound.vue')
const Waitlist = () => import('../views/Waitlist.vue')
NProgress.configure({
showSpinner: false,
})// 开启轻量模式(顶部细线)
@ -30,17 +31,17 @@ const routes = [
name: 'home',
component: home,
meta: { fullScreen: false }
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: home, // 显示404页面组件
meta: { requiresAuth: false, keepAlive: false, fullScreen: true }
},{
path: '/login',
name: 'login',
component: Login,
meta: { requiresGuest: true }
meta: { requiresGuest: true }
},
{
path: '/login/phone',
name: 'phone-login',
component: () => import('@/views/Login/PhoneLogin.vue'),
meta: { requiresGuest: true, fullScreen: true }
},
{
path: '/czhome',
@ -51,8 +52,8 @@ const routes = [
{
path: '/register',
name: 'register',
component: Register,
meta: { requiresGuest: true, fullScreen: true }
component: Register,
meta: { requiresGuest: true, fullScreen: true }
},
{
path: '/forgot-password',
@ -60,15 +61,15 @@ const routes = [
component: ForgotPassword,
meta: { requiresGuest: true, fullScreen: true }
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: NotFound, // 显示404页面组件
meta: { requiresAuth: false, keepAlive: false, fullScreen: true }
}
]
//免费会员/达人会员动态路由
export const freeRoutes = [
{
path: '/czhome',
name: 'czhome',
component: ModernHome,
meta: { requiresAuth: false, keepAlive: false }
},
{
path: '/ui-test',
name: 'ui-test',
@ -172,7 +173,8 @@ router.beforeEach(async (to, from, next) => {
// window.localStorage.setItem('token','123')
// return next()
// }
if (to.meta.requiresAuth) {
const newto = freeRoutes.find(route => route.path == to.path)
if (newto?.meta?.requiresAuth) {
const token = localStorage.getItem('token')
// 如果没有 token跳转到登录页
if (!token) {
@ -182,8 +184,8 @@ router.beforeEach(async (to, from, next) => {
}
// 检查是否需要添加动态路由
const authStore = useAuthStore();
const user_role = authStore.user?.user_role || '0'
if(user_role != '0' && router.getRoutes().length <= routes.length) {
const user_role = authStore.user?.user_role;
if(user_role != '0' && router.getRoutes().length == routes.length) {
// 添加动态路由
addDynamicRoutes();
if(isDynamicRoute(to.path)) {
@ -193,18 +195,38 @@ router.beforeEach(async (to, from, next) => {
}, 20);
return
}
}else if(user_role == '0'){
// 恢复默认路由
removeDynamicRoutes()
router.addRoute(
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: Waitlist, // 显示404页面组件
meta: { requiresAuth: false, keepAlive: false, fullScreen: true }
}
)
}
next()
})
// 添加动态路由的函数
function addDynamicRoutes() {
console.log('添加动态路由前路由数量:', router.getRoutes().length);
freeRoutes.forEach(route => {
router.addRoute(route)
})
}
//恢复默认路由
function removeDynamicRoutes() {
if(router.getRoutes().length == routes.length){
return
}
router.getRoutes().forEach(route => {
if (route.name && !routes.some(r => r.name === route.name)) {
router.removeRoute(route.name)
}
})
}
// 检查是否是动态路由
function isDynamicRoute(path) {
return freeRoutes.some(route => {
@ -216,11 +238,8 @@ function isDynamicRoute(path) {
}
window.Redirectlogin = () => {
localStorage.removeItem('token')
router.getRoutes().forEach(route => {
if (route.name && !routes.some(r => r.name === route.name)) {
router.removeRoute(route.name)
}
})
// 恢复默认路由
removeDynamicRoutes()
router.push('/login')
}
router.afterEach(() => {

View File

@ -6,7 +6,7 @@ export const useAuthStore = defineStore('auth', () => {
// 状态定义
const user = ref({})
const token = ref('')
// 登录方法
// 邮箱登录方法
const login = async (data,callback=null) => {
try {
const res = await requestUtils.common(clientApi.default.LOGIN, data)
@ -22,6 +22,38 @@ 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
@ -52,6 +84,8 @@ export const useAuthStore = defineStore('auth', () => {
user,
token,
login,
phoneLogin,
phoneRegister,
logout,
loginSuccess,
}

View File

@ -48,6 +48,15 @@
<!-- 忘记密码和注册链接 -->
<div class="auth-links">
<div class="auth-links-row">
<button
type="button"
class="auth-link phone-login-link"
@click="goToPhoneLogin"
>
<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"
@ -77,7 +86,7 @@ import { onMounted, reactive, ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth'
import { WarningFilled, InfoFilled, QuestionFilled, UserFilled } from '@element-plus/icons-vue'
import { WarningFilled, InfoFilled, QuestionFilled, UserFilled, Phone } from '@element-plus/icons-vue'
//
import GoogleOAuthButton from '@/components/auth/GoogleOAuthButton.vue'
import LoginForm from '@/components/auth/LoginForm.vue'
@ -125,6 +134,11 @@ const goToRegister = () => {
router.push('/register')
}
//
const goToPhoneLogin = () => {
router.push('/login/phone')
}
//
onMounted(() => {
})
@ -953,7 +967,7 @@ html.dark .error-icon {
.auth-links-row {
display: flex;
justify-content: space-between;
justify-content: center;
align-items: center;
gap: 16px;
}

View File

@ -0,0 +1,668 @@
<template>
<div class="login-page">
<!-- 全屏背景 -->
<div class="login-background"></div>
<!-- 右上角控制组件 -->
<div class="top-right-controls">
<div class="controls-container">
<ThemeToggle
position="top-right"
:tooltip-text="t('login.theme_toggle_tooltip')"
/>
<LanguageToggle
position="top-right"
:tooltip-text="t('login.language_toggle_tooltip')"
/>
</div>
</div>
<!-- 主登录卡片 -->
<div class="login-container">
<div class="login-card">
<!-- 卡片标题 -->
<div class="login-title">
<h2>{{ t('login.phone_login_title') }}</h2>
<p class="login-subtitle">{{ t('login.phone_login_subtitle') }}</p>
</div>
<!-- 手机号登录表单 -->
<div class="phone-login-section">
<PhoneLoginForm
@login="handleLogin"
@register="handleRegister"
/>
</div>
<!-- 错误提示 -->
<div v-if="authStore.error" class="error-message">
<el-icon class="error-icon"><WarningFilled /></el-icon>
<span>{{ authStore.error }}</span>
</div>
<!-- 其他登录方式和链接 -->
<div class="auth-links">
<div class="auth-links-row">
<button
type="button"
class="auth-link email-login-link"
@click="goToEmailLogin"
>
<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>
</div>
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth'
import { WarningFilled, Message, QuestionFilled } from '@element-plus/icons-vue'
//
import PhoneLoginForm from '@/components/auth/PhoneLoginForm.vue'
import ThemeToggle from '@/components/ui/ThemeToggle.vue'
import LanguageToggle from '@/components/ui/LanguageToggle.vue'
import LOGIN from './login'
const router = useRouter()
const authStore = useAuthStore()
const { t } = useI18n()
const plugin = reactive(new LOGIN());
//
const handleLogin = async (data) => {
plugin.phoneLogin(data)
}
//
const handleRegister = async (data) => {
plugin.phoneRegister(data)
}
//
const goToEmailLogin = () => {
router.push('/login')
}
//
const goToForgotPassword = () => {
router.push('/forgot-password')
}
//
onMounted(() => {
})
</script>
<style scoped>
/* 登录页面基础样式 */
.login-page {
position: relative;
min-height: 100%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
/* 全屏背景渐变 */
.login-background {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg,
#6B46C1 0%,
#8B5CF6 25%,
#A78BFA 50%,
#DDD6FE 75%,
#F3F4F6 100%);
background-size: 400% 400%;
animation: gradientShift 8s ease infinite;
}
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* 主登录容器 */
.login-container {
position: relative;
z-index: 10;
width: 100%;
max-width: 440px;
padding: 20px;
}
/* 登录卡片 */
.login-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 24px;
padding: 48px 40px;
box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04),
0 0 0 1px rgba(139, 92, 246, 0.1);
border: 1px solid rgba(139, 92, 246, 0.2);
transform: translateY(0);
transition: all 0.3s ease;
animation: slideInUp 0.6s ease-out;
position: relative;
overflow: hidden;
}
/* 卡片微妙的背景动画 */
.login-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg,
rgba(139, 92, 246, 0.03) 0%,
transparent 50%,
rgba(167, 139, 250, 0.03) 100%);
pointer-events: none;
z-index: -1;
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.login-card:hover {
transform: translateY(-2px);
box-shadow:
0 25px 30px -5px rgba(0, 0, 0, 0.15),
0 15px 15px -5px rgba(0, 0, 0, 0.1),
0 0 0 1px rgba(139, 92, 246, 0.15);
}
/* 登录标题 */
.login-title {
text-align: center;
margin-bottom: 32px;
}
.login-title h2 {
font-size: 28px;
font-weight: 700;
color: #374151;
margin: 0 0 8px 0;
}
.login-subtitle {
font-size: 14px;
color: #6B7280;
margin: 0;
}
/* 手机号登录区域 */
.phone-login-section {
margin-bottom: 32px;
}
/* 忘记密码和注册链接样式 */
.auth-links {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid rgba(139, 92, 246, 0.1);
}
.auth-links-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
}
.auth-link {
display: flex;
align-items: center;
gap: 6px;
background: none;
border: none;
color: #6B46C1;
font-size: 14px;
font-weight: 500;
cursor: pointer;
padding: 8px 12px;
border-radius: 8px;
transition: all 0.2s ease;
text-decoration: none;
white-space: nowrap;
}
.auth-link:hover {
background: rgba(107, 70, 193, 0.1);
color: #5B21B6;
transform: translateY(-1px);
}
.auth-link:active {
transform: translateY(0);
}
.link-icon {
font-size: 14px;
opacity: 0.8;
}
.email-login-link:hover .link-icon {
color: #8B5CF6;
}
.forgot-password-link:hover .link-icon {
color: #8B5CF6;
}
/* 错误消息 */
.error-message {
display: flex;
align-items: center;
gap: 8px;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
border-radius: 12px;
padding: 12px 16px;
color: #EF4444;
font-size: 14px;
margin-top: 20px;
animation: shake 0.5s ease-in-out;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
.error-icon {
flex-shrink: 0;
}
/* 右上角控制组件样式 */
.top-right-controls {
position: fixed;
top: 24px;
right: 24px;
z-index: 1000;
display: flex;
justify-content: flex-end;
align-items: center;
box-sizing: border-box;
}
/* 容器包装控制组件 */
.controls-container {
display: flex;
gap: 12px;
margin-left: auto;
}
/* 确保组件对齐 */
.top-right-controls .theme-toggle,
.top-right-controls .language-toggle {
display: flex;
align-items: center;
}
/* 统一组件高度,确保对齐 */
.top-right-controls .theme-toggle .theme-toggle-btn,
.top-right-controls .language-toggle .language-toggle__button {
height: 48px;
display: flex;
align-items: center;
justify-content: center;
min-width: 48px;
}
/* ==================== 暗色主题样式 ==================== */
/* 暗色主题背景 */
html.dark .login-background {
background: linear-gradient(135deg,
#1a1a2e 0%,
#16213e 25%,
#0f3460 50%,
#533483 75%,
#2d1b69 100%);
/* 优化性能,避免滚动条 */
background-attachment: fixed;
}
/* 暗色主题登录卡片 */
html.dark .login-card {
background: rgba(17, 24, 39, 0.95);
border: 1px solid rgba(139, 92, 246, 0.2);
backdrop-filter: blur(20px);
}
html.dark .login-card:hover {
border-color: rgba(139, 92, 246, 0.4);
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(139, 92, 246, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
/* 暗色主题登录标题 */
html.dark .login-title h2 {
color: #f3f4f6;
}
html.dark .login-subtitle {
color: #d1d5db;
}
/* 暗色主题下的链接样式 */
html.dark .auth-links {
border-top-color: rgba(139, 92, 246, 0.2);
}
html.dark .auth-link {
color: #a78bfa;
}
html.dark .auth-link:hover {
background: rgba(139, 92, 246, 0.2);
color: #8b5cf6;
}
html.dark .link-icon {
color: #9ca3af;
}
html.dark .email-login-link:hover .link-icon,
html.dark .forgot-password-link:hover .link-icon {
color: #8b5cf6;
}
/* 暗色主题错误提示 */
html.dark .error-message {
background: rgba(127, 29, 29, 0.8);
border: 1px solid rgba(239, 68, 68, 0.3);
color: #fca5a5;
}
html.dark .error-icon {
color: #ef4444;
}
/* 响应式设计 */
/* 大屏幕桌面端 (1200px+) */
@media (min-width: 1200px) {
.login-container {
max-width: 480px;
}
.login-card {
padding: 56px 48px;
border-radius: 28px;
box-shadow:
0 25px 30px -5px rgba(0, 0, 0, 0.12),
0 15px 15px -5px rgba(0, 0, 0, 0.08),
0 0 0 1px rgba(139, 92, 246, 0.12);
}
.login-card:hover {
transform: translateY(-3px);
box-shadow:
0 30px 35px -5px rgba(0, 0, 0, 0.15),
0 20px 20px -5px rgba(0, 0, 0, 0.1),
0 0 0 1px rgba(139, 92, 246, 0.15);
}
.top-right-controls {
padding: 0 48px;
}
.top-right-controls .theme-toggle .theme-toggle-btn,
.top-right-controls .language-toggle .language-toggle__button {
height: 52px;
min-width: 52px;
}
}
/* 平板端横屏和大平板竖屏 (1024px) */
@media (max-width: 1199px) and (min-width: 1024px) {
.login-container {
max-width: 420px;
padding: 24px;
}
.login-card {
padding: 44px 36px;
border-radius: 24px;
backdrop-filter: blur(25px);
}
.login-card:hover {
transform: translateY(-2px);
}
.phone-login-section {
margin-bottom: 28px;
}
.auth-links {
padding-top: 24px;
margin-top: 24px;
}
.top-right-controls {
top: 20px;
right: 20px;
}
.top-right-controls .theme-toggle .theme-toggle-btn,
.top-right-controls .language-toggle .language-toggle__button {
height: 50px;
min-width: 50px;
}
}
/* 平板端 (768px - 1023px) */
@media (max-width: 1023px) and (min-width: 768px) {
.login-container {
max-width: 92%;
padding: 20px;
}
.login-card {
padding: 36px 32px;
}
.phone-login-section {
margin-bottom: 20px;
}
.auth-links {
padding-top: 20px;
margin-top: 20px;
}
.top-right-controls {
top: 16px;
right: 16px;
}
.controls-container {
gap: 8px;
}
.top-right-controls .theme-toggle .theme-toggle-btn,
.top-right-controls .language-toggle .language-toggle__button {
height: 44px;
min-width: 44px;
transform: scale(0.95);
}
}
/* 移动端 (481px - 767px) */
@media (max-width: 767px) and (min-width: 481px) {
.login-container {
max-width: 94%;
padding: 16px;
}
.login-card {
padding: 32px 28px;
}
.top-right-controls {
top: 12px;
right: 12px;
}
.controls-container {
gap: 6px;
}
.top-right-controls .theme-toggle .theme-toggle-btn,
.top-right-controls .language-toggle .language-toggle__button {
height: 40px;
min-width: 40px;
transform: scale(0.9);
}
}
/* 小屏幕手机端 (320px - 480px) */
@media (max-width: 480px) {
.login-container {
max-width: 96%;
padding: 12px;
}
.login-card {
padding: 20px 16px;
border-radius: 16px;
backdrop-filter: blur(15px);
background: rgba(255, 255, 255, 0.98);
}
.login-card:hover {
transform: none;
}
.login-title h2 {
font-size: 24px;
}
.login-subtitle {
font-size: 13px;
}
.phone-login-section {
margin-bottom: 20px;
}
.auth-links {
margin-top: 20px;
padding-top: 14px;
}
.auth-links-row {
flex-direction: column;
gap: 12px;
align-items: stretch;
}
.auth-link {
justify-content: center;
padding: 10px 16px;
font-size: 13px;
}
.top-right-controls {
top: 8px;
right: 8px;
}
.controls-container {
gap: 4px;
}
.top-right-controls .theme-toggle .theme-toggle-btn,
.top-right-controls .language-toggle .language-toggle__button {
height: 36px;
min-width: 36px;
transform: scale(0.85);
}
/* 在非常小的屏幕上,可以考虑隐藏其中一个组件 */
@media (max-width: 360px) {
.login-container {
max-width: 98%;
padding: 8px;
}
.login-card {
padding: 16px 12px;
border-radius: 12px;
}
.login-title h2 {
font-size: 22px;
}
.auth-links {
margin-top: 16px;
padding-top: 12px;
}
.auth-link {
padding: 8px 12px;
font-size: 12px;
}
.link-icon {
font-size: 13px;
}
.controls-container {
gap: 2px;
}
.top-right-controls .theme-toggle .theme-toggle-btn,
.top-right-controls .language-toggle .language-toggle__button {
height: 34px;
min-width: 34px;
transform: scale(0.8);
}
}
}
/* 小屏幕手机端暗色主题优化 */
@media (max-width: 480px) {
html.dark .login-card {
background: rgba(17, 24, 39, 0.98);
}
}
</style>

View File

@ -17,6 +17,22 @@ export default class Login {
// 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达人
// userData.user_role=1
@ -38,6 +54,16 @@ export default class Login {
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();
})
}
//确认注册功能
confirmRegister(data,callback){
let params = {

View File

@ -0,0 +1,197 @@
<template>
<div class="waitlist-container">
<div class="waitlist-content">
<div class="waitlist-icon">
<svg width="120" height="120" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="#6B46C1" stroke-width="2" fill="rgba(107, 70, 193, 0.1)"/>
<path d="M9 12L11 14L15 10" stroke="#6B46C1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="waitlist-title">{{ t('waitlist.title') }}</div>
<div class="waitlist-description">{{ t('waitlist.description') }}</div>
<div class="action-buttons">
<button class="primary-button" @click="goHome">
{{ t('waitlist.goHome') }}
</button>
</div>
</div>
<div class="decoration">
<div class="circle circle-1"></div>
<div class="circle circle-2"></div>
<div class="circle circle-3"></div>
</div>
</div>
</template>
<script>
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
export default {
name: 'Waitlist',
setup() {
const { t } = useI18n()
const router = useRouter()
const goHome = () => {
router.push('/')
}
return {
t,
goHome
}
}
}
</script>
<style scoped>
.waitlist-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #f3f4f6 0%, #e9d5ff 100%);
position: relative;
overflow: hidden;
padding: 20px;
}
.waitlist-content {
text-align: center;
z-index: 10;
max-width: 600px;
}
.waitlist-icon {
margin-bottom: 30px;
display: flex;
justify-content: center;
align-items: center;
}
.waitlist-title {
font-size: 2.5rem;
font-weight: 600;
color: #1F2937;
margin-bottom: 16px;
}
.waitlist-description {
font-size: 1.2rem;
color: #6B7280;
margin-bottom: 40px;
line-height: 1.6;
}
.action-buttons {
display: flex;
gap: 16px;
justify-content: center;
flex-wrap: wrap;
}
.primary-button {
padding: 12px 24px;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
background-color: #6B46C1;
color: white;
}
.primary-button:hover {
background-color: #5B3FA1;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(107, 70, 193, 0.3);
}
.decoration {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1;
pointer-events: none;
}
.circle {
position: absolute;
border-radius: 50%;
opacity: 0.1;
}
.circle-1 {
width: 300px;
height: 300px;
background-color: #6B46C1;
top: -100px;
right: -100px;
}
.circle-2 {
width: 200px;
height: 200px;
background-color: #A78BFA;
bottom: -50px;
left: -50px;
}
.circle-3 {
width: 150px;
height: 150px;
background-color: #8B5CF6;
top: 50%;
left: 10%;
}
@media (max-width: 768px) {
.waitlist-title {
font-size: 2rem;
}
.waitlist-description {
font-size: 1rem;
}
.action-buttons {
flex-direction: column;
align-items: center;
}
.primary-button {
width: 200px;
}
}
@media (max-width: 480px) {
.waitlist-icon svg {
width: 80px;
height: 80px;
}
.waitlist-title {
font-size: 1.5rem;
}
.circle-1 {
width: 200px;
height: 200px;
}
.circle-2 {
width: 150px;
height: 150px;
}
.circle-3 {
width: 100px;
height: 100px;
}
}
</style>

41
package-lock.json generated
View File

@ -30,7 +30,6 @@
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@google/genai": "^1.27.0",
"@stripe/stripe-js": "^4.8.0",
"@twind/core": "^1.1.3",
"@twind/preset-autoprefix": "^1.0.7",
"@twind/preset-tailwind": "^1.1.4",
@ -41,6 +40,7 @@
"country-state-city": "^3.2.1",
"dayjs": "^1.11.13",
"element-plus": "^2.11.7",
"install": "^0.13.0",
"jose": "^6.1.1",
"motion-v": "^1.7.4",
"normalize.css": "^8.0.1",
@ -146,7 +146,8 @@
"three": "^0.180.0",
"vue": "^3.5.24",
"vue-i18n": "^9.14.2",
"vue-router": "^4.4.5"
"vue-router": "^4.4.5",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
@ -1782,15 +1783,6 @@
"win32"
]
},
"node_modules/@stripe/stripe-js": {
"version": "4.10.0",
"resolved": "https://registry.npmmirror.com/@stripe/stripe-js/-/stripe-js-4.10.0.tgz",
"integrity": "sha512-KrMOL+sH69htCIXCaZ4JluJ35bchuCCznyPyrbN8JXSGQfwBI1SuIEMZNwvy8L8ykj29t6sa5BAAiL7fNoLZ8A==",
"license": "MIT",
"engines": {
"node": ">=12.16"
}
},
"node_modules/@tailwindcss/node": {
"version": "4.1.17",
"resolved": "https://registry.npmmirror.com/@tailwindcss/node/-/node-4.1.17.tgz",
@ -4347,6 +4339,15 @@
"dev": true,
"license": "ISC"
},
"node_modules/install": {
"version": "0.13.0",
"resolved": "https://registry.npmmirror.com/install/-/install-0.13.0.tgz",
"integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
@ -5729,6 +5730,12 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/sortablejs": {
"version": "1.14.0",
"resolved": "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.14.0.tgz",
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==",
"license": "MIT"
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
@ -6756,6 +6763,18 @@
}
}
},
"node_modules/vuedraggable": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/vuedraggable/-/vuedraggable-4.1.0.tgz",
"integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
"license": "MIT",
"dependencies": {
"sortablejs": "1.14.0"
},
"peerDependencies": {
"vue": "^3.0.1"
}
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",

View File

@ -1,17 +1,25 @@
const permission = {
updateRole: {url: '/api-base/admin/role', method: 'PUT'},//修改角色
addRole: {url: '/api-base/admin/role', method: 'POST'},//新增角色
assignRoleToUser: {url: '/api-base/admin/role/assign/{userId}', method: 'POST'},//为用户分配角色
getRoleDetail: {url: '/api-base/admin/role/{roleId}', method: 'GET'},//查询角色详情
getRolesByUserId: {url: '/api-base/admin/role/user/{userId}', method: 'GET'},//根据用户ID查询角色列表
getRoleList: {url: '/api-base/admin/role/list', method: 'GET'},//查询角色列表
deleteRole: {url: '/api-base/admin/role/{roleIds}', method: 'DELETE'},//删除角色
updatePermission: {url: '/api-base/admin/permission', method: 'PUT'},//修改权限
addPermission: {url: '/api-base/admin/permission', method: 'POST'},//新增权限
assignPermissionToRole: {url: '/api-base/admin/permission/assign/{roleId}', method: 'POST'},//为角色分配权限
getPermissionDetail: {url: '/api-base/admin/permission/{permissionId}', method: 'GET'},//查询权限详情
deletePermission: {url: '/api-base/admin/permission/{permissionId}', method: 'DELETE'},//删除权限
getPermissionList: {url: '/api-base/admin/permission/list', method: 'GET'},//查询权限列表
getPermissionCodesByUserId: {url: '/api-base/admin/permission/codes/user/{userId}', method: 'GET'},//根据用户ID查询权限代码集合
updateRole: {url: '/api-base/admin/role', method: 'PUT', isLoading: true},//修改角色
addRole: {url: '/api-base/admin/role', method: 'POST', isLoading: true},//新增角色
assignRoleToUser: {url: '/api-base/admin/role/assign/{userId}', method: 'POST', isLoading: true},//为用户分配角色
getRoleDetail: {url: '/api-base/admin/role/{roleId}', method: 'GET', isLoading: true},//查询角色详情
getRolesByUserId: {url: '/api-base/admin/role/user/{userId}', method: 'GET', isLoading: true},//根据用户ID查询角色列表
getRoleList: {url: '/api-base/admin/role/list', method: 'GET', isLoading: true},//查询角色列表
deleteRole: {url: '/api-base/admin/role/{roleIds}', method: 'DELETE', isLoading: true},//删除角色
updatePermission: {url: '/api-base/admin/permission', method: 'PUT', isLoading: true},//修改权限
addPermission: {url: '/api-base/admin/permission', method: 'POST', isLoading: true},//新增权限
assignPermissionToRole: {url: '/api-base/admin/permission/assign/{roleId}', method: 'POST', isLoading: true},//为角色分配权限
getPermissionDetail: {url: '/api-base/admin/permission/{permissionId}', method: 'GET', isLoading: true},//查询权限详情
deletePermission: {url: '/api-base/admin/permission/{permissionId}', method: 'DELETE', isLoading: true},//删除权限
getPermissionList: {url: '/api-base/admin/permission/list', method: 'GET', isLoading: true},//查询权限列表
getPermissionCodesByUserId: {url: '/api-base/admin/permission/codes/user/{userId}', method: 'GET', isLoading: true},//根据用户ID查询权限代码集合
updateAdminUser: {url: '/api-base/admin/admin-user/update', method: 'POST', isLoading: true},//更新管理员用户
toggleAdminUserStatus: {url: '/api-base/admin/admin-user/toggle-status/{userId}', method: 'POST', isLoading: true},//启用/禁用用户
resetAdminUserPassword: {url: '/api-base/admin/admin-user/reset-password/{userId}', method: 'POST', isLoading: true},//重置用户密码
createAdminUser: {url: '/api-base/admin/admin-user/create', method: 'POST', isLoading: true},//创建管理员用户
getAdminUserDetail: {url: '/api-base/admin/admin-user/{userId}', method: 'GET', isLoading: true},//根据用户ID查询管理员用户详情
deleteAdminUser: {url: '/api-base/admin/admin-user/{userId}', method: 'DELETE', isLoading: true},//删除管理员用户
getAdminUserList: {url: '/api-base/admin/admin-user/list', method: 'GET', isLoading: true},//分页查询管理员用户列表
batchDeleteAdminUser: {url: '/api-base/admin/admin-user/batch', method: 'DELETE', isLoading: true},//批量删除管理员用户
}
export default permission;

View File

@ -185,7 +185,7 @@ export class GiminiServer extends FileServer {
// "aspectRatio": "9:16"
// },
"aspect_ratio": "16:9",
"model": "doubao",//models/gemini-3-pro-image-preview/"gemini-2.5-flash-image"/"doubao",
"model": "gemini-2.5-flash-image",//models/gemini-3-pro-image-preview/"gemini-2.5-flash-image"/"doubao"/"ali",
"location": "global",
"vertexai": true,
...config,
@ -197,6 +197,11 @@ export class GiminiServer extends FileServer {
if(params.model=='doubao'){
params.aspect_ratio = '768x1344';
}
console.log(window.localStorage.getItem('selectedModel')||{});
const selectedModel = JSON.parse(window.localStorage.getItem('selectedModel')||'{}');
if(selectedModel.model){
params.model = selectedModel.model;
}
const requestUrl = this.RULE=='admin'?adminApi.default.GENERATE_IMAGE_ADMIN:clientApi.default.GENERATE_IMAGE;
const response = await requestUtils.common(requestUrl, params);
// const response = {"code":0,"message":"","success":true,"data":{"id":2177,"message":"任务已提交,正在处理"}}

View File

@ -1,4 +1,4 @@
import { loadStripe } from '@stripe/stripe-js';
// import { loadStripe } from '@stripe/stripe-js';
import { request as requestUtils } from '../utils/request.js'
import * as clientApi from '../api/frontend/index.js'
//获取Stripe公钥
@ -19,20 +19,45 @@ export function getStripePublishableKey() {
export class PayServer {
static stripe = null// Stripe实例
static isInitializing = false // 防止重复初始化
static initPromise = null // 初始化Promise
constructor() {
}
//初始化
async init() {
return new Promise(async (resolve) => {
if (!PayServer.stripe) {
await loadStripe(getStripePublishableKey()).then((stripe) => {
PayServer.stripe = stripe;
resolve(PayServer.stripe);
});
} else {
return
// 如果已经初始化,直接返回
if (PayServer.stripe) {
return PayServer.stripe;
}
// 如果正在初始化,等待初始化完成
if (PayServer.isInitializing && PayServer.initPromise) {
return PayServer.initPromise;
}
// 开始初始化
PayServer.isInitializing = true;
PayServer.initPromise = new Promise(async (resolve, reject) => {
try {
const stripeKey = getStripePublishableKey();
if (!stripeKey) {
throw new Error('Stripe publishable key not found');
}
const stripe = await loadStripe(stripeKey);
PayServer.stripe = stripe;
PayServer.isInitializing = false;
resolve(PayServer.stripe);
} catch (error) {
PayServer.isInitializing = false;
reject(error);
}
})
});
return PayServer.initPromise;
}
/**
* 创建支付意图
@ -123,7 +148,7 @@ export class PayServer {
async createPayorOrder(orderInfo) {
// let payReducerUrl = 'https://www.deotaland.ai/#/order-management'
let payReducerUrl = `${window.location.origin}/#/order-management`
await this.init();
// await this.init();
return new Promise(async (resolve, reject) => {
let pamras = {
"methods": [

View File

@ -59,6 +59,9 @@ importers:
vue-router:
specifier: ^4.4.5
version: 4.6.3(vue@3.5.24)
vuedraggable:
specifier: ^4.1.0
version: 4.1.0(vue@3.5.24)
devDependencies:
'@vitejs/plugin-vue':
specifier: ^6.0.1
@ -77,7 +80,7 @@ importers:
version: 5.44.1
unplugin-auto-import:
specifier: ^20.2.0
version: 20.2.0
version: 20.2.0(@vueuse/core@14.1.0)
unplugin-vue-components:
specifier: ^30.0.0
version: 30.0.0(vue@3.5.24)
@ -93,9 +96,6 @@ importers:
'@google/genai':
specifier: ^1.27.0
version: 1.27.0
'@stripe/stripe-js':
specifier: ^4.8.0
version: 4.8.0
'@twind/core':
specifier: ^1.1.3
version: 1.1.3
@ -114,9 +114,9 @@ importers:
'@vuelidate/validators':
specifier: ^2.0.4
version: 2.0.4(vue@3.5.24)
axios:
specifier: ^1.13.2
version: 1.13.2
'@vueuse/core':
specifier: ^14.1.0
version: 14.1.0(vue@3.5.24)
country-state-city:
specifier: ^3.2.1
version: 3.2.1
@ -126,9 +126,15 @@ importers:
element-plus:
specifier: ^2.11.7
version: 2.11.7(vue@3.5.24)
install:
specifier: ^0.13.0
version: 0.13.0
jose:
specifier: ^6.1.1
version: 6.1.1
motion-v:
specifier: ^1.7.4
version: 1.7.4(@vueuse/core@14.1.0)(vue@3.5.24)
normalize.css:
specifier: ^8.0.1
version: 8.0.1
@ -169,6 +175,9 @@ importers:
'@iconify-json/feather':
specifier: ^1.2.1
version: 1.2.1
'@inspira-ui/plugins':
specifier: ^0.0.1
version: 0.0.1
'@tailwindcss/postcss':
specifier: ^4.1.17
version: 4.1.17
@ -178,18 +187,30 @@ importers:
autoprefixer:
specifier: ^10.4.22
version: 10.4.22(postcss@8.5.6)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
clsx:
specifier: ^2.1.1
version: 2.1.1
postcss:
specifier: ^8.5.6
version: 8.5.6
tailwind-merge:
specifier: ^3.4.0
version: 3.4.0
tailwindcss:
specifier: ^4.1.17
version: 4.1.17
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@4.1.17)
terser:
specifier: ^5.44.1
version: 5.44.1
unplugin-auto-import:
specifier: ^20.2.0
version: 20.2.0
version: 20.2.0(@vueuse/core@14.1.0)
unplugin-icons:
specifier: ^22.5.0
version: 22.5.0
@ -733,6 +754,12 @@ packages:
- supports-color
dev: true
/@inspira-ui/plugins@0.0.1:
resolution: {integrity: sha512-gM4iZptDoStA7QT1lltC6Jl4qRLhkZwVtcXomQy/PPxe0lAxhrZx40KXEUJakRZLOSmyfVJCExzZhRzOrE6DBQ==}
dependencies:
mini-svg-data-uri: 1.4.4
dev: true
/@intlify/core-base@11.1.12:
resolution: {integrity: sha512-whh0trqRsSqVLNEUCwU59pyJZYpU8AmSWl8M3Jz2Mv5ESPP6kFh4juas2NpZ1iCvy7GlNRffUD1xr84gceimjg==}
engines: {node: '>= 16'}
@ -1040,11 +1067,6 @@ packages:
dev: true
optional: true
/@stripe/stripe-js@4.8.0:
resolution: {integrity: sha512-+4Cb0bVHlV4BJXxkJ3cCLSLuWxm3pXKtgcRacox146EuugjCzRRII5T5gUMgL4HpzrBLVwVxjKaZqntNWAXawQ==}
engines: {node: '>=12.16'}
dev: false
/@sxzz/popperjs-es@2.11.7:
resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==}
dev: false
@ -1288,6 +1310,9 @@ packages:
resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
dev: false
/@types/web-bluetooth@0.0.21:
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
/@types/webxr@0.5.24:
resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==}
dev: false
@ -1423,6 +1448,16 @@ packages:
vue-demi: 0.13.11(vue@3.5.24)
dev: false
/@vueuse/core@14.1.0(vue@3.5.24):
resolution: {integrity: sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==}
peerDependencies:
vue: ^3.5.0
dependencies:
'@types/web-bluetooth': 0.0.21
'@vueuse/metadata': 14.1.0
'@vueuse/shared': 14.1.0(vue@3.5.24)
vue: 3.5.24
/@vueuse/core@9.13.0(vue@3.5.24):
resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==}
dependencies:
@ -1435,10 +1470,20 @@ packages:
- vue
dev: false
/@vueuse/metadata@14.1.0:
resolution: {integrity: sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==}
/@vueuse/metadata@9.13.0:
resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==}
dev: false
/@vueuse/shared@14.1.0(vue@3.5.24):
resolution: {integrity: sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==}
peerDependencies:
vue: ^3.5.0
dependencies:
vue: 3.5.24
/@vueuse/shared@9.13.0(vue@3.5.24):
resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
dependencies:
@ -1624,6 +1669,12 @@ packages:
readdirp: 4.1.2
dev: true
/class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
dependencies:
clsx: 2.1.1
dev: true
/cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
@ -1633,6 +1684,11 @@ packages:
wrap-ansi: 7.0.0
dev: true
/clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
dev: true
/color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@ -2287,6 +2343,25 @@ packages:
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
dev: true
/framer-motion@12.23.12:
resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
dependencies:
motion-dom: 12.23.12
motion-utils: 12.23.6
tslib: 2.8.1
dev: false
/fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
dev: true
@ -2469,6 +2544,10 @@ packages:
function-bind: 1.1.2
dev: false
/hey-listen@1.0.8:
resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==}
dev: false
/hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
dev: false
@ -2522,6 +2601,11 @@ packages:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
dev: true
/install@0.13.0:
resolution: {integrity: sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==}
engines: {node: '>= 0.10'}
dev: false
/is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
@ -2823,6 +2907,11 @@ packages:
mime-db: 1.52.0
dev: false
/mini-svg-data-uri@1.4.4:
resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==}
hasBin: true
dev: true
/minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
dependencies:
@ -2856,6 +2945,33 @@ packages:
ufo: 1.6.1
dev: true
/motion-dom@12.23.12:
resolution: {integrity: sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==}
dependencies:
motion-utils: 12.23.6
dev: false
/motion-utils@12.23.6:
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
dev: false
/motion-v@1.7.4(@vueuse/core@14.1.0)(vue@3.5.24):
resolution: {integrity: sha512-YNDUAsany04wfI7YtHxQK3kxzNvh+OdFUk9GpA3+hMt7j6P+5WrVAAgr8kmPPoVza9EsJiAVhqoN3YYFN0Twrw==}
peerDependencies:
'@vueuse/core': '>=10.0.0'
vue: '>=3.0.0'
dependencies:
'@vueuse/core': 14.1.0(vue@3.5.24)
framer-motion: 12.23.12
hey-listen: 1.0.8
motion-dom: 12.23.12
vue: 3.5.24
transitivePeerDependencies:
- '@emotion/is-prop-valid'
- react
- react-dom
dev: false
/ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@ -3235,6 +3351,10 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
/sortablejs@1.14.0:
resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==}
dev: false
/source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@ -3324,6 +3444,18 @@ packages:
has-flag: 4.0.0
dev: true
/tailwind-merge@3.4.0:
resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==}
dev: true
/tailwindcss-animate@1.0.7(tailwindcss@4.1.17):
resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==}
peerDependencies:
tailwindcss: '>=3.0.0 || insiders'
dependencies:
tailwindcss: 4.1.17
dev: true
/tailwindcss@4.1.17:
resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==}
dev: true
@ -3372,7 +3504,6 @@ packages:
/tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
dev: true
/turbo-darwin-64@1.10.0:
resolution: {integrity: sha512-N0aVGFtBgOKd7pIdUiKREwnDhNHRIvpMJbmUw04c1AqEoTiKAKT6iuzcCozO5N/gYMVr0hxrXgLal5OLYXtcsw==}
@ -3485,7 +3616,7 @@ packages:
unplugin-utils: 0.3.1
dev: true
/unplugin-auto-import@20.2.0:
/unplugin-auto-import@20.2.0(@vueuse/core@14.1.0):
resolution: {integrity: sha512-vfBI/SvD9hJqYNinipVOAj5n8dS8DJXFlCKFR5iLDp2SaQwsfdnfLXgZ+34Kd3YY3YEY9omk8XQg0bwos3Q8ug==}
engines: {node: '>=14'}
peerDependencies:
@ -3497,6 +3628,7 @@ packages:
'@vueuse/core':
optional: true
dependencies:
'@vueuse/core': 14.1.0(vue@3.5.24)
local-pkg: 1.1.2
magic-string: 0.30.21
picomatch: 4.0.3
@ -3812,6 +3944,15 @@ packages:
'@vue/server-renderer': 3.5.24(vue@3.5.24)
'@vue/shared': 3.5.24
/vuedraggable@4.1.0(vue@3.5.24):
resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}
peerDependencies:
vue: ^3.0.1
dependencies:
sortablejs: 1.14.0
vue: 3.5.24
dev: false
/web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}