Compare commits

...

10 Commits

Author SHA1 Message Date
00d60b78c4 优化 2026-03-01 17:10:41 +08:00
c121f03ad1 优化 2026-02-26 22:05:39 +08:00
3bbb28677b feat(point-record): add status filter for point record queries
- Change API endpoint from APP_MEMBER to APP_TIK for point record requests
- Add status parameter to filter only confirmed records in frontend
- Update fetchPointRecords function to pass status: 'confirmed' by default
- Modify handleTableChange function signature to accept page and pageSize directly
- Add new business type mappings for AI-related services (dify_chat, voice_tts, tikhub_fetch, forecast_rewrite)
- Remove redundant reason display and status tag from record items
- Add status field to backend query conditions in PointRecordMapper
- Include status parameter in PointRecordPageReqVO with proper schema documentation
2026-02-26 21:41:55 +08:00
9c4d39e29d 优化 2026-02-26 21:22:32 +08:00
6ec2a0aa6c 优化 2026-02-26 21:04:33 +08:00
1e5a1d422b refactor(TaskStatusTag): replace a-tag with span element and improve status configuration
- Replace a-tag component with semantic span element for better accessibility
- Introduce centralized STATUS_CONFIG object for consistent status mapping
- Add isRunning computed property for cleaner conditional logic
- Remove redundant statusMap handling and normalize status values
- Add proper CSS class bindings for styling consistency
- Update component structure to use
2026-02-26 20:45:51 +08:00
72fa2c63a1 feat: 优化 2026-02-26 20:27:41 +08:00
2d96e8ca4e refactor(agent): 优化收藏功能代码实现
- 前端移除未使用的 ArrowRightOutlined 导入
- 优化智能体列表排序逻辑,避免修改原数组
- 后端使用 Stream API 简化收藏智能体 ID 获取逻辑
2026-02-26 20:17:00 +08:00
d429dc887a feat: add agent favorite functionality with UI and API implementation
- Add `addFavorite` and `removeFavorite` API functions in agent.js
- Implement favorite button UI in Agents.vue with star icons
- Add login check before favorite operations using token manager
- Sort agents with favorites appearing at the top of the list
- Include success/error messages for user feedback
- Add backend API endpoints for creating/deleting agent favorites
- Update agent list response to include favorite status
- Style favorite button with hover and active states
2026-02-26 20:15:24 +08:00
120a4529a5 feat: 功能优化 2026-02-26 20:04:09 +08:00
26 changed files with 1630 additions and 1664 deletions

View File

@@ -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>
```
---
> **迁移完成后**,项目将拥有统一的设计语言,后续开发效率将显著提升。

View File

@@ -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 }
})
}

View File

@@ -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 })
}

View File

@@ -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

View File

@@ -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);
}
// ============================================
// 空状态
// ============================================

View File

@@ -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>

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,

View File

@@ -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>

View File

@@ -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>

View File

@@ -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.

View File

@@ -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()

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -29,4 +29,7 @@ public class AppAiAgentRespVO {
@Schema(description = "分类名称(中文)", example = "文案创作")
private String categoryName;
@Schema(description = "是否已收藏", example = "true")
private Boolean isFavorite;
}

View File

@@ -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));
}

View File

@@ -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;

View File

@@ -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:

View File

@@ -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 # 请求超时时间(秒)
--- #################### 芋道相关配置 ####################