Compare commits
10 Commits
b76e3ff47d
...
00d60b78c4
| Author | SHA1 | Date | |
|---|---|---|---|
| 00d60b78c4 | |||
| c121f03ad1 | |||
| 3bbb28677b | |||
| 9c4d39e29d | |||
| 6ec2a0aa6c | |||
| 1e5a1d422b | |||
| 72fa2c63a1 | |||
| 2d96e8ca4e | |||
| d429dc887a | |||
| 120a4529a5 |
@@ -1,277 +0,0 @@
|
||||
# 设计系统迁移指南
|
||||
|
||||
> 将现有前端代码迁移到统一设计系统
|
||||
|
||||
---
|
||||
|
||||
## 一、迁移步骤
|
||||
|
||||
### Step 1: 引入设计令牌
|
||||
|
||||
在 `src/main.ts` 中添加设计令牌导入:
|
||||
|
||||
```typescript
|
||||
// main.ts
|
||||
import './styles/design-tokens.css' // 添加在最先
|
||||
import './style.less'
|
||||
// ... 其他导入
|
||||
```
|
||||
|
||||
### Step 2: 配置 TailwindCSS
|
||||
|
||||
更新 `tailwind.config.js` 为 `tailwind.config.ts`:
|
||||
|
||||
```bash
|
||||
# 删除旧配置
|
||||
rm tailwind.config.js
|
||||
|
||||
# 使用新配置
|
||||
# 已创建: tailwind.config.ts
|
||||
```
|
||||
|
||||
### Step 3: 配置 Ant Design Vue 主题
|
||||
|
||||
更新 `src/App.vue`:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { ConfigProvider, theme } from 'ant-design-vue'
|
||||
import { antdThemeConfig, themeManager } from '@gold/styles/antd-theme'
|
||||
|
||||
const isDark = ref(themeManager.initTheme())
|
||||
const currentTheme = computed(() => themeManager.getAntdTheme(isDark.value))
|
||||
|
||||
// 监听系统主题变化
|
||||
watchEffect(() => {
|
||||
const media = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const handler = (e: MediaQueryListEvent) => {
|
||||
if (!localStorage.getItem('theme')) {
|
||||
isDark.value = e.matches
|
||||
themeManager.setTheme(e.matches)
|
||||
}
|
||||
}
|
||||
media.addEventListener('change', handler)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfigProvider :theme="currentTheme">
|
||||
<router-view />
|
||||
</ConfigProvider>
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、组件迁移示例
|
||||
|
||||
### 2.1 颜色迁移
|
||||
|
||||
**Before (问题代码)**:
|
||||
```less
|
||||
// ❌ 组件内定义变量
|
||||
@primary: #6366f1;
|
||||
@primary-gold: #D4A853;
|
||||
|
||||
.button {
|
||||
background: #1890FF;
|
||||
}
|
||||
```
|
||||
|
||||
**After (规范代码)**:
|
||||
```less
|
||||
// ✅ 使用全局设计令牌
|
||||
.button {
|
||||
background: var(--color-primary-500);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-primary-400);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 字体迁移
|
||||
|
||||
**Before**:
|
||||
```css
|
||||
/* ❌ 硬编码字体大小 */
|
||||
.title { font-size: 24px; }
|
||||
.desc { font-size: 12px; }
|
||||
```
|
||||
|
||||
**After**:
|
||||
```css
|
||||
/* ✅ 使用设计令牌 */
|
||||
.title { font-size: var(--font-size-2xl); }
|
||||
.desc { font-size: var(--font-size-xs); }
|
||||
```
|
||||
|
||||
### 2.3 间距迁移
|
||||
|
||||
**Before**:
|
||||
```css
|
||||
/* ❌ 魔法数字 */
|
||||
.card {
|
||||
padding: 20px;
|
||||
margin-top: 24px;
|
||||
gap: 16px;
|
||||
}
|
||||
```
|
||||
|
||||
**After**:
|
||||
```css
|
||||
/* ✅ 语义化间距 */
|
||||
.card {
|
||||
padding: var(--space-5);
|
||||
margin-top: var(--space-6);
|
||||
gap: var(--space-4);
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 使用 TailwindCSS
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- 使用 TailwindCSS 工具类 -->
|
||||
<div class="p-5 rounded-lg shadow-base bg-white">
|
||||
<h2 class="text-lg font-medium text-gray-900">标题</h2>
|
||||
<p class="mt-2 text-sm text-gray-500">描述文字</p>
|
||||
<button class="mt-4 px-4 py-2 bg-primary-500 text-white rounded-base
|
||||
hover:bg-primary-400 transition-duration-fast">
|
||||
操作按钮
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、需要修改的文件清单
|
||||
|
||||
### 优先级 P0 - 立即修改
|
||||
|
||||
| 文件 | 问题 | 修改内容 |
|
||||
|------|------|----------|
|
||||
| `src/components/agents/ChatDrawer.vue` | 局部定义 `@primary: #6366f1` | 改用 `var(--color-primary-500)` |
|
||||
| `src/components/agents/HistoryPanel.vue` | 局部定义 `@primary: #6366f1` | 改用 `var(--color-primary-500)` |
|
||||
| `src/views/auth/Login.vue` | 金色主题 `@primary-gold: #D4A853` | 改用品牌蓝 |
|
||||
| `src/views/home/Home.vue` | 硬编码颜色 `#1890FF` | 改用 `var(--color-primary-500)` |
|
||||
|
||||
### 优先级 P1 - 短期修改
|
||||
|
||||
| 文件 | 问题 |
|
||||
|------|------|
|
||||
| `src/views/content-style/Copywriting.vue` | 混用 `var(--color-primary)` 和 `#1677ff` |
|
||||
| `src/components/GradientButton.vue` | 需要验证与设计系统一致 |
|
||||
|
||||
---
|
||||
|
||||
## 四、批量替换脚本
|
||||
|
||||
### 4.1 颜色替换
|
||||
|
||||
```bash
|
||||
# 在 frontend/app/web-gold 目录执行
|
||||
|
||||
# 替换硬编码的主色
|
||||
find src -name "*.vue" -o -name "*.less" | xargs sed -i 's/#1890FF/var(--color-primary-500)/g'
|
||||
find src -name "*.vue" -o -name "*.less" | xargs sed -i 's/#40A9FF/var(--color-primary-400)/g'
|
||||
find src -name "*.vue" -o -name "*.less" | xargs sed -i 's/#6366f1/var(--color-primary-500)/g'
|
||||
find src -name "*.vue" -o -name "*.less" | xargs sed -i 's/#1677ff/var(--color-primary-500)/g'
|
||||
```
|
||||
|
||||
### 4.2 间距替换
|
||||
|
||||
```bash
|
||||
# padding: 16px -> padding: var(--space-4)
|
||||
find src -name "*.vue" -o -name "*.less" | xargs sed -i 's/padding: 16px/padding: var(--space-4)/g'
|
||||
find src -name "*.vue" -o -name "*.less" | xargs sed -i 's/padding: 20px/padding: var(--space-5)/g'
|
||||
find src -name "*.vue" -o -name "*.less" | xargs sed -i 's/padding: 24px/padding: var(--space-6)/g'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、验证清单
|
||||
|
||||
完成迁移后,检查以下项目:
|
||||
|
||||
- [ ] 所有颜色使用 CSS 变量或 TailwindCSS 类
|
||||
- [ ] 没有硬编码的 `#xxxxxx` 颜色值(除特殊情况)
|
||||
- [ ] 字体大小使用 `var(--font-size-*)` 或 TailwindCSS
|
||||
- [ ] 间距使用 `var(--space-*)` 或 TailwindCSS
|
||||
- [ ] 组件内没有重复定义 `@primary` 等 Less 变量
|
||||
- [ ] 页面视觉效果一致
|
||||
- [ ] 深色模式切换正常工作
|
||||
|
||||
---
|
||||
|
||||
## 六、设计令牌速查表
|
||||
|
||||
### 颜色
|
||||
|
||||
| 用途 | 变量 | 值 |
|
||||
|------|------|-----|
|
||||
| 主色 | `--color-primary-500` | #3B82F6 |
|
||||
| 主色悬浮 | `--color-primary-400` | #60A5FA |
|
||||
| 成功 | `--color-success-500` | #22C55E |
|
||||
| 警告 | `--color-warning-500` | #F59E0B |
|
||||
| 错误 | `--color-error-500` | #EF4444 |
|
||||
| 正文 | `--color-text-primary` | #111827 |
|
||||
| 次要文字 | `--color-text-secondary` | #4B5563 |
|
||||
| 边框 | `--color-border` | #E5E7EB |
|
||||
|
||||
### 间距
|
||||
|
||||
| 变量 | 值 | 用途 |
|
||||
|------|-----|------|
|
||||
| `--space-2` | 8px | 图标与文字 |
|
||||
| `--space-3` | 12px | 小间距 |
|
||||
| `--space-4` | 16px | 标准间距 |
|
||||
| `--space-5` | 20px | 卡片内边距 |
|
||||
| `--space-6` | 24px | 区块间距 |
|
||||
|
||||
### 圆角
|
||||
|
||||
| 变量 | 值 | 用途 |
|
||||
|------|-----|------|
|
||||
| `--radius-sm` | 4px | 标签 |
|
||||
| `--radius-base` | 6px | 按钮、输入框 |
|
||||
| `--radius-md` | 8px | 下拉框 |
|
||||
| `--radius-lg` | 12px | 卡片、弹窗 |
|
||||
|
||||
---
|
||||
|
||||
## 七、常见问题
|
||||
|
||||
### Q: 旧组件样式不生效?
|
||||
|
||||
确保设计令牌在 `main.ts` 中最先导入。
|
||||
|
||||
### Q: Ant Design 组件样式不一致?
|
||||
|
||||
检查 `App.vue` 中是否正确配置了 `ConfigProvider` 的 `theme` 属性。
|
||||
|
||||
### Q: TailwindCSS 类不生效?
|
||||
|
||||
1. 确保 `tailwind.config.ts` 文件存在
|
||||
2. 确保 `postcss.config.js` 配置正确
|
||||
3. 重启开发服务器
|
||||
|
||||
### Q: 深色模式切换有闪烁?
|
||||
|
||||
在 `index.html` 的 `<head>` 中添加:
|
||||
|
||||
```html
|
||||
<script>
|
||||
(function() {
|
||||
const theme = localStorage.getItem('theme');
|
||||
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **迁移完成后**,项目将拥有统一的设计语言,后续开发效率将显著提升。
|
||||
@@ -8,7 +8,6 @@ import { API_BASE } from '@gold/config/api'
|
||||
|
||||
const BASE_URL = `${API_BASE.APP_TIK}`
|
||||
|
||||
|
||||
/**
|
||||
* 获取启用的智能体列表
|
||||
*/
|
||||
@@ -116,3 +115,27 @@ export function getMessages(params) {
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加智能体收藏
|
||||
* @param {number} agentId - 智能体ID
|
||||
*/
|
||||
export function addFavorite(agentId) {
|
||||
return request({
|
||||
url: `${BASE_URL}/agent/favorite/create`,
|
||||
method: 'post',
|
||||
params: { agentId }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消智能体收藏
|
||||
* @param {number} agentId - 智能体ID
|
||||
*/
|
||||
export function removeFavorite(agentId) {
|
||||
return request({
|
||||
url: `${BASE_URL}/agent/favorite/delete`,
|
||||
method: 'delete',
|
||||
params: { agentId }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,5 +15,5 @@ import { API_BASE } from '@gold/config/api'
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getPointRecordPage(params = {}) {
|
||||
return http.get(`${API_BASE.APP_MEMBER}/tik/point-record/page`, { params })
|
||||
return http.get(`${API_BASE.APP_TIK}/point-record/page`, { params })
|
||||
}
|
||||
|
||||
@@ -20,6 +20,21 @@ function filterVisibleGroups(config, isLoggedIn) {
|
||||
const visibleNavConfig = computed(() => {
|
||||
return filterVisibleGroups(navConfig, userStore.isLoggedIn)
|
||||
})
|
||||
|
||||
// 脱敏手机号
|
||||
const maskedMobile = computed(() => {
|
||||
const mobile = userStore.mobile
|
||||
if (!mobile) return '未设置'
|
||||
return mobile.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||||
})
|
||||
|
||||
// 剩余额度百分比
|
||||
const remainingPercent = computed(() => {
|
||||
const total = userStore.totalStorage
|
||||
const remaining = userStore.remainingStorage
|
||||
if (total === 0) return 0
|
||||
return Math.min(100, Math.round((remaining / total) * 100))
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -43,6 +58,17 @@ const visibleNavConfig = computed(() => {
|
||||
</router-link>
|
||||
</div>
|
||||
</nav>
|
||||
<!-- 底部用户信息卡片 -->
|
||||
<router-link
|
||||
v-if="userStore.isLoggedIn"
|
||||
to="/user/profile"
|
||||
class="sidebar__footer"
|
||||
>
|
||||
<div class="user-card">
|
||||
<div class="user-card__mobile">{{ maskedMobile }}</div>
|
||||
<div class="user-card__quota">剩余额度 {{ remainingPercent }}%</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
@@ -54,10 +80,13 @@ const visibleNavConfig = computed(() => {
|
||||
width: 220px;
|
||||
border-right: 1px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar__nav {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
@@ -125,4 +154,37 @@ const visibleNavConfig = computed(() => {
|
||||
.nav-item__label {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 底部用户信息卡片 */
|
||||
.sidebar__footer {
|
||||
flex-shrink: 0;
|
||||
padding: 12px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
background: var(--color-slate-50);
|
||||
cursor: pointer;
|
||||
transition: background .2s ease, transform .12s ease;
|
||||
}
|
||||
|
||||
.user-card:hover {
|
||||
background: var(--color-slate-100);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.user-card__mobile {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-slate-700);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.user-card__quota {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -109,6 +109,16 @@
|
||||
:style="{ '--i': index }"
|
||||
@click="handleAgentClick(agent)"
|
||||
>
|
||||
<!-- 收藏图标 - 右上角 -->
|
||||
<button
|
||||
class="favorite-icon"
|
||||
:class="{ 'favorite-icon--active': agent.isFavorite }"
|
||||
@click.stop="handleFavorite(agent)"
|
||||
>
|
||||
<StarFilled v-if="agent.isFavorite" />
|
||||
<StarOutlined v-else />
|
||||
</button>
|
||||
|
||||
<!-- 卡片内容 -->
|
||||
<div class="card-content">
|
||||
<!-- 左侧头像 -->
|
||||
@@ -134,11 +144,16 @@
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<div class="card-footer">
|
||||
<div class="footer-spacer"></div>
|
||||
<button class="chat-btn" @click.stop="handleChat(agent)">
|
||||
<MessageOutlined class="chat-btn-icon" />
|
||||
<span>对话</span>
|
||||
</button>
|
||||
<div class="footer-actions">
|
||||
<button class="action-btn" @click.stop="handleHistory(agent)">
|
||||
<HistoryOutlined class="action-btn-icon" />
|
||||
<span>历史</span>
|
||||
</button>
|
||||
<button class="action-btn action-btn--primary" @click.stop="handleChat(agent)">
|
||||
<MessageOutlined class="action-btn-icon" />
|
||||
<span>对话</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
@@ -161,6 +176,13 @@
|
||||
:agent="currentAgent"
|
||||
@send="handleSendMessage"
|
||||
/>
|
||||
|
||||
<!-- 历史记录面板 -->
|
||||
<HistoryPanel
|
||||
:visible="historyPanelVisible"
|
||||
:agent-id="historyAgentId"
|
||||
@close="closeHistoryPanel"
|
||||
/>
|
||||
</FullWidthLayout>
|
||||
</template>
|
||||
|
||||
@@ -170,14 +192,18 @@ import {
|
||||
SearchOutlined,
|
||||
RobotOutlined,
|
||||
CloseOutlined,
|
||||
ArrowRightOutlined,
|
||||
MessageOutlined,
|
||||
AppstoreOutlined
|
||||
AppstoreOutlined,
|
||||
StarOutlined,
|
||||
StarFilled,
|
||||
HistoryOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import FullWidthLayout from '@/layouts/components/FullWidthLayout.vue'
|
||||
import ChatDrawer from '@/components/agents/ChatDrawer.vue'
|
||||
import { getAgentList } from '@/api/agent'
|
||||
import HistoryPanel from '@/components/agents/HistoryPanel.vue'
|
||||
import { getAgentList, addFavorite, removeFavorite } from '@/api/agent'
|
||||
import tokenManager from '@gold/utils/token-manager'
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
@@ -189,6 +215,8 @@ const categoryScrollRef = ref(null)
|
||||
const expandTriggerRef = ref(null)
|
||||
const showCategoryPanel = ref(false)
|
||||
const panelTop = ref(0)
|
||||
const historyPanelVisible = ref(false)
|
||||
const historyAgentId = ref(null)
|
||||
|
||||
// 面板样式
|
||||
const panelStyle = computed(() => ({
|
||||
@@ -218,7 +246,7 @@ const categories = computed(() => {
|
||||
return cats
|
||||
})
|
||||
|
||||
// 过滤后的列表
|
||||
// 过滤后的列表(收藏置顶)
|
||||
const filteredAgentList = computed(() => {
|
||||
let list = agentList.value
|
||||
|
||||
@@ -234,7 +262,11 @@ const filteredAgentList = computed(() => {
|
||||
)
|
||||
}
|
||||
|
||||
return list
|
||||
// 收藏的智能体置顶(复制数组避免修改原数组)
|
||||
return [...list].sort((a, b) => {
|
||||
if (a.isFavorite === b.isFavorite) return 0
|
||||
return a.isFavorite ? -1 : 1
|
||||
})
|
||||
})
|
||||
|
||||
// 获取智能体列表
|
||||
@@ -250,7 +282,8 @@ const fetchAgentList = async () => {
|
||||
description: item.description,
|
||||
avatar: item.icon,
|
||||
categoryName: item.categoryName || '其他',
|
||||
tagColor: getTagColor(item.categoryName)
|
||||
tagColor: getTagColor(item.categoryName),
|
||||
isFavorite: item.isFavorite || false
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -323,10 +356,48 @@ const handleChat = (agent) => {
|
||||
chatDrawerVisible.value = true
|
||||
}
|
||||
|
||||
const handleHistory = (agent) => {
|
||||
// 检查登录状态
|
||||
if (!tokenManager.isLoggedIn()) {
|
||||
message.warning('请先登录')
|
||||
return
|
||||
}
|
||||
historyAgentId.value = agent.id
|
||||
historyPanelVisible.value = true
|
||||
}
|
||||
|
||||
const closeHistoryPanel = () => {
|
||||
historyPanelVisible.value = false
|
||||
}
|
||||
|
||||
const handleSendMessage = (data) => {
|
||||
console.log('发送消息:', data)
|
||||
}
|
||||
|
||||
// 收藏切换
|
||||
const handleFavorite = async (agent) => {
|
||||
// 检查登录状态
|
||||
if (!tokenManager.isLoggedIn()) {
|
||||
message.warning('请先登录')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (agent.isFavorite) {
|
||||
await removeFavorite(agent.id)
|
||||
agent.isFavorite = false
|
||||
message.success('已取消收藏')
|
||||
} else {
|
||||
await addFavorite(agent.id)
|
||||
agent.isFavorite = true
|
||||
message.success('收藏成功')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('收藏操作失败:', error)
|
||||
message.error('操作失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchAgentList()
|
||||
@@ -767,38 +838,105 @@ onMounted(() => {
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.footer-spacer {
|
||||
flex: 1;
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.chat-btn {
|
||||
.action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 14px;
|
||||
background: #111827;
|
||||
border: none;
|
||||
gap: 5px;
|
||||
padding: 6px 12px;
|
||||
background: white;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
color: #6B7280;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #1F2937;
|
||||
transform: scale(1.02);
|
||||
border-color: #D1D5DB;
|
||||
background: #F9FAFB;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
&--primary {
|
||||
background: #111827;
|
||||
border-color: #111827;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: #1F2937;
|
||||
border-color: #1F2937;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-btn-icon {
|
||||
.action-btn-icon {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
// 收藏图标 - 右上角
|
||||
.favorite-icon {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-gray-400);
|
||||
font-size: 16px;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 2;
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
|
||||
&:hover {
|
||||
background: white;
|
||||
color: #F59E0B;
|
||||
transform: scale(1);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
// 收藏状态 - 常显
|
||||
&--active {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
color: #F59E0B;
|
||||
background: #FEF3C7;
|
||||
|
||||
&:hover {
|
||||
background: #FDE68A;
|
||||
color: #D97706;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片hover时显示收藏图标
|
||||
.agent-card:hover .favorite-icon:not(.favorite-icon--active) {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 空状态
|
||||
// ============================================
|
||||
|
||||
@@ -39,24 +39,6 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">合成模型等级</label>
|
||||
<div class="model-options">
|
||||
<button
|
||||
class="model-btn"
|
||||
:class="{ 'model-btn--active': store.speechRate <= 1 }"
|
||||
>
|
||||
标准版 (1x积分)
|
||||
</button>
|
||||
<button
|
||||
class="model-btn model-btn--pro"
|
||||
:class="{ 'model-btn--active': store.speechRate > 1 }"
|
||||
>
|
||||
Pro 旗舰版 (3x积分)
|
||||
<CrownFilled class="pro-icon" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -193,7 +175,7 @@
|
||||
<div class="preview-meta">
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">预计消耗积分</span>
|
||||
<span class="meta-value">150 积分</span>
|
||||
<span class="meta-value">{{ estimatedPoints }} 积分</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">当前余额</span>
|
||||
@@ -218,6 +200,7 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import { CloudUploadOutlined, CrownFilled, PictureOutlined } from '@ant-design/icons-vue'
|
||||
import { useVoiceCopyStore } from '@/stores/voiceCopy'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { usePointsConfigStore } from '@/stores/pointsConfig'
|
||||
import VideoSelector from '@/components/VideoSelector.vue'
|
||||
import VoiceSelector from '@/components/VoiceSelector.vue'
|
||||
import ResultPanel from '@/components/ResultPanel.vue'
|
||||
@@ -228,6 +211,7 @@ import { useDigitalHumanStore } from './stores/useDigitalHumanStore'
|
||||
const store = useDigitalHumanStore()
|
||||
const voiceStore = useVoiceCopyStore()
|
||||
const userStore = useUserStore()
|
||||
const pointsConfigStore = usePointsConfigStore()
|
||||
|
||||
// ==================== 本地状态 ====================
|
||||
const dragOver = ref(false)
|
||||
@@ -248,6 +232,12 @@ const progressStatus = computed(() => {
|
||||
return 'active'
|
||||
})
|
||||
|
||||
// 预计消耗积分(从配置获取 kling 模型积分)
|
||||
const estimatedPoints = computed(() => {
|
||||
const points = pointsConfigStore.getConsumePoints('kling')
|
||||
return points ?? 150 // 默认 150 积分
|
||||
})
|
||||
|
||||
// ==================== 方法 ====================
|
||||
function triggerFileSelect() {
|
||||
fileInput.value?.click()
|
||||
@@ -288,7 +278,8 @@ function getVideoPreviewUrl(video: any): string {
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
voiceStore.refresh(),
|
||||
userStore.fetchUserProfile()
|
||||
userStore.fetchUserProfile(),
|
||||
pointsConfigStore.loadConfig()
|
||||
])
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -770,7 +770,7 @@ onMounted(async () => {
|
||||
|
||||
.sidebar-slide-enter-active,
|
||||
.sidebar-slide-leave-active {
|
||||
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: all var(--duration-slow) cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.sidebar-slide-enter-from,
|
||||
@@ -790,7 +790,7 @@ onMounted(async () => {
|
||||
margin-bottom: var(--space-3);
|
||||
|
||||
span {
|
||||
font-size: 11px;
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-500);
|
||||
text-transform: uppercase;
|
||||
@@ -829,7 +829,7 @@ onMounted(async () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px var(--space-3);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: 2px;
|
||||
@@ -860,8 +860,8 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
margin-right: var(--space-2);
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--color-gray-500);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -881,7 +881,7 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
&__count {
|
||||
font-size: 11px;
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-gray-500);
|
||||
margin-right: var(--space-2);
|
||||
}
|
||||
@@ -922,7 +922,7 @@ onMounted(async () => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: all var(--duration-slow) cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
@@ -942,7 +942,7 @@ onMounted(async () => {
|
||||
.category-switcher {
|
||||
display: flex;
|
||||
background: var(--color-gray-50);
|
||||
border-radius: 10px;
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-1);
|
||||
gap: var(--space-1);
|
||||
|
||||
@@ -950,13 +950,13 @@ onMounted(async () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: 10px var(--space-5);
|
||||
padding: var(--space-2) var(--space-5);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-600);
|
||||
transition: all 0.25s ease;
|
||||
transition: all var(--duration-base) ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-gray-900);
|
||||
@@ -974,9 +974,9 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 16px;
|
||||
font-size: var(--font-size-md);
|
||||
color: var(--color-gray-500);
|
||||
transition: color 0.25s ease;
|
||||
transition: color var(--duration-base) ease;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1013,7 +1013,7 @@ onMounted(async () => {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--color-primary-500), var(--color-primary-400));
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
transition: width var(--duration-slow) ease;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1066,13 +1066,13 @@ onMounted(async () => {
|
||||
&__info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: var(--space-2);
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--color-gray-900);
|
||||
|
||||
.anticon {
|
||||
color: var(--color-primary-500);
|
||||
font-size: 16px;
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
strong {
|
||||
@@ -1089,7 +1089,7 @@ onMounted(async () => {
|
||||
|
||||
.fade-slide-enter-active,
|
||||
.fade-slide-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
transition: all var(--duration-slow) ease;
|
||||
}
|
||||
|
||||
.fade-slide-enter-from,
|
||||
@@ -1159,7 +1159,7 @@ onMounted(async () => {
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: all var(--duration-slow) cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary-500);
|
||||
@@ -1193,8 +1193,8 @@ onMounted(async () => {
|
||||
// 选择指示器
|
||||
&__check {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
top: var(--space-2);
|
||||
left: var(--space-2);
|
||||
z-index: 10;
|
||||
opacity: 0;
|
||||
transition: opacity var(--duration-fast);
|
||||
@@ -1234,14 +1234,14 @@ onMounted(async () => {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 10;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
background: linear-gradient(135deg, var(--color-gray-50) 0%, var(--color-gray-100) 100%);
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.4s ease;
|
||||
transition: transform var(--duration-slower) ease;
|
||||
}
|
||||
|
||||
&:hover img {
|
||||
@@ -1270,11 +1270,11 @@ onMounted(async () => {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.6));
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
transition: opacity var(--duration-slow) ease;
|
||||
}
|
||||
|
||||
&__type-tag {
|
||||
font-size: 11px;
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
@@ -1283,7 +1283,7 @@ onMounted(async () => {
|
||||
|
||||
// 信息区
|
||||
&__info {
|
||||
padding: 14px;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
}
|
||||
|
||||
&__name {
|
||||
@@ -1330,7 +1330,7 @@ onMounted(async () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px var(--space-6);
|
||||
padding: var(--space-3) var(--space-6);
|
||||
background: var(--color-bg-card);
|
||||
border-top: 1px solid var(--color-gray-200);
|
||||
|
||||
|
||||
@@ -772,18 +772,18 @@ onMounted(() => {
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
gap: var(--space-4);
|
||||
|
||||
&__icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
font-size: var(--font-size-2xl);
|
||||
box-shadow: var(--shadow-inset-card);
|
||||
}
|
||||
|
||||
@@ -792,7 +792,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 24px;
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
@@ -800,9 +800,9 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-size: 14px;
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--color-text-secondary);
|
||||
margin: 4px 0 0;
|
||||
margin: var(--space-1) 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -830,34 +830,34 @@ onMounted(() => {
|
||||
.card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
height: fit-content;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
padding: 12px 16px;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 16px;
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: var(--space-2);
|
||||
|
||||
.anticon {
|
||||
color: var(--color-primary);
|
||||
font-size: 18px;
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: 16px;
|
||||
padding: var(--space-4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -876,14 +876,14 @@ onMounted(() => {
|
||||
.slider-box {
|
||||
.slider-value {
|
||||
text-align: center;
|
||||
margin-top: 12px;
|
||||
font-size: 18px;
|
||||
margin-top: var(--space-3);
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
padding: 8px 16px;
|
||||
padding: var(--space-2) var(--space-4);
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -892,9 +892,9 @@ onMounted(() => {
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
margin: var(--space-3) 0;
|
||||
padding: 20px;
|
||||
padding: var(--space-5);
|
||||
background: var(--color-surface);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
|
||||
.scene-row {
|
||||
@@ -904,13 +904,13 @@ onMounted(() => {
|
||||
|
||||
span {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--color-text);
|
||||
font-size: 18px;
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 700;
|
||||
|
||||
&.text-green {
|
||||
@@ -922,13 +922,13 @@ onMounted(() => {
|
||||
|
||||
.btn-secondary {
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text);
|
||||
transition: all 0.2s ease;
|
||||
transition: all var(--duration-base) ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--color-bg);
|
||||
@@ -952,16 +952,16 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.scene-header {
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.scene-title {
|
||||
font-size: 16px;
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
gap: var(--space-3);
|
||||
|
||||
.scene-index {
|
||||
color: var(--color-primary);
|
||||
@@ -969,23 +969,23 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.scene-duration {
|
||||
font-size: 14px;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: 500;
|
||||
padding: 4px 12px;
|
||||
padding: var(--space-1) var(--space-3);
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text);
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-base);
|
||||
}
|
||||
|
||||
.scene-count {
|
||||
font-size: 13px;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-success);
|
||||
margin-left: auto;
|
||||
padding: 4px 12px;
|
||||
padding: var(--space-1) var(--space-3);
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-base);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
@@ -1001,14 +1001,14 @@ onMounted(() => {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
border: 2px dashed var(--color-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
transition: all var(--duration-base) ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
gap: var(--space-2);
|
||||
background: var(--color-surface);
|
||||
position: relative;
|
||||
|
||||
@@ -1024,7 +1024,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 13px;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -1033,11 +1033,11 @@ onMounted(() => {
|
||||
.candidate-item {
|
||||
width: 140px;
|
||||
height: 130px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
transition: all 0.2s ease;
|
||||
transition: all var(--duration-base) ease;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
@@ -1060,13 +1060,13 @@ onMounted(() => {
|
||||
justify-content: center;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 24px;
|
||||
font-size: var(--font-size-2xl);
|
||||
}
|
||||
|
||||
.candidate-name {
|
||||
font-size: 12px;
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text);
|
||||
padding: 8px 10px;
|
||||
padding: var(--space-2) var(--space-2);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -1090,8 +1090,8 @@ onMounted(() => {
|
||||
line-height: 1;
|
||||
min-width: auto;
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 6px;
|
||||
transition: all var(--duration-base) ease;
|
||||
border-radius: var(--radius-base);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-secondary);
|
||||
@@ -1108,7 +1108,7 @@ onMounted(() => {
|
||||
top: 50% ;
|
||||
left: 50% ;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 12px;
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1125,11 +1125,11 @@ onMounted(() => {
|
||||
|
||||
.material-item {
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
transition: all 0.2s ease;
|
||||
transition: all var(--duration-base) ease;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
@@ -1174,21 +1174,21 @@ onMounted(() => {
|
||||
|
||||
.material-badge {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
top: var(--space-2);
|
||||
right: var(--space-2);
|
||||
background: var(--color-error);
|
||||
color: #fff;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
padding: var(--space-1) var(--space-2);
|
||||
border-radius: var(--radius-base);
|
||||
font-size: var(--font-size-xs);
|
||||
z-index: 2;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 4px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.material-name {
|
||||
padding: 10px;
|
||||
font-size: 13px;
|
||||
padding: var(--space-2);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -1200,9 +1200,9 @@ onMounted(() => {
|
||||
.selector-container {
|
||||
.selector-actions {
|
||||
margin-bottom: var(--space-2);
|
||||
padding: 16px;
|
||||
padding: var(--space-4);
|
||||
background: var(--color-surface);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
@@ -1213,18 +1213,18 @@ onMounted(() => {
|
||||
gap: var(--space-2);
|
||||
max-height: 480px;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
padding: var(--space-2);
|
||||
background: var(--color-surface);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.selector-item {
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
border: 2px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
transition: all var(--duration-base) ease;
|
||||
position: relative;
|
||||
background: var(--color-surface);
|
||||
box-shadow: var(--shadow-inset-card);
|
||||
@@ -1246,7 +1246,7 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 24px;
|
||||
font-size: var(--font-size-2xl);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
@@ -1256,8 +1256,8 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.selector-name {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-size: var(--font-size-xs);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -1269,8 +1269,8 @@ onMounted(() => {
|
||||
|
||||
.selector-checkmark {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
top: var(--space-2);
|
||||
right: var(--space-2);
|
||||
background: var(--color-success);
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
@@ -1279,7 +1279,7 @@ onMounted(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-size: var(--font-size-base);
|
||||
z-index: 2;
|
||||
box-shadow: 0 2px 4px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
@@ -1287,21 +1287,21 @@ onMounted(() => {
|
||||
|
||||
.selector-actions-footer {
|
||||
margin-top: var(--space-2);
|
||||
padding: 16px;
|
||||
padding: var(--space-4);
|
||||
background: var(--color-surface);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
text-align: right;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
:deep(.selector-modal) {
|
||||
.ant-modal-content {
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ant-modal-header {
|
||||
padding: 20px;
|
||||
padding: var(--space-5);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
@@ -1312,7 +1312,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
padding: 20px;
|
||||
padding: var(--space-5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1356,14 +1356,14 @@ onMounted(() => {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
gap: var(--space-3);
|
||||
|
||||
&__title {
|
||||
font-size: 20px;
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-size: 13px;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,97 +1,93 @@
|
||||
<template>
|
||||
<a-tag :color="color" :class="statusClass">
|
||||
<template v-if="showIcon && (status === 'running' || status === 'RUNNING')">
|
||||
<LoadingOutlined :spin="true" />
|
||||
<span class="task-status-tag" :class="statusClass">
|
||||
<template v-if="showIcon && isRunning">
|
||||
<LoadingOutlined class="task-status-tag__icon" :spin="true" />
|
||||
</template>
|
||||
{{ text }}
|
||||
</a-tag>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { LoadingOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
// 状态值
|
||||
status: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
// 是否显示图标
|
||||
showIcon: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 自定义状态映射
|
||||
statusMap: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
status: { type: String, required: true },
|
||||
showIcon: { type: Boolean, default: true },
|
||||
statusMap: { type: Object, default: () => ({}) }
|
||||
})
|
||||
|
||||
// 计算属性:状态文本
|
||||
const text = computed(() => {
|
||||
// 使用自定义映射或默认映射(同时支持大小写)
|
||||
const map = {
|
||||
pending: '待处理',
|
||||
running: '处理中',
|
||||
success: '已完成',
|
||||
failed: '失败',
|
||||
canceled: '已取消',
|
||||
// 大写状态支持
|
||||
PENDING: '待处理',
|
||||
RUNNING: '处理中',
|
||||
SUCCESS: '已完成',
|
||||
PROCESSING: '处理中',
|
||||
FAILED: '失败',
|
||||
CANCELED: '已取消',
|
||||
...props.statusMap
|
||||
}
|
||||
return map[props.status] || props.status
|
||||
const STATUS_CONFIG = {
|
||||
pending: { text: '待处理', class: 'task-status-tag--pending' },
|
||||
running: { text: '处理中', class: 'task-status-tag--running' },
|
||||
processing: { text: '处理中', class: 'task-status-tag--running' },
|
||||
success: { text: '已完成', class: 'task-status-tag--success' },
|
||||
failed: { text: '失败', class: 'task-status-tag--failed' },
|
||||
canceled: { text: '已取消', class: 'task-status-tag--canceled' }
|
||||
}
|
||||
|
||||
const normalizedStatus = computed(() => props.status?.toLowerCase() || '')
|
||||
|
||||
const config = computed(() => {
|
||||
const custom = props.statusMap[props.status]
|
||||
if (custom) return { text: custom, class: `task-status-tag--${normalizedStatus.value}` }
|
||||
return STATUS_CONFIG[normalizedStatus.value] || { text: props.status, class: 'task-status-tag--default' }
|
||||
})
|
||||
|
||||
// 计算属性:状态颜色
|
||||
const color = computed(() => {
|
||||
const colorMap = {
|
||||
pending: 'default',
|
||||
running: 'processing',
|
||||
success: 'success',
|
||||
failed: 'error',
|
||||
canceled: 'warning',
|
||||
// 大写状态支持
|
||||
PENDING: 'default',
|
||||
RUNNING: 'processing',
|
||||
SUCCESS: 'success',
|
||||
FAILED: 'error',
|
||||
CANCELED: 'warning'
|
||||
}
|
||||
return colorMap[props.status] || 'default'
|
||||
})
|
||||
|
||||
// 计算属性:状态样式类
|
||||
const statusClass = computed(() => {
|
||||
// 将状态标准化为小写,用于CSS类名
|
||||
const normalizedStatus = props.status.toLowerCase()
|
||||
return `task-status-tag--${normalizedStatus}`
|
||||
})
|
||||
const text = computed(() => config.value.text)
|
||||
const statusClass = computed(() => config.value.class)
|
||||
const isRunning = computed(() => normalizedStatus.value === 'running' || normalizedStatus.value === 'processing')
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 状态标签动画效果 */
|
||||
.task-status-tag--running {
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
<style scoped lang="less">
|
||||
.task-status-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-medium);
|
||||
border-radius: var(--radius-sm);
|
||||
line-height: 1.5;
|
||||
|
||||
&__icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&--pending {
|
||||
background: var(--color-gray-100);
|
||||
color: var(--color-gray-600);
|
||||
}
|
||||
|
||||
&--running {
|
||||
background: var(--color-primary-50);
|
||||
color: var(--color-primary-600);
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
&--success {
|
||||
background: var(--color-success-50);
|
||||
color: var(--color-success-600);
|
||||
}
|
||||
|
||||
&--failed {
|
||||
background: var(--color-error-50);
|
||||
color: var(--color-error-600);
|
||||
}
|
||||
|
||||
&--canceled {
|
||||
background: var(--color-warning-50);
|
||||
color: var(--color-warning-600);
|
||||
}
|
||||
|
||||
&--default {
|
||||
background: var(--color-gray-100);
|
||||
color: var(--color-gray-600);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.6; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div class="digital-human-task-page">
|
||||
<div class="task-page">
|
||||
<!-- 筛选条件 -->
|
||||
<div class="digital-human-task-page__filters">
|
||||
<div class="task-page__filters">
|
||||
<a-space :size="16">
|
||||
<a-select
|
||||
v-model:value="filters.status"
|
||||
class="filter-select"
|
||||
placeholder="任务状态"
|
||||
@change="handleFilterChange"
|
||||
allow-clear
|
||||
@change="handleFilterChange"
|
||||
>
|
||||
<a-select-option value="">全部状态</a-select-option>
|
||||
<a-select-option value="pending">待处理</a-select-option>
|
||||
@@ -25,9 +25,7 @@
|
||||
allow-clear
|
||||
@press-enter="handleFilterChange"
|
||||
>
|
||||
<template #prefix>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
|
||||
<a-range-picker
|
||||
@@ -39,39 +37,22 @@
|
||||
@change="handleFilterChange"
|
||||
/>
|
||||
|
||||
<a-button type="primary" class="filter-button" @click="handleFilterChange">
|
||||
查询
|
||||
</a-button>
|
||||
<a-button class="filter-button" @click="handleResetFilters">
|
||||
重置
|
||||
</a-button>
|
||||
<a-button type="primary" @click="handleFilterChange">查询</a-button>
|
||||
<a-button @click="handleResetFilters">重置</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<div class="digital-human-task-page__content">
|
||||
<div class="task-page__content">
|
||||
<!-- 批量操作栏 -->
|
||||
<div v-if="selectedRowKeys.length > 0" class="batch-actions">
|
||||
<a-alert
|
||||
:message="`已选中 ${selectedRowKeys.length} 项`"
|
||||
type="info"
|
||||
show-icon
|
||||
>
|
||||
<a-alert :message="`已选中 ${selectedRowKeys.length} 项`" type="info" show-icon>
|
||||
<template #action>
|
||||
<a-space>
|
||||
|
||||
<a-popconfirm
|
||||
title="确定要删除选中的任务吗?删除后无法恢复。"
|
||||
@confirm="handleBatchDelete"
|
||||
>
|
||||
<a-button size="small" danger>
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
批量删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
<a-popconfirm title="确定要删除选中的任务吗?" @confirm="handleBatchDelete">
|
||||
<a-button size="small" danger>
|
||||
<DeleteOutlined /> 批量删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</a-alert>
|
||||
</div>
|
||||
@@ -82,21 +63,14 @@
|
||||
:columns="columns"
|
||||
:row-key="record => record.id"
|
||||
:pagination="paginationConfig"
|
||||
@change="handleTableChange"
|
||||
:row-selection="rowSelection"
|
||||
:scroll="{ x: 1000 }"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<!-- 任务名称列 -->
|
||||
<template #bodyCell="{ column, record }">
|
||||
<!-- 任务名称列 -->
|
||||
<template v-if="column.key === 'taskName'">
|
||||
<div class="task-name-cell">
|
||||
<strong>{{ record.taskName }}</strong>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 音色列 -->
|
||||
<template v-else-if="column.key === 'voiceId'">
|
||||
<span>{{ record.voiceId || '-' }}</span>
|
||||
<strong>{{ record.taskName }}</strong>
|
||||
</template>
|
||||
|
||||
<!-- 状态列 -->
|
||||
@@ -106,10 +80,10 @@
|
||||
|
||||
<!-- 进度列 -->
|
||||
<template v-else-if="column.key === 'progress'">
|
||||
<div style="min-width: 100px">
|
||||
<div class="progress-cell">
|
||||
<a-progress
|
||||
:percent="record.progress"
|
||||
:status="getProgressStatus(record.status)"
|
||||
:status="PROGRESS_STATUS[record.status]"
|
||||
size="small"
|
||||
:show-info="false"
|
||||
/>
|
||||
@@ -118,27 +92,22 @@
|
||||
|
||||
<!-- 创建时间列 -->
|
||||
<template v-else-if="column.key === 'createTime'">
|
||||
{{ formatDateTime(record.createTime) }}
|
||||
{{ formatDate(record.createTime) }}
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space>
|
||||
|
||||
<!-- 下载按钮 -->
|
||||
<a-button
|
||||
v-if="isStatus(record.status, 'success')"
|
||||
type="link"
|
||||
size="small"
|
||||
class="action-btn action-btn--success"
|
||||
@click="handleDownload(record)"
|
||||
class="action-btn-download"
|
||||
>
|
||||
<template #icon>
|
||||
<DownloadOutlined />
|
||||
</template>
|
||||
下载
|
||||
<DownloadOutlined /> 下载
|
||||
</a-button>
|
||||
<!-- 取消按钮 -->
|
||||
|
||||
<a-button
|
||||
v-if="isStatus(record.status, 'running')"
|
||||
type="link"
|
||||
@@ -148,11 +117,8 @@
|
||||
取消
|
||||
</a-button>
|
||||
|
||||
<a-popconfirm
|
||||
title="确定删除这个任务吗?删除后无法恢复。"
|
||||
@confirm="() => handleDelete(record.id)"
|
||||
>
|
||||
<a-button size="small" type="link" class="action-btn-delete">删除</a-button>
|
||||
<a-popconfirm title="确定删除?删除后无法恢复。" @confirm="handleDelete(record.id)">
|
||||
<a-button type="link" size="small" class="action-btn action-btn--danger">删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
@@ -166,364 +132,145 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
SearchOutlined,
|
||||
PlayCircleOutlined,
|
||||
DownloadOutlined,
|
||||
DeleteOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import {
|
||||
getDigitalHumanTaskPage,
|
||||
cancelTask,
|
||||
deleteTask
|
||||
} from '@/api/digitalHuman'
|
||||
import { SearchOutlined, DownloadOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
||||
import { getDigitalHumanTaskPage, cancelTask, deleteTask } from '@/api/digitalHuman'
|
||||
import { formatDate } from '@/utils/file'
|
||||
import { useTaskList } from '@/views/system/task-management/composables/useTaskList'
|
||||
import { useTaskOperations } from '@/views/system/task-management/composables/useTaskOperations'
|
||||
import { useTaskPolling } from '@/views/system/task-management/composables/useTaskPolling'
|
||||
import TaskStatusTag from '@/views/system/task-management/components/TaskStatusTag.vue'
|
||||
|
||||
// 使用 Composable
|
||||
const {
|
||||
loading,
|
||||
list,
|
||||
filters,
|
||||
paginationConfig,
|
||||
fetchList,
|
||||
handleFilterChange,
|
||||
handleResetFilters,
|
||||
handleTableChange
|
||||
} = useTaskList(getDigitalHumanTaskPage)
|
||||
// 进度状态映射
|
||||
const PROGRESS_STATUS = {
|
||||
pending: 'normal', running: 'active', success: 'success', failed: 'exception', canceled: 'normal',
|
||||
PENDING: 'normal', RUNNING: 'active', SUCCESS: 'success', FAILED: 'exception', CANCELED: 'normal'
|
||||
}
|
||||
|
||||
// 使用任务操作 Composable
|
||||
const {
|
||||
handleDelete,
|
||||
handleCancel,
|
||||
} = useTaskOperations(
|
||||
{
|
||||
deleteApi: deleteTask,
|
||||
cancelApi: cancelTask,
|
||||
},
|
||||
fetchList
|
||||
)
|
||||
// Composables
|
||||
const { loading, list, filters, paginationConfig, fetchList, handleFilterChange, handleResetFilters, handleTableChange } = useTaskList(getDigitalHumanTaskPage)
|
||||
const { handleDelete, handleCancel } = useTaskOperations({ deleteApi: deleteTask, cancelApi: cancelTask }, fetchList)
|
||||
useTaskPolling(getDigitalHumanTaskPage, { onTaskUpdate: fetchList })
|
||||
|
||||
// 使用轮询 Composable
|
||||
useTaskPolling(getDigitalHumanTaskPage, {
|
||||
onTaskUpdate: () => {
|
||||
fetchList()
|
||||
}
|
||||
})
|
||||
|
||||
// 表格选择相关
|
||||
// 表格选择
|
||||
const selectedRowKeys = ref([])
|
||||
|
||||
// 表格行选择配置
|
||||
const rowSelection = {
|
||||
selectedRowKeys,
|
||||
onChange: (keys) => {
|
||||
selectedRowKeys.value = keys
|
||||
},
|
||||
onSelectAll: (selected, selectedRows, changeRows) => {
|
||||
// 全选逻辑
|
||||
console.log('全选状态:', selected, '选中行数:', selectedRows.length, '变化行数:', changeRows.length)
|
||||
}
|
||||
onChange: (keys) => { selectedRowKeys.value = keys }
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDateTime = (dateStr) => {
|
||||
return formatDate(dateStr)
|
||||
}
|
||||
// 状态判断
|
||||
const isStatus = (status, target) => status === target || status === target.toUpperCase()
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
fixed: 'left'
|
||||
},
|
||||
{
|
||||
title: '任务名称',
|
||||
dataIndex: 'taskName',
|
||||
key: 'taskName',
|
||||
width: 250,
|
||||
ellipsis: true,
|
||||
fixed: 'left'
|
||||
},
|
||||
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '进度',
|
||||
dataIndex: 'progress',
|
||||
key: 'progress',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 240,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 获取进度条状态
|
||||
const getProgressStatus = (status) => {
|
||||
const statusMap = {
|
||||
pending: 'normal',
|
||||
running: 'active',
|
||||
success: 'success',
|
||||
failed: 'exception',
|
||||
canceled: 'normal',
|
||||
// 大写状态支持
|
||||
PENDING: 'normal',
|
||||
RUNNING: 'active',
|
||||
SUCCESS: 'success',
|
||||
FAILED: 'exception',
|
||||
CANCELED: 'normal'
|
||||
}
|
||||
return statusMap[status] || 'normal'
|
||||
}
|
||||
|
||||
// 检查状态(同时支持大小写)
|
||||
const isStatus = (status, targetStatus) => {
|
||||
return status === targetStatus || status === targetStatus.toUpperCase()
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 下载视频 - 新窗口打开(浏览器自动处理下载)
|
||||
// 下载视频
|
||||
const handleDownload = (record) => {
|
||||
console.log(record)
|
||||
if (!record.resultVideoUrl) {
|
||||
message.warning('该任务暂无视频结果,请稍后再试')
|
||||
return
|
||||
}
|
||||
|
||||
window.open(record.resultVideoUrl, '_blank')
|
||||
}
|
||||
|
||||
// 批量删除任务
|
||||
// 批量删除
|
||||
const handleBatchDelete = async () => {
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
message.warning('请选择要删除的任务')
|
||||
return
|
||||
}
|
||||
if (!selectedRowKeys.value.length) return
|
||||
|
||||
try {
|
||||
// 逐个删除选中的任务
|
||||
for (const id of selectedRowKeys.value) {
|
||||
await deleteTask(id)
|
||||
}
|
||||
|
||||
for (const id of selectedRowKeys.value) await deleteTask(id)
|
||||
message.success(`成功删除 ${selectedRowKeys.value.length} 个任务`)
|
||||
|
||||
// 清空选择并刷新列表
|
||||
selectedRowKeys.value = []
|
||||
await fetchList()
|
||||
} catch (error) {
|
||||
console.error('批量删除失败:', error)
|
||||
} catch (e) {
|
||||
console.error('批量删除失败:', e)
|
||||
message.error('批量删除失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchList()
|
||||
})
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80, fixed: 'left' },
|
||||
{ title: '任务名称', dataIndex: 'taskName', key: 'taskName', width: 250, ellipsis: true, fixed: 'left' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 },
|
||||
{ title: '进度', dataIndex: 'progress', key: 'progress', width: 150 },
|
||||
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 180 },
|
||||
{ title: '操作', key: 'actions', width: 200, fixed: 'right' }
|
||||
]
|
||||
|
||||
onMounted(fetchList)
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.digital-human-task-page {
|
||||
padding: 0 var(--space-3);
|
||||
.task-page {
|
||||
padding: var(--space-4);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
&__filters {
|
||||
padding: var(--space-3);
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-card);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
|
||||
.filter-select,
|
||||
.filter-input {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.filter-date-picker {
|
||||
width: 280px;
|
||||
}
|
||||
.task-page__filters {
|
||||
padding: var(--space-4);
|
||||
background: var(--color-bg-card);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
|
||||
.filter-select,
|
||||
.filter-input {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-card);
|
||||
padding: var(--space-3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
.filter-date-picker {
|
||||
width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 批量操作栏 */
|
||||
.task-page__content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background: var(--color-bg-card);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.batch-actions {
|
||||
margin-bottom: var(--space-3);
|
||||
margin-bottom: var(--space-4);
|
||||
|
||||
+ .ant-spin {
|
||||
+ :deep(.ant-spin) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
:deep(.ant-spin-container) {
|
||||
.ant-spin-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:deep(.ant-table) {
|
||||
.ant-table {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 任务名称单元格 */
|
||||
.task-name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
.progress-cell {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
/* 操作按钮样式 */
|
||||
.action-btn-preview {
|
||||
color: var(--color-primary);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary-hover, var(--color-blue-600));
|
||||
}
|
||||
.action-btn {
|
||||
&--success { color: var(--color-success-500); &:hover { color: var(--color-success-600); } }
|
||||
&--danger { color: var(--color-error-500); &:hover { color: var(--color-error-600); } }
|
||||
}
|
||||
|
||||
.action-btn-download {
|
||||
color: var(--color-success);
|
||||
|
||||
&:hover {
|
||||
color: #059669;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn-delete {
|
||||
color: var(--color-error);
|
||||
|
||||
&:hover {
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
/* 文本截断 */
|
||||
.text-ellipsis {
|
||||
display: inline-block;
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 展开内容 */
|
||||
.expanded-content {
|
||||
padding: var(--space-3);
|
||||
background: var(--color-bg-2);
|
||||
border-radius: var(--radius-card);
|
||||
margin: var(--space-2);
|
||||
}
|
||||
|
||||
.task-text {
|
||||
margin-bottom: var(--space-3);
|
||||
|
||||
p {
|
||||
margin: var(--space-2) 0 0 0;
|
||||
padding: var(--space-2);
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-card);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.task-params {
|
||||
margin-bottom: var(--space-3);
|
||||
|
||||
.params-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: var(--space-2);
|
||||
margin-top: var(--space-2);
|
||||
|
||||
.param-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--space-2);
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-card);
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.param-label {
|
||||
font-weight: 600;
|
||||
margin-right: var(--space-2);
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
.param-value {
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-result {
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.processing-tip {
|
||||
color: var(--color-text-3);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.task-error {
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
/* 确保按钮内的图标和文字对齐 */
|
||||
:deep(.ant-btn .anticon) {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
:deep(.ant-table-tbody > tr > td) {
|
||||
padding: 12px 8px;
|
||||
padding: var(--space-3) var(--space-2);
|
||||
}
|
||||
|
||||
:deep(.ant-table-thead > tr > th) {
|
||||
background: var(--color-bg-2);
|
||||
font-weight: 600;
|
||||
background: var(--color-gray-50);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
/* 桌面端样式优化 */
|
||||
:deep(.ant-btn .anticon) {
|
||||
line-height: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,22 +8,13 @@
|
||||
</div>
|
||||
<ul class="task-layout__nav-list">
|
||||
<li
|
||||
v-for="item in navItems"
|
||||
v-for="item in NAV_ITEMS"
|
||||
:key="item.type"
|
||||
class="task-layout__nav-item"
|
||||
:class="{
|
||||
'is-active': currentType === item.type
|
||||
}"
|
||||
:class="{ 'is-active': currentType === item.type }"
|
||||
>
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
class="task-layout__nav-link"
|
||||
@click="navigateTo(item.type)"
|
||||
>
|
||||
<span class="nav-icon">
|
||||
<VideoCameraOutlined v-if="item.icon === 'video'" />
|
||||
<UserOutlined v-else-if="item.icon === 'user'" />
|
||||
</span>
|
||||
<a class="task-layout__nav-link" @click="navigateTo(item.type)">
|
||||
<component :is="item.icon" class="nav-icon" />
|
||||
<span class="nav-text">{{ item.label }}</span>
|
||||
</a>
|
||||
</li>
|
||||
@@ -41,55 +32,37 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, defineAsyncComponent } from 'vue'
|
||||
import { computed, defineAsyncComponent, markRaw } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { VideoCameraOutlined, UserOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
// 响应式数据
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 当前任务类型
|
||||
const currentType = computed(() => {
|
||||
const type = route.params.type
|
||||
|
||||
if (!type || type === 'task-management') {
|
||||
return 'mix-task'
|
||||
}
|
||||
|
||||
return type
|
||||
const { type } = route.params
|
||||
return !type || type === 'task-management' ? 'mix-task' : type
|
||||
})
|
||||
|
||||
// 动态导入组件
|
||||
const MixTaskList = defineAsyncComponent(() => import('../mix-task/index.vue'))
|
||||
const DigitalHumanTaskList = defineAsyncComponent(() => import('../digital-human-task/index.vue'))
|
||||
|
||||
// 导航项配置
|
||||
const navItems = [
|
||||
const NAV_ITEMS = [
|
||||
{
|
||||
type: 'mix-task',
|
||||
label: '混剪视频任务',
|
||||
icon: 'video',
|
||||
component: MixTaskList
|
||||
icon: VideoCameraOutlined,
|
||||
component: markRaw(defineAsyncComponent(() => import('../mix-task/index.vue')))
|
||||
},
|
||||
{
|
||||
type: 'digital-human-task',
|
||||
label: '数字人视频任务',
|
||||
icon: 'user',
|
||||
component: DigitalHumanTaskList
|
||||
icon: UserOutlined,
|
||||
component: markRaw(defineAsyncComponent(() => import('../digital-human-task/index.vue')))
|
||||
}
|
||||
]
|
||||
|
||||
// 当前组件
|
||||
const currentComponent = computed(() => {
|
||||
const item = navItems.find(item => item.type === currentType.value)
|
||||
if (!item) {
|
||||
return navItems[0].component
|
||||
}
|
||||
return item.component
|
||||
return NAV_ITEMS.find(item => item.type === currentType.value)?.component ?? NAV_ITEMS[0].component
|
||||
})
|
||||
|
||||
// 导航到指定类型
|
||||
const navigateTo = (type) => {
|
||||
router.push(`/system/task-management/${type}`)
|
||||
}
|
||||
@@ -99,141 +72,96 @@ const navigateTo = (type) => {
|
||||
.task-layout {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: var(--color-bg-card);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
/* 左侧导航 */
|
||||
.task-layout__sidebar {
|
||||
width: 220px;
|
||||
background: var(--color-surface);
|
||||
border-right: 1px solid var(--color-border);
|
||||
background: transparent;
|
||||
border-right: 1px solid var(--color-gray-200);
|
||||
flex-shrink: 0;
|
||||
overflow-y: auto;
|
||||
|
||||
@media (max-width: 1199px) {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&.is-mobile-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 导航头部 */
|
||||
.task-layout__nav-header {
|
||||
height: 64px;
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
flex-shrink: 0;
|
||||
padding: 0 var(--space-6);
|
||||
border-bottom: 1px solid var(--color-gray-200);
|
||||
}
|
||||
|
||||
.task-layout__nav-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
line-height: 1.4;
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-gray-800);
|
||||
}
|
||||
|
||||
/* 导航列表 */
|
||||
.task-layout__nav-list {
|
||||
list-style: none;
|
||||
padding: var(--space-1) 0;
|
||||
padding: var(--space-2) 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 导航项 */
|
||||
.task-layout__nav-item {
|
||||
margin: 4px var(--space-2);
|
||||
margin: var(--space-1) var(--space-3);
|
||||
|
||||
&.is-active {
|
||||
.task-layout__nav-link {
|
||||
background: var(--color-primary);
|
||||
&.is-active .task-layout__nav-link {
|
||||
background: var(--color-primary-500);
|
||||
color: #fff;
|
||||
box-shadow: var(--shadow-sm);
|
||||
|
||||
.nav-icon {
|
||||
color: #fff;
|
||||
|
||||
.nav-icon {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 导航链接 */
|
||||
.task-layout__nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--space-2) var(--space-2);
|
||||
border-radius: var(--radius-card);
|
||||
color: var(--color-text-secondary);
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
padding: var(--space-3);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-gray-600);
|
||||
cursor: pointer;
|
||||
transition: all var(--duration-fast) var(--ease-out);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-2);
|
||||
color: var(--color-primary);
|
||||
background: var(--color-gray-100);
|
||||
color: var(--color-primary-500);
|
||||
}
|
||||
|
||||
.is-active & {
|
||||
&:hover {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 导航图标 */
|
||||
.nav-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-right: var(--space-2);
|
||||
color: var(--color-text-3);
|
||||
transition: color 0.2s;
|
||||
|
||||
.task-layout__nav-item.is-active & {
|
||||
.is-active &:hover {
|
||||
background: var(--color-primary-600);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
/* 导航文本 */
|
||||
.nav-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
.nav-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-right: var(--space-2);
|
||||
color: var(--color-gray-500);
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
/* 右侧内容 */
|
||||
.task-layout__content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background: var(--color-bg);
|
||||
padding: 0;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
padding: 0;
|
||||
}
|
||||
background: var(--color-bg-page);
|
||||
}
|
||||
|
||||
/* 过渡动画 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.25s ease;
|
||||
transition: opacity var(--duration-base) var(--ease-out);
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div class="mix-task-page">
|
||||
<div class="task-page">
|
||||
<!-- 筛选条件 -->
|
||||
<div class="mix-task-page__filters">
|
||||
<div class="task-page__filters">
|
||||
<a-space :size="16">
|
||||
<a-select
|
||||
v-model:value="filters.status"
|
||||
class="filter-select"
|
||||
placeholder="任务状态"
|
||||
@change="handleFilterChange"
|
||||
allow-clear
|
||||
@change="handleFilterChange"
|
||||
>
|
||||
<a-select-option value="">全部状态</a-select-option>
|
||||
<a-select-option value="pending">待处理</a-select-option>
|
||||
@@ -24,9 +24,7 @@
|
||||
allow-clear
|
||||
@press-enter="handleFilterChange"
|
||||
>
|
||||
<template #prefix>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
|
||||
<a-range-picker
|
||||
@@ -38,177 +36,138 @@
|
||||
@change="handleFilterChange"
|
||||
/>
|
||||
|
||||
<a-button type="primary" class="filter-button" @click="handleFilterChange">
|
||||
查询
|
||||
</a-button>
|
||||
<a-button class="filter-button" @click="handleResetFilters">
|
||||
重置
|
||||
</a-button>
|
||||
<a-button type="primary" @click="handleFilterChange">查询</a-button>
|
||||
<a-button @click="handleResetFilters">重置</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<div class="mix-task-page__content">
|
||||
<div class="task-page__content">
|
||||
<a-spin :spinning="loading" tip="加载中...">
|
||||
<a-table
|
||||
:data-source="list"
|
||||
:columns="columns"
|
||||
:row-key="record => record.id"
|
||||
:pagination="paginationConfig"
|
||||
@change="handleTableChange"
|
||||
:expanded-row-keys="expandedRowKeys"
|
||||
@expandedRowsChange="handleExpandedRowsChange"
|
||||
:scroll="{ x: 'max-content' }"
|
||||
@change="handleTableChange"
|
||||
@expandedRowsChange="handleExpandedRowsChange"
|
||||
>
|
||||
<!-- 标题列 -->
|
||||
<template #bodyCell="{ column, record }">
|
||||
<!-- 标题列 -->
|
||||
<template v-if="column.key === 'title'">
|
||||
<div class="title-cell">
|
||||
<strong>{{ record.title }}</strong>
|
||||
<a-tag v-if="record.text" size="small" style="margin-left: 8px">有文案</a-tag>
|
||||
<a-tag v-if="record.text" size="small">有文案</a-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 状态列 -->
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)" class="status-tag">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
<TaskStatusTag :status="record.status" />
|
||||
</template>
|
||||
|
||||
<!-- 创建时间列 -->
|
||||
<!-- 时间列 -->
|
||||
<template v-else-if="column.key === 'createTime'">
|
||||
{{ formatDate(record.createTime) }}
|
||||
</template>
|
||||
|
||||
<!-- 完成时间列 -->
|
||||
<template v-else-if="column.key === 'finishTime'">
|
||||
{{ record.finishTime ? formatDate(record.finishTime) : '-' }}
|
||||
</template>
|
||||
|
||||
<!-- 生成结果列 -->
|
||||
<template v-else-if="column.key === 'outputUrls'">
|
||||
<div v-if="record.outputUrls && record.outputUrls.length > 0">
|
||||
<a-tag color="success">{{ record.outputUrls.length }} 个视频</a-tag>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
<a-tag v-if="record.outputUrls?.length" color="success">
|
||||
{{ record.outputUrls.length }} 个视频
|
||||
</a-tag>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
|
||||
<!-- 操作列 (增强版:预览+下载+其他操作) -->
|
||||
<!-- 操作列 -->
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space>
|
||||
<!-- 预览按钮 -->
|
||||
<a-button
|
||||
v-if="isStatus(record.status, 'success') && record.outputUrls && record.outputUrls.length > 0"
|
||||
v-if="canOperate(record, 'preview')"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="openPreviewModal(record)"
|
||||
class="action-btn-preview"
|
||||
class="action-btn action-btn--primary"
|
||||
@click="openPreview(record)"
|
||||
>
|
||||
<template #icon>
|
||||
<PlayCircleOutlined />
|
||||
</template>
|
||||
预览
|
||||
<PlayCircleOutlined /> 预览
|
||||
</a-button>
|
||||
|
||||
<!-- 下载按钮 -->
|
||||
<a-button
|
||||
v-if="isStatus(record.status, 'success') && record.outputUrls && record.outputUrls.length > 0"
|
||||
v-if="canOperate(record, 'download')"
|
||||
type="link"
|
||||
size="small"
|
||||
class="action-btn action-btn--success"
|
||||
@click="handleDownload(record)"
|
||||
class="action-btn-download"
|
||||
>
|
||||
<template #icon>
|
||||
<DownloadOutlined />
|
||||
</template>
|
||||
下载
|
||||
<DownloadOutlined /> 下载
|
||||
</a-button>
|
||||
|
||||
<!-- 取消按钮 -->
|
||||
<a-button
|
||||
v-if="isStatus(record.status, 'running')"
|
||||
v-if="canOperate(record, 'cancel')"
|
||||
size="small"
|
||||
@click="handleCancel(record.id)"
|
||||
>
|
||||
取消
|
||||
</a-button>
|
||||
|
||||
<!-- 重试按钮 -->
|
||||
<a-button
|
||||
v-if="isStatus(record.status, 'failed')"
|
||||
v-if="canOperate(record, 'retry')"
|
||||
size="small"
|
||||
@click="handleRetry(record.id)"
|
||||
>
|
||||
重试
|
||||
</a-button>
|
||||
|
||||
<!-- 删除按钮 -->
|
||||
<a-popconfirm
|
||||
title="确定删除这个任务吗?删除后无法恢复。"
|
||||
@confirm="() => handleDelete(record.id)"
|
||||
>
|
||||
<a-button size="small" type="link" class="action-btn-delete">删除</a-button>
|
||||
<a-popconfirm title="确定删除?删除后无法恢复。" @confirm="handleDelete(record.id)">
|
||||
<a-button type="link" size="small" class="action-btn action-btn--danger">删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 展开行内容 (优化版) -->
|
||||
<!-- 展开行内容 -->
|
||||
<template #expandedRowRender="{ record }">
|
||||
<div class="expanded-content">
|
||||
<!-- 任务详情 -->
|
||||
<div v-if="record.text" class="task-text">
|
||||
<strong>文案内容:</strong>
|
||||
<p>{{ record.text }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 生成结果 -->
|
||||
<div v-if="record.outputUrls && record.outputUrls.length > 0" class="task-results">
|
||||
<div v-if="record.outputUrls?.length" class="task-results">
|
||||
<div class="result-header">
|
||||
<strong>生成结果:</strong>
|
||||
<span class="result-count">{{ record.outputUrls.length }} 个视频</span>
|
||||
</div>
|
||||
<div class="result-list">
|
||||
<div
|
||||
v-for="(url, index) in record.outputUrls"
|
||||
:key="index"
|
||||
class="result-item"
|
||||
>
|
||||
<div v-for="(_, index) in record.outputUrls" :key="index" class="result-item">
|
||||
<a-button
|
||||
v-if="isStatus(record.status, 'success')"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handlePreviewSingle(record, index)"
|
||||
class="result-preview-btn"
|
||||
@click="previewVideo(record, index)"
|
||||
>
|
||||
<PlayCircleOutlined />
|
||||
视频 {{ index + 1 }}
|
||||
<PlayCircleOutlined /> 视频 {{ index + 1 }}
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="isStatus(record.status, 'success')"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleDownloadSingle(record.id, index)"
|
||||
class="result-download-btn"
|
||||
@click="downloadVideo(record.id, index)"
|
||||
>
|
||||
<DownloadOutlined />
|
||||
</a-button>
|
||||
<span v-else class="processing-tip">
|
||||
视频 {{ index + 1 }} (处理中...)
|
||||
</span>
|
||||
<span v-else class="text-muted">视频 {{ index + 1 }} (处理中...)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误信息 -->
|
||||
<div v-if="record.errorMsg" class="task-error">
|
||||
<a-alert
|
||||
type="error"
|
||||
:message="record.errorMsg"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
<a-alert v-if="record.errorMsg" type="error" :message="record.errorMsg" show-icon />
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
@@ -216,21 +175,9 @@
|
||||
</div>
|
||||
|
||||
<!-- 预览模态框 -->
|
||||
<a-modal
|
||||
v-model:open="previewVisible"
|
||||
:title="previewTitle"
|
||||
width="800px"
|
||||
:footer="null"
|
||||
:centered="true"
|
||||
class="preview-modal"
|
||||
>
|
||||
<div v-if="previewUrl" class="preview-container">
|
||||
<video
|
||||
:src="previewUrl"
|
||||
controls
|
||||
autoplay
|
||||
style="width: 100%; max-height: 600px; border-radius: 8px;"
|
||||
>
|
||||
<a-modal v-model:open="preview.visible" :title="preview.title" width="800px" :footer="null" centered>
|
||||
<div v-if="preview.url" class="preview-container">
|
||||
<video :src="preview.url" controls autoplay class="preview-video">
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</div>
|
||||
@@ -242,76 +189,64 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import {
|
||||
SearchOutlined,
|
||||
PlayCircleOutlined,
|
||||
DownloadOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { SearchOutlined, PlayCircleOutlined, DownloadOutlined } from '@ant-design/icons-vue'
|
||||
import { MixTaskService } from '@/api/mixTask'
|
||||
import { formatDate } from '@/utils/file'
|
||||
import { useTaskList } from '@/views/system/task-management/composables/useTaskList'
|
||||
import { useTaskOperations } from '@/views/system/task-management/composables/useTaskOperations'
|
||||
import { useTaskPolling } from '@/views/system/task-management/composables/useTaskPolling'
|
||||
import TaskStatusTag from '@/views/system/task-management/components/TaskStatusTag.vue'
|
||||
|
||||
// 使用 Composable
|
||||
const {
|
||||
loading,
|
||||
list,
|
||||
filters,
|
||||
paginationConfig,
|
||||
fetchList,
|
||||
handleFilterChange,
|
||||
handleResetFilters,
|
||||
handleTableChange,
|
||||
buildParams
|
||||
} = useTaskList(MixTaskService.getTaskPage)
|
||||
|
||||
// 使用任务操作 Composable
|
||||
const {
|
||||
handleDelete,
|
||||
handleCancel,
|
||||
handleRetry,
|
||||
handleBatchDownload
|
||||
} = useTaskOperations(
|
||||
{
|
||||
deleteApi: MixTaskService.deleteTask,
|
||||
cancelApi: MixTaskService.cancelTask,
|
||||
retryApi: MixTaskService.retryTask,
|
||||
getSignedUrlsApi: MixTaskService.getSignedUrls
|
||||
},
|
||||
// Composables
|
||||
const { loading, list, filters, paginationConfig, fetchList, handleFilterChange, handleResetFilters, handleTableChange } = useTaskList(MixTaskService.getTaskPage)
|
||||
const { handleDelete, handleCancel, handleRetry, handleBatchDownload } = useTaskOperations(
|
||||
{ deleteApi: MixTaskService.deleteTask, cancelApi: MixTaskService.cancelTask, retryApi: MixTaskService.retryTask, getSignedUrlsApi: MixTaskService.getSignedUrls },
|
||||
fetchList
|
||||
)
|
||||
useTaskPolling(MixTaskService.getTaskPage, { onTaskUpdate: fetchList })
|
||||
|
||||
// 预览相关状态
|
||||
const previewVisible = ref(false)
|
||||
const previewUrl = ref('')
|
||||
const previewTitle = ref('')
|
||||
// 展开行
|
||||
const expandedRowKeys = ref([])
|
||||
const handleExpandedRowsChange = (keys) => { expandedRowKeys.value = keys }
|
||||
|
||||
// 预览单个视频
|
||||
const handlePreviewSingle = async (record, index) => {
|
||||
// 预览状态
|
||||
const preview = reactive({ visible: false, title: '', url: '' })
|
||||
|
||||
// 状态判断
|
||||
const isStatus = (status, target) => status === target || status === target.toUpperCase()
|
||||
const canOperate = (record, action) => {
|
||||
const isSuccess = isStatus(record.status, 'success')
|
||||
const hasUrls = record.outputUrls?.length > 0
|
||||
const actions = {
|
||||
preview: isSuccess && hasUrls,
|
||||
download: isSuccess && hasUrls,
|
||||
cancel: isStatus(record.status, 'running'),
|
||||
retry: isStatus(record.status, 'failed')
|
||||
}
|
||||
return actions[action]
|
||||
}
|
||||
|
||||
// 预览视频
|
||||
const previewVideo = async (record, index) => {
|
||||
preview.title = `${record.title} - 视频 ${index + 1}`
|
||||
preview.visible = true
|
||||
preview.url = ''
|
||||
try {
|
||||
previewTitle.value = `${record.title} - 视频 ${index + 1}`
|
||||
previewVisible.value = true
|
||||
previewUrl.value = ''
|
||||
|
||||
// 获取签名URL
|
||||
const res = await MixTaskService.getSignedUrls(record.id)
|
||||
if (res.code === 0 && res.data && res.data[index]) {
|
||||
previewUrl.value = res.data[index]
|
||||
} else {
|
||||
console.warn('获取预览链接失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取预览链接失败:', error)
|
||||
if (res.code === 0 && res.data?.[index]) preview.url = res.data[index]
|
||||
} catch (e) {
|
||||
console.error('获取预览链接失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 下载单个视频
|
||||
const handleDownloadSingle = async (taskId, index) => {
|
||||
const openPreview = (record) => previewVideo(record, 0)
|
||||
|
||||
// 下载视频
|
||||
const downloadVideo = async (taskId, index) => {
|
||||
try {
|
||||
const res = await MixTaskService.getSignedUrls(taskId)
|
||||
if (res.code === 0 && res.data && res.data[index]) {
|
||||
if (res.code === 0 && res.data?.[index]) {
|
||||
const link = document.createElement('a')
|
||||
link.href = res.data[index]
|
||||
link.download = `video_${taskId}_${index + 1}.mp4`
|
||||
@@ -319,242 +254,112 @@ const handleDownloadSingle = async (taskId, index) => {
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
} else {
|
||||
console.warn('获取下载链接失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取下载链接失败:', error)
|
||||
} catch (e) {
|
||||
console.error('获取下载链接失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 预览任务(主列表)
|
||||
const openPreviewModal = async (record) => {
|
||||
await handlePreviewSingle(record, 0)
|
||||
}
|
||||
|
||||
// 下载任务
|
||||
const handleDownload = async (record) => {
|
||||
if (record.outputUrls && record.outputUrls.length > 0) {
|
||||
await handleBatchDownload(
|
||||
[],
|
||||
MixTaskService.getSignedUrls,
|
||||
record.id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 使用轮询 Composable
|
||||
useTaskPolling(MixTaskService.getTaskPage, {
|
||||
onTaskUpdate: () => {
|
||||
fetchList()
|
||||
}
|
||||
})
|
||||
|
||||
// 扩展行键
|
||||
const expandedRowKeys = ref([])
|
||||
|
||||
// 处理展开行变化
|
||||
const handleExpandedRowsChange = (keys) => {
|
||||
expandedRowKeys.value = keys
|
||||
const handleDownload = (record) => {
|
||||
if (record.outputUrls?.length) handleBatchDownload([], MixTaskService.getSignedUrls, record.id)
|
||||
}
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 70,
|
||||
fixed: 'left'
|
||||
},
|
||||
{
|
||||
title: '标题',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 90
|
||||
},
|
||||
{
|
||||
title: '生成结果',
|
||||
dataIndex: 'outputUrls',
|
||||
key: 'outputUrls',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
title: '完成时间',
|
||||
dataIndex: 'finishTime',
|
||||
key: 'finishTime',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 240,
|
||||
fixed: 'right'
|
||||
}
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 70, fixed: 'left' },
|
||||
{ title: '标题', dataIndex: 'title', key: 'title', ellipsis: true },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 90 },
|
||||
{ title: '生成结果', dataIndex: 'outputUrls', key: 'outputUrls', width: 100 },
|
||||
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 160 },
|
||||
{ title: '完成时间', dataIndex: 'finishTime', key: 'finishTime', width: 160 },
|
||||
{ title: '操作', key: 'actions', width: 240, fixed: 'right' }
|
||||
]
|
||||
|
||||
// 状态映射函数
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
pending: '待处理',
|
||||
running: '处理中',
|
||||
success: '已完成',
|
||||
failed: '失败'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
const colorMap = {
|
||||
pending: 'default',
|
||||
running: 'processing',
|
||||
success: 'success',
|
||||
failed: 'error'
|
||||
}
|
||||
return colorMap[status] || 'default'
|
||||
}
|
||||
|
||||
const getProgressStatus = (status) => {
|
||||
const statusMap = {
|
||||
pending: 'normal',
|
||||
running: 'active',
|
||||
success: 'success',
|
||||
failed: 'exception',
|
||||
// 大写状态支持
|
||||
PENDING: 'normal',
|
||||
RUNNING: 'active',
|
||||
SUCCESS: 'success',
|
||||
FAILED: 'exception'
|
||||
}
|
||||
return statusMap[status] || 'normal'
|
||||
}
|
||||
|
||||
// 检查状态(同时支持大小写)
|
||||
const isStatus = (status, targetStatus) => {
|
||||
return status === targetStatus || status === targetStatus.toUpperCase()
|
||||
}
|
||||
|
||||
// 删除未使用的方法
|
||||
// handleDownloadSignedUrl 和 handleDownloadAll 已被移除
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchList()
|
||||
})
|
||||
onMounted(fetchList)
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.mix-task-page {
|
||||
padding: 0 var(--space-3);
|
||||
.task-page {
|
||||
padding: var(--space-4);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
&__filters {
|
||||
padding: var(--space-3);
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-card);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
|
||||
.filter-select,
|
||||
.filter-input {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.filter-date-picker {
|
||||
width: 280px;
|
||||
}
|
||||
.task-page__filters {
|
||||
padding: var(--space-4);
|
||||
background: var(--color-bg-card);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
|
||||
.filter-select,
|
||||
.filter-input {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-card);
|
||||
padding: var(--space-3);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
.filter-date-picker {
|
||||
width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 标题单元格 */
|
||||
.task-page__content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background: var(--color-bg-card);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-4);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.title-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
/* 操作按钮样式 */
|
||||
.action-btn-preview {
|
||||
color: var(--color-primary);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary-hover, var(--color-blue-600));
|
||||
}
|
||||
.text-muted {
|
||||
color: var(--color-gray-400);
|
||||
}
|
||||
|
||||
.action-btn-download {
|
||||
color: var(--color-success);
|
||||
|
||||
&:hover {
|
||||
color: #059669;
|
||||
}
|
||||
.action-btn {
|
||||
&--primary { color: var(--color-primary-500); &:hover { color: var(--color-primary-600); } }
|
||||
&--success { color: var(--color-success-500); &:hover { color: var(--color-success-600); } }
|
||||
&--danger { color: var(--color-error-500); &:hover { color: var(--color-error-600); } }
|
||||
}
|
||||
|
||||
.action-btn-delete {
|
||||
color: var(--color-error);
|
||||
|
||||
&:hover {
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
/* 展开内容 */
|
||||
.expanded-content {
|
||||
padding: var(--space-3);
|
||||
background: var(--color-bg-2);
|
||||
border-radius: var(--radius-card);
|
||||
padding: var(--space-4);
|
||||
background: var(--color-gray-50);
|
||||
border-radius: var(--radius-md);
|
||||
margin: var(--space-2);
|
||||
}
|
||||
|
||||
.task-text {
|
||||
margin-bottom: var(--space-3);
|
||||
margin-bottom: var(--space-4);
|
||||
|
||||
p {
|
||||
margin: var(--space-2) 0 0 0;
|
||||
padding: var(--space-2);
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-card);
|
||||
line-height: 1.6;
|
||||
margin: var(--space-2) 0 0;
|
||||
padding: var(--space-3);
|
||||
background: var(--color-gray-100);
|
||||
border-radius: var(--radius-md);
|
||||
line-height: var(--line-height-base);
|
||||
}
|
||||
}
|
||||
|
||||
.task-results {
|
||||
margin-bottom: var(--space-3);
|
||||
margin-bottom: var(--space-4);
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.result-count {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3, #8c8c8c);
|
||||
}
|
||||
.result-count {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-gray-500);
|
||||
}
|
||||
|
||||
.result-list {
|
||||
@@ -562,72 +367,20 @@ onMounted(() => {
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
margin-top: var(--space-2);
|
||||
|
||||
.result-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-1) var(--space-2);
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-card);
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.result-preview-btn {
|
||||
color: var(--color-primary);
|
||||
padding: 0;
|
||||
height: auto;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary-hover, var(--color-blue-600));
|
||||
}
|
||||
}
|
||||
|
||||
.result-download-btn {
|
||||
color: var(--color-success);
|
||||
padding: 0;
|
||||
height: auto;
|
||||
|
||||
&:hover {
|
||||
color: #059669;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.processing-tip {
|
||||
color: var(--color-text-3);
|
||||
font-size: 12px;
|
||||
}
|
||||
.result-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: var(--color-gray-100);
|
||||
border-radius: var(--radius-md);
|
||||
transition: box-shadow var(--duration-fast) var(--ease-out);
|
||||
|
||||
.task-error {
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
/* 确保按钮内的图标和文字对齐 */
|
||||
:deep(.ant-btn .anticon) {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
:deep(.ant-table-tbody > tr > td) {
|
||||
padding: 12px 8px;
|
||||
}
|
||||
|
||||
:deep(.ant-table-thead > tr > th) {
|
||||
background: var(--color-bg-2);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 预览模态框样式 */
|
||||
.preview-modal {
|
||||
:deep(.ant-modal-body) {
|
||||
padding: var(--space-3);
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -638,6 +391,12 @@ onMounted(() => {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.preview-video {
|
||||
width: 100%;
|
||||
max-height: 600px;
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.preview-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -645,5 +404,16 @@ onMounted(() => {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
/* 桌面端样式优化 */
|
||||
:deep(.ant-table-tbody > tr > td) {
|
||||
padding: var(--space-3) var(--space-2);
|
||||
}
|
||||
|
||||
:deep(.ant-table-thead > tr > th) {
|
||||
background: var(--color-gray-50);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
:deep(.ant-btn .anticon) {
|
||||
line-height: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -66,13 +66,14 @@ function maskMobile(mobile) {
|
||||
return mobile.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||||
}
|
||||
|
||||
// 获取积分记录
|
||||
// 获取积分记录(只显示已完成的记录)
|
||||
async function fetchPointRecords() {
|
||||
recordsLoading.value = true
|
||||
try {
|
||||
const res = await getPointRecordPage({
|
||||
pageNo: recordsPagination.current,
|
||||
pageSize: recordsPagination.pageSize
|
||||
pageSize: recordsPagination.pageSize,
|
||||
status: 'confirmed'
|
||||
})
|
||||
if (res.data) {
|
||||
pointRecords.value = res.data.list || []
|
||||
@@ -86,9 +87,9 @@ async function fetchPointRecords() {
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
function handleTableChange(pagination) {
|
||||
recordsPagination.current = pagination.current
|
||||
recordsPagination.pageSize = pagination.pageSize
|
||||
function handleTableChange(page, pageSize) {
|
||||
recordsPagination.current = page
|
||||
recordsPagination.pageSize = pageSize
|
||||
fetchPointRecords()
|
||||
}
|
||||
|
||||
@@ -112,7 +113,11 @@ function getBizTypeName(bizType) {
|
||||
'exchange': '兑换',
|
||||
'admin': '后台调整',
|
||||
'gift': '礼包赠送',
|
||||
'digital_human': '数字人生成'
|
||||
'dify_chat': 'AI文案',
|
||||
'digital_human': '数字人',
|
||||
'voice_tts': '语音克隆',
|
||||
'tikhub_fetch': '数据采集',
|
||||
'forecast_rewrite': '文案改写'
|
||||
}
|
||||
return typeMap[bizType] || bizType || '其他'
|
||||
}
|
||||
@@ -216,7 +221,7 @@ onMounted(async () => {
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">剩余积分</div>
|
||||
<div class="stat-value">{{ formatCredits(userStore.remainingPoints) }}</div>
|
||||
<div class="stat-desc">用于AI生成消耗</div>
|
||||
<div class="stat-desc">用于生成消耗</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
@@ -258,16 +263,12 @@ onMounted(async () => {
|
||||
</template>
|
||||
<template #title>
|
||||
<div class="record-title">
|
||||
<span class="record-reason">{{ item.reason || getBizTypeName(item.bizType) }}</span>
|
||||
<a-tag :color="getStatusInfo(item.status).color" size="small">
|
||||
{{ getStatusInfo(item.status).text }}
|
||||
</a-tag>
|
||||
<span class="record-reason">{{ getBizTypeName(item.bizType) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="record-desc">
|
||||
<span>{{ formatRecordTime(item.createTime) }}</span>
|
||||
<span v-if="item.bizType" class="record-biz-type">{{ getBizTypeName(item.bizType) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
|
||||
@@ -34,12 +34,14 @@ public class SiliconFlowImageOptions implements ImageOptions {
|
||||
* The number of images to generate. Must be between 1 and 4.
|
||||
*/
|
||||
@JsonProperty("batch_size")
|
||||
@Builder.Default
|
||||
private Integer batchSize = 1;
|
||||
|
||||
/**
|
||||
* number of inference steps
|
||||
*/
|
||||
@JsonProperty("num_inference_steps")
|
||||
@Builder.Default
|
||||
private Integer numInferenceSteps = 25;
|
||||
|
||||
/**
|
||||
@@ -48,6 +50,7 @@ public class SiliconFlowImageOptions implements ImageOptions {
|
||||
* Required range: 0 <= x <= 20
|
||||
*/
|
||||
@JsonProperty("guidance_scale")
|
||||
@Builder.Default
|
||||
private Float guidanceScale = 0.75F;
|
||||
|
||||
/**
|
||||
@@ -55,7 +58,8 @@ public class SiliconFlowImageOptions implements ImageOptions {
|
||||
*
|
||||
*/
|
||||
@JsonProperty("seed")
|
||||
private Integer seed = (int)(Math.random() * 1_000_000_000);
|
||||
@Builder.Default
|
||||
private Integer seed = (int) (Math.random() * 1_000_000_000);
|
||||
|
||||
/**
|
||||
* The image that needs to be uploaded should be converted into base64 format.
|
||||
|
||||
@@ -265,6 +265,7 @@ public class DifyClient {
|
||||
return WebClient.builder()
|
||||
.baseUrl(url)
|
||||
.defaultHeader("Authorization", "Bearer " + apiKey)
|
||||
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(16 * 1024 * 1024))
|
||||
.build()
|
||||
.get()
|
||||
.retrieve()
|
||||
|
||||
@@ -2,25 +2,25 @@ package cn.iocoder.yudao.module.tik.muye.aiagent;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.yudao.module.tik.muye.aiagent.dal.AiAgentDO;
|
||||
import cn.iocoder.yudao.module.tik.muye.aiagent.service.AiAgentFavoriteService;
|
||||
import cn.iocoder.yudao.module.tik.muye.aiagent.service.AiAgentService;
|
||||
import cn.iocoder.yudao.module.tik.muye.aiagent.vo.AppAiAgentRespVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 用户 App - AI智能体
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Tag(name = "用户 App - AI智能体")
|
||||
@RestController
|
||||
@@ -31,11 +31,43 @@ public class AppAiAgentController {
|
||||
@Resource
|
||||
private AiAgentService aiAgentService;
|
||||
|
||||
@Resource
|
||||
private AiAgentFavoriteService aiAgentFavoriteService;
|
||||
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "获取启用的智能体列表")
|
||||
public CommonResult<List<AppAiAgentRespVO>> getEnabledAgentList() {
|
||||
// 获取智能体列表
|
||||
List<AiAgentDO> list = aiAgentService.getEnabledAgentList();
|
||||
return success(BeanUtils.toBean(list, AppAiAgentRespVO.class));
|
||||
List<AppAiAgentRespVO> voList = BeanUtils.toBean(list, AppAiAgentRespVO.class);
|
||||
|
||||
// 获取当前用户收藏的智能体ID
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
if (userId != null) {
|
||||
Set<Long> favoriteIds = aiAgentFavoriteService.getFavoriteAgentIds(userId);
|
||||
voList.forEach(vo -> vo.setIsFavorite(favoriteIds.contains(vo.getId())));
|
||||
} else {
|
||||
voList.forEach(vo -> vo.setIsFavorite(false));
|
||||
}
|
||||
|
||||
return success(voList);
|
||||
}
|
||||
|
||||
@PostMapping("/favorite/create")
|
||||
@Operation(summary = "添加收藏")
|
||||
@Parameter(name = "agentId", description = "智能体ID", required = true)
|
||||
public CommonResult<Long> createFavorite(@RequestParam("agentId") Long agentId) {
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
return success(aiAgentFavoriteService.createFavorite(userId, agentId));
|
||||
}
|
||||
|
||||
@DeleteMapping("/favorite/delete")
|
||||
@Operation(summary = "取消收藏")
|
||||
@Parameter(name = "agentId", description = "智能体ID", required = true)
|
||||
public CommonResult<Boolean> deleteFavorite(@RequestParam("agentId") Long agentId) {
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
aiAgentFavoriteService.deleteFavorite(userId, agentId);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package cn.iocoder.yudao.module.tik.muye.aiagent.dal;
|
||||
|
||||
import lombok.*;
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
|
||||
|
||||
/**
|
||||
* AI智能体收藏 DO
|
||||
*/
|
||||
@TenantIgnore
|
||||
@TableName("muye_ai_agent_favorite")
|
||||
@KeySequence("muye_ai_agent_favorite_seq")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AiAgentFavoriteDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
/**
|
||||
* 智能体ID
|
||||
*/
|
||||
private Long agentId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package cn.iocoder.yudao.module.tik.muye.aiagent.mapper;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.module.tik.muye.aiagent.dal.AiAgentFavoriteDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface AiAgentFavoriteMapper extends BaseMapperX<AiAgentFavoriteDO> {
|
||||
|
||||
default AiAgentFavoriteDO selectByUserIdAndAgentId(Long userId, Long agentId) {
|
||||
return selectOne(AiAgentFavoriteDO::getUserId, userId,
|
||||
AiAgentFavoriteDO::getAgentId, agentId);
|
||||
}
|
||||
|
||||
default List<AiAgentFavoriteDO> selectListByUserId(Long userId) {
|
||||
return selectList(AiAgentFavoriteDO::getUserId, userId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package cn.iocoder.yudao.module.tik.muye.aiagent.service;
|
||||
|
||||
import cn.iocoder.yudao.module.tik.muye.aiagent.dal.AiAgentFavoriteDO;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* AI智能体收藏 Service 接口
|
||||
*/
|
||||
public interface AiAgentFavoriteService {
|
||||
|
||||
/**
|
||||
* 添加收藏
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param agentId 智能体ID
|
||||
* @return 收藏ID
|
||||
*/
|
||||
Long createFavorite(Long userId, Long agentId);
|
||||
|
||||
/**
|
||||
* 取消收藏
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
void deleteFavorite(Long userId, Long agentId);
|
||||
|
||||
/**
|
||||
* 获取用户收藏的智能体ID列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 智能体ID集合
|
||||
*/
|
||||
Set<Long> getFavoriteAgentIds(Long userId);
|
||||
|
||||
/**
|
||||
* 判断是否已收藏
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param agentId 智能体ID
|
||||
* @return 是否已收藏
|
||||
*/
|
||||
boolean isFavorite(Long userId, Long agentId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package cn.iocoder.yudao.module.tik.muye.aiagent.service;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
|
||||
import cn.iocoder.yudao.module.tik.muye.aiagent.dal.AiAgentFavoriteDO;
|
||||
import cn.iocoder.yudao.module.tik.muye.aiagent.mapper.AiAgentFavoriteMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
|
||||
/**
|
||||
* AI智能体收藏 Service 实现类
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
public class AiAgentFavoriteServiceImpl implements AiAgentFavoriteService {
|
||||
|
||||
private static final ErrorCode FAVORITE_NOT_EXISTS = new ErrorCode(1_013_001_001, "未收藏该智能体");
|
||||
|
||||
@Resource
|
||||
private AiAgentFavoriteMapper aiAgentFavoriteMapper;
|
||||
|
||||
@Override
|
||||
public Long createFavorite(Long userId, Long agentId) {
|
||||
// 检查是否已收藏(幂等:已存在则直接返回)
|
||||
AiAgentFavoriteDO existFavorite = aiAgentFavoriteMapper.selectByUserIdAndAgentId(userId, agentId);
|
||||
if (existFavorite != null) {
|
||||
return existFavorite.getId();
|
||||
}
|
||||
|
||||
// 创建收藏
|
||||
AiAgentFavoriteDO favorite = AiAgentFavoriteDO.builder()
|
||||
.userId(userId)
|
||||
.agentId(agentId)
|
||||
.build();
|
||||
aiAgentFavoriteMapper.insert(favorite);
|
||||
return favorite.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteFavorite(Long userId, Long agentId) {
|
||||
AiAgentFavoriteDO favorite = aiAgentFavoriteMapper.selectByUserIdAndAgentId(userId, agentId);
|
||||
if (favorite == null) {
|
||||
throw exception(FAVORITE_NOT_EXISTS);
|
||||
}
|
||||
aiAgentFavoriteMapper.deleteById(favorite.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Long> getFavoriteAgentIds(Long userId) {
|
||||
List<AiAgentFavoriteDO> favorites = aiAgentFavoriteMapper.selectListByUserId(userId);
|
||||
return favorites.stream()
|
||||
.map(AiAgentFavoriteDO::getAgentId)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFavorite(Long userId, Long agentId) {
|
||||
return aiAgentFavoriteMapper.selectByUserIdAndAgentId(userId, agentId) != null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,4 +29,7 @@ public class AppAiAgentRespVO {
|
||||
@Schema(description = "分类名称(中文)", example = "文案创作")
|
||||
private String categoryName;
|
||||
|
||||
@Schema(description = "是否已收藏", example = "true")
|
||||
private Boolean isFavorite;
|
||||
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ public interface PointRecordMapper extends BaseMapperX<PointRecordDO> {
|
||||
.eqIfPresent(PointRecordDO::getBizType, reqVO.getBizType())
|
||||
.eqIfPresent(PointRecordDO::getBizId, reqVO.getBizId())
|
||||
.eqIfPresent(PointRecordDO::getRemark, reqVO.getRemark())
|
||||
.eqIfPresent(PointRecordDO::getStatus, reqVO.getStatus())
|
||||
.betweenIfPresent(PointRecordDO::getCreateTime, reqVO.getCreateTime())
|
||||
.orderByDesc(PointRecordDO::getId));
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@ public class PointRecordPageReqVO extends PageParam {
|
||||
@Schema(description = "备注", example = "你猜")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "状态:pending-预扣 confirmed-已确认 canceled-已取消")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] createTime;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
server:
|
||||
port: 48080
|
||||
port: 9900
|
||||
|
||||
--- #################### 数据库相关配置 ####################
|
||||
|
||||
@@ -47,22 +47,22 @@ spring:
|
||||
primary: master
|
||||
datasource:
|
||||
master:
|
||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
|
||||
username: root
|
||||
password: 123456
|
||||
slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改
|
||||
url: jdbc:mysql://127.0.0.1:3306/sion_rui_dev?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
||||
username: sion_rui_dev
|
||||
password: 4w8GaSMmcihjzxkz
|
||||
slave: # 模拟从库,可根据自己需要修改
|
||||
lazy: true # 开启懒加载,保证启动速度
|
||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
|
||||
username: root
|
||||
password: 123456
|
||||
url: jdbc:mysql://127.0.0.1:3306/sion_rui_dev?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
||||
username: sion_rui_dev
|
||||
password: 4w8GaSMmcihjzxkz
|
||||
|
||||
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
|
||||
data:
|
||||
redis:
|
||||
host: 400-infra.server.iocoder.cn # 地址
|
||||
host: 127.0.0.1 # 地址
|
||||
port: 6379 # 端口
|
||||
database: 1 # 数据库索引
|
||||
# password: 123456 # 密码,建议生产环境开启
|
||||
database: 0 # 数据库索引
|
||||
password: sion+Rui!$ # 密码
|
||||
|
||||
--- #################### 定时任务相关配置 ####################
|
||||
|
||||
@@ -106,8 +106,8 @@ spring:
|
||||
rabbitmq:
|
||||
host: 127.0.0.1 # RabbitMQ 服务的地址
|
||||
port: 5672 # RabbitMQ 服务的端口
|
||||
username: guest # RabbitMQ 服务的账号
|
||||
password: guest # RabbitMQ 服务的密码
|
||||
username: rabbit # RabbitMQ 服务的账号
|
||||
password: rabbit # RabbitMQ 服务的密码
|
||||
# Kafka 配置项,对应 KafkaProperties 配置类
|
||||
kafka:
|
||||
bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔
|
||||
@@ -145,21 +145,45 @@ spring:
|
||||
logging:
|
||||
file:
|
||||
name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径
|
||||
level:
|
||||
# 配置自己写的 MyBatis Mapper 打印日志
|
||||
cn.iocoder.yudao.module.bpm.dal.mysql: debug
|
||||
cn.iocoder.yudao.module.infra.dal.mysql: debug
|
||||
cn.iocoder.yudao.module.infra.dal.mysql.logger.ApiErrorLogMapper: INFO # 配置 ApiErrorLogMapper 的日志级别为 info,避免和 GlobalExceptionHandler 重复打印
|
||||
cn.iocoder.yudao.module.infra.dal.mysql.job.JobLogMapper: INFO # 配置 JobLogMapper 的日志级别为 info
|
||||
cn.iocoder.yudao.module.infra.dal.mysql.file.FileConfigMapper: INFO # 配置 FileConfigMapper 的日志级别为 info
|
||||
cn.iocoder.yudao.module.pay.dal.mysql: debug
|
||||
cn.iocoder.yudao.module.pay.dal.mysql.notify.PayNotifyTaskMapper: INFO # 配置 PayNotifyTaskMapper 的日志级别为 info
|
||||
cn.iocoder.yudao.module.system.dal.mysql: debug
|
||||
cn.iocoder.yudao.module.system.dal.mysql.sms.SmsChannelMapper: INFO # 配置 SmsChannelMapper 的日志级别为 info
|
||||
cn.iocoder.yudao.module.tool.dal.mysql: debug
|
||||
cn.iocoder.yudao.module.member.dal.mysql: debug
|
||||
cn.iocoder.yudao.module.trade.dal.mysql: debug
|
||||
cn.iocoder.yudao.module.promotion.dal.mysql: debug
|
||||
cn.iocoder.yudao.module.statistics.dal.mysql: debug
|
||||
cn.iocoder.yudao.module.crm.dal.mysql: debug
|
||||
cn.iocoder.yudao.module.erp.dal.mysql: debug
|
||||
cn.iocoder.yudao.module.iot.dal.mysql: debug
|
||||
cn.iocoder.yudao.module.iot.dal.tdengine: DEBUG
|
||||
cn.iocoder.yudao.module.iot.service.rule: debug
|
||||
cn.iocoder.yudao.module.ai.dal.mysql: debug
|
||||
org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO 芋艿:先禁用,Spring Boot 3.X 存在部分错误的 WARN 提示
|
||||
|
||||
--- #################### 微信公众号相关配置 ####################
|
||||
wx: # 参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md 文档
|
||||
mp:
|
||||
# 公众号配置(必填)
|
||||
app-id: wx041349c6f39b268b
|
||||
secret: 5abee519483bc9f8cb37ce280e814bd0
|
||||
debug: false
|
||||
|
||||
--- #################### 微信公众号、小程序相关配置 ####################
|
||||
wx:
|
||||
mp: # 公众号配置(必填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md 文档
|
||||
app-id: wxf56b1542b9e85f8a # 测试号(Kongdy 提供的)
|
||||
secret: 496379dcef1ba869e9234de8d598cfd3
|
||||
# 存储配置,解决 AccessToken 的跨节点的共享
|
||||
config-storage:
|
||||
type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取
|
||||
key-prefix: wx # Redis Key 的前缀
|
||||
http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台
|
||||
miniapp: # 小程序配置(必填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-miniapp-spring-boot-starter/README.md 文档
|
||||
appid: wx63c280fe3248a3e7
|
||||
secret: 6f270509224a7ae1296bbf1c8cb97aed
|
||||
appid: wxc4598c446f8a9cb3 # 测试号(Kongdy 提供的)
|
||||
secret: 4a1a04e07f6a4a0751b39c3064a92c8b
|
||||
config-storage:
|
||||
type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取
|
||||
key-prefix: wa # Redis Key 的前缀
|
||||
@@ -169,11 +193,34 @@ wx: # 参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-sta
|
||||
|
||||
# 芋道配置项,设置当前项目所有自定义的配置
|
||||
yudao:
|
||||
voice:
|
||||
default-provider: siliconflow
|
||||
siliconflow:
|
||||
enabled: true
|
||||
api-key: sk-kcvifijrafkzxsmnxbgxspnxdvjiaawcbyoiqhmfobykynpx
|
||||
base-url: https://api.siliconflow.cn
|
||||
default-model: IndexTeam/IndexTTS-2
|
||||
ice:
|
||||
access-key-id: LTAI5tPV9Ag3csf41GZjaLTA
|
||||
access-key-secret: kDqlGeJTKw6tJtFYiaY8vQTFuVIQDs
|
||||
region-id: cn-hangzhou
|
||||
bucket: muye-ai-chat
|
||||
enabled: true
|
||||
captcha:
|
||||
enable: false # 关闭图片验证码,方便登录等接口的测试
|
||||
security:
|
||||
mock-enable: true
|
||||
access-log:
|
||||
enable: false
|
||||
pay:
|
||||
order-notify-url: http://8.155.172.147/admin-api/pay/notify/order # 支付渠道的【支付】回调地址
|
||||
refund-notify-url: http://8.155.172.147/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址
|
||||
transfer-notify-url: https://8.155.172.147/admin-api/pay/notify/transfer # 支付渠道的【转账】回调地址
|
||||
demo: false # 开启演示模式
|
||||
demo: false # 关闭演示模式
|
||||
wxa-code:
|
||||
env-version: develop # 小程序版本: 正式版为 "release";体验版为 "trial";开发版为 "develop"
|
||||
wxa-subscribe-message:
|
||||
miniprogram-state: developer # 跳转小程序类型:开发版为 "developer";体验版为 "trial"为;正式版为 "formal"
|
||||
tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc
|
||||
|
||||
justauth:
|
||||
|
||||
@@ -269,7 +269,7 @@ yudao:
|
||||
bucket: muye-ai-chat
|
||||
enabled: true
|
||||
dify:
|
||||
api-url: http://8.155.172.147:8088 # Dify API 地址,请根据实际情况修改
|
||||
api-url: http://127.0.0.1:8088 # Dify API 地址,请根据实际情况修改
|
||||
timeout: 240 # 请求超时时间(秒)
|
||||
--- #################### 芋道相关配置 ####################
|
||||
|
||||
|
||||
Reference in New Issue
Block a user