This commit is contained in:
parent
0f7647fdbc
commit
bdb9fa3a39
|
|
@ -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方法来创建、列出、更新、启用/禁用、重置密码、查看详情和删除管理员用户。
|
||||
|
|
@ -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)
|
||||
- 响应式设计
|
||||
- 暗色主题支持
|
||||
|
|
@ -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. 注意事项
|
||||
- 确保拖拽功能在各种浏览器中正常工作
|
||||
- 实现响应式设计,适配不同屏幕尺寸
|
||||
- 保持代码风格与现有代码一致
|
||||
- 添加适当的用户反馈和验证
|
||||
|
|
@ -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 }
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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: '已从生效状态移除'
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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: '提示词管理'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -55,12 +55,12 @@
|
|||
</div>
|
||||
|
||||
<!-- 列表内容 -->
|
||||
<!-- stripe -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="commissionList"
|
||||
style="width: 100%"
|
||||
border
|
||||
stripe
|
||||
>
|
||||
<el-table-column
|
||||
prop="creatorName"
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -54,9 +54,9 @@
|
|||
|
||||
<!-- 订单列表区域 -->
|
||||
<div class="disassembly-table">
|
||||
<!-- stripe -->
|
||||
<el-table
|
||||
:data="paginatedOrders"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
// 排序已经通过sortedActivePrompts的setter自动处理
|
||||
}
|
||||
|
||||
// 处理拖拽结束(从左侧到右侧)
|
||||
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>
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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')" />
|
||||
|
|
|
|||
|
|
@ -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')" />
|
||||
|
|
|
|||
|
|
@ -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')" />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -76,9 +76,9 @@
|
|||
|
||||
<!-- 用户列表 -->
|
||||
<div class="user-table">
|
||||
<!-- stripe -->
|
||||
<el-table
|
||||
:data="userList"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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]`
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
})
|
||||
|
||||
// 监听金额变化,重新计算费用
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
<span class="points-icon">🪄</span>
|
||||
<span class="points-text">{{ remainingPoints }}</span>
|
||||
</div> -->
|
||||
<!-- 用户信息模块已取消 -->
|
||||
<!-- 用户信息模块已取消 -->
|
||||
</div>
|
||||
|
||||
<!-- 折叠状态下的用户头像 -->
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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":"任务已提交,正在处理"}}
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
171
pnpm-lock.yaml
171
pnpm-lock.yaml
|
|
@ -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'}
|
||||
|
|
|
|||
Loading…
Reference in New Issue