Compare commits
155 Commits
master
...
seedance_b
| Author | SHA1 | Date |
|---|---|---|
|
|
97a91a39e2 | |
|
|
413a3b118a | |
|
|
24f518acd1 | |
|
|
e1733e3f47 | |
|
|
f7d40414c0 | |
|
|
9eb8514fdf | |
|
|
ba675ce0e0 | |
|
|
63f251de66 | |
|
|
d578280f82 | |
|
|
0a714b1322 | |
|
|
24c6936dd0 | |
|
|
742310844d | |
|
|
b1707c8d20 | |
|
|
abbd589b82 | |
|
|
fb38439543 | |
|
|
28e51137c5 | |
|
|
7b4395761c | |
|
|
e1a574d99e | |
|
|
8fcdb74507 | |
|
|
4800af812d | |
|
|
fd2ed863b3 | |
|
|
181228bb59 | |
|
|
bbcc14a395 | |
|
|
5c4c62d729 | |
|
|
0385ac9640 | |
|
|
9ffe5c8152 | |
|
|
20a063d6aa | |
|
|
e516105893 | |
|
|
8d7cd70cfc | |
|
|
2498b4deed | |
|
|
589bb9c7c8 | |
|
|
cb72d4edae | |
|
|
b2f1bc8439 | |
|
|
1a7205564d | |
|
|
d1b27c42cd | |
|
|
a3bc2e24da | |
|
|
b3b54f14bc | |
|
|
56389e5154 | |
|
|
7177d7c404 | |
|
|
009886d9d8 | |
|
|
6600f4f1f9 | |
|
|
690bc43a55 | |
|
|
4a55202d35 | |
|
|
bfa30bb543 | |
|
|
c7334ab7d9 | |
|
|
f98ceb5785 | |
|
|
e4292ee87d | |
|
|
17d5e3a1c9 | |
|
|
1ae99ae660 | |
|
|
ddd2a60f7e | |
|
|
8be42f7a4f | |
|
|
460cbc2a38 | |
|
|
db2052b9fd | |
|
|
c92f531f1b | |
|
|
580e484dd0 | |
|
|
d69d2a1df7 | |
|
|
a275194cfb | |
|
|
dba0b2d9ca | |
|
|
d975e4fc19 | |
|
|
5eaa4fe6df | |
|
|
edbebf0d5d | |
|
|
4f1240ab0b | |
|
|
391071c8f7 | |
|
|
ffc23990a7 | |
|
|
b3459fc01f | |
|
|
3dfa25d23b | |
|
|
abae79949f | |
|
|
ff46210c14 | |
|
|
b93dd9470c | |
|
|
a26021dc1c | |
|
|
e93fa6baa9 | |
|
|
1bedde7fa7 | |
|
|
f31e4c38c6 | |
|
|
fedaef2ff7 | |
|
|
b9e08938e0 | |
|
|
85dc61b07d | |
|
|
3328aae4e3 | |
|
|
73f4fcdaf6 | |
|
|
f1e09eac1a | |
|
|
b7d15052cd | |
|
|
f6408400ee | |
|
|
7068fad278 | |
|
|
dda43f2de6 | |
|
|
4d428b9d94 | |
|
|
982ce1f179 | |
|
|
6279abb4f3 | |
|
|
a67a8e202a | |
|
|
dbc292b058 | |
|
|
bb5daf02be | |
|
|
992d95992a | |
|
|
f53f849ec8 | |
|
|
80c136ad18 | |
|
|
38b26f9ffb | |
|
|
d807f71677 | |
|
|
2e38bd7539 | |
|
|
ae3be9b48d | |
|
|
b567ba14b7 | |
|
|
b780a4e0d3 | |
|
|
23817f7850 | |
|
|
a2076fe18b | |
|
|
9250253238 | |
|
|
6037bae970 | |
|
|
249565ef6d | |
|
|
c2c1ff8063 | |
|
|
eb8cab3f5a | |
|
|
fa99a4fb9b | |
|
|
b17aec4a1a | |
|
|
78b32dd252 | |
|
|
0b21a293b0 | |
|
|
37f3237f61 | |
|
|
4ea4d009a6 | |
|
|
dc56530d47 | |
|
|
9f7e43a21a | |
|
|
f3c3e78901 | |
|
|
353d8e0069 | |
|
|
67c252d174 | |
|
|
9a460e2de7 | |
|
|
96646cf5ed | |
|
|
e888da137e | |
|
|
e2ab5bcb13 | |
|
|
9c0f533aff | |
|
|
9705c4f472 | |
|
|
0d4b1d18be | |
|
|
eb5211b9e4 | |
|
|
aec9dc0152 | |
|
|
089ffca4f5 | |
|
|
8faff7562d | |
|
|
2d026feba3 | |
|
|
f075e6b2b1 | |
|
|
d2c1fff13f | |
|
|
a347873a4e | |
|
|
3ffd78f928 | |
|
|
2a91a33825 | |
|
|
b13e51a6ca | |
|
|
0f6695207e | |
|
|
afca767422 | |
|
|
c04c1e572b | |
|
|
ca2989902c | |
|
|
ff18ca4f51 | |
|
|
f9c12bb1e0 | |
|
|
a04bd71afc | |
|
|
4cb3f4eb88 | |
|
|
1edf9601c0 | |
|
|
adcfea0d3c | |
|
|
33a10f55a5 | |
|
|
55588504a1 | |
|
|
d69d16e196 | |
|
|
5ba6c9f746 | |
|
|
0e94beb477 | |
|
|
5ae8614b1d | |
|
|
4bba35a426 | |
|
|
abb8d279c4 | |
|
|
8959b775a4 | |
|
|
aed52b0437 | |
|
|
809b47215c |
|
|
@ -0,0 +1,29 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules/
|
||||||
|
unpackage
|
||||||
|
/dist/
|
||||||
|
/manager/dist/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
/test/unit/coverage/
|
||||||
|
/test/e2e/reports/
|
||||||
|
selenium-debug.log
|
||||||
|
nodejs/public/uploads
|
||||||
|
*.zip
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
package-lock.json
|
||||||
|
.history
|
||||||
|
/unpackage/resources/
|
||||||
|
/unpackage/dist/dev
|
||||||
|
/unpackage/dist/build/.automator
|
||||||
|
/unpackage/dist/build/app-plus
|
||||||
|
*.lock
|
||||||
|
/.logs/*
|
||||||
|
/.cursor/*
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/python" />
|
||||||
|
<excludePattern pattern="*.zip" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
|
|
||||||
|
|
@ -5,4 +5,7 @@ VUE_APP_TITLE = 管理系统
|
||||||
ENV = 'production'
|
ENV = 'production'
|
||||||
|
|
||||||
# 若依管理系统/生产环境
|
# 若依管理系统/生产环境
|
||||||
VUE_APP_BASE_API = 'https://admin-api.undressing.name'
|
# VUE_APP_BASE_API = 'http://111.230.37.169:10009'
|
||||||
|
# VUE_APP_BASE_API = 'http://101.96.201.225:8011'
|
||||||
|
VUE_APP_BASE_API = 'http://47.86.170.114:8011'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,4 @@ NODE_ENV = production
|
||||||
# 测试环境配置
|
# 测试环境配置
|
||||||
ENV = 'staging'
|
ENV = 'staging'
|
||||||
|
|
||||||
# 若依管理系统/测试环境
|
VUE_APP_BASE_API = 'http://101.96.201.225:8011'
|
||||||
VUE_APP_BASE_API = '/api'
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ yarn-error.log*
|
||||||
tests/**/coverage/
|
tests/**/coverage/
|
||||||
tests/e2e/reports
|
tests/e2e/reports
|
||||||
selenium-debug.log
|
selenium-debug.log
|
||||||
|
*.zip
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.idea
|
.idea
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,44 @@
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 查询团队(部门)对应火山引擎配置列表
|
||||||
|
export function listConfig(query) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/config/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询团队(部门)对应火山引擎配置详细
|
||||||
|
export function getConfig(id) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/config/' + id,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增团队(部门)对应火山引擎配置
|
||||||
|
export function addConfig(data) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/config',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改团队(部门)对应火山引擎配置
|
||||||
|
export function updateConfig(data) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/config',
|
||||||
|
method: 'put',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除团队(部门)对应火山引擎配置
|
||||||
|
export function delConfig(id) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/config/' + id,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 查询团队每日消耗统计
|
||||||
|
export function listData(query) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/data/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export function listDept(query) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/dept/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listDeptExcludeChild(deptId) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/dept/list/exclude/' + deptId,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDept(deptId) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/dept/' + deptId,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addDept(data) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/dept',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateDept(data) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/dept',
|
||||||
|
method: 'put',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function delDept(deptId) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/dept/' + deptId,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function chargeRefundDept(data) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/dept/charge-refund',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 查询团队(部门)充值退款订单列表
|
||||||
|
export function listGroupChargeOrder(query) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/group/chargeorder/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询团队(部门)充值退款订单详细
|
||||||
|
export function getGroupChargeOrder(id) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/group/chargeorder/' + id,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增团队(部门)充值退款订单
|
||||||
|
export function addGroupChargeOrder(data) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/group/chargeorder',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改团队(部门)充值退款订单
|
||||||
|
export function updateGroupChargeOrder(data) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/group/chargeorder',
|
||||||
|
method: 'put',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除团队(部门)充值退款订单
|
||||||
|
export function delGroupChargeOrder(id) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/group/chargeorder/' + id,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,63 +1,27 @@
|
||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
|
||||||
// 查询订单管理列表
|
// 查询团队(部门)充值退款订单列表
|
||||||
export function listOrder(query) {
|
export function listOrder(query) {
|
||||||
return request({
|
return request({
|
||||||
url: '/ai/order/list',
|
url: '/ai/video/order/',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: query
|
params: query
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询订单管理详细
|
// 查询当前用户所属部门的视频订单列表
|
||||||
|
export function listGroupOrder(query) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/video/order/group/',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询团队(部门)充值退款订单详细(只读)
|
||||||
export function getOrder(id) {
|
export function getOrder(id) {
|
||||||
return request({
|
return request({
|
||||||
url: '/ai/order/' + id,
|
url: '/ai/video/order/' + id,
|
||||||
method: 'get'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 新增订单管理
|
|
||||||
export function addOrder(data) {
|
|
||||||
return request({
|
|
||||||
url: '/ai/order',
|
|
||||||
method: 'post',
|
|
||||||
data: data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修改订单管理
|
|
||||||
export function updateOrder(data) {
|
|
||||||
return request({
|
|
||||||
url: '/ai/order',
|
|
||||||
method: 'put',
|
|
||||||
data: data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function changeIsTop(id, isTop) {
|
|
||||||
const data = {
|
|
||||||
id,
|
|
||||||
isTop
|
|
||||||
}
|
|
||||||
return request({
|
|
||||||
url: '/ai/order',
|
|
||||||
method: 'put',
|
|
||||||
data: data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除订单管理
|
|
||||||
export function delOrder(id) {
|
|
||||||
return request({
|
|
||||||
url: '/ai/order/' + id,
|
|
||||||
method: 'delete'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function downloadVideo(id) {
|
|
||||||
return request({
|
|
||||||
url: '/api/ai/' + id,
|
|
||||||
method: 'get'
|
method: 'get'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 查询团队(部门)余额变动列表
|
||||||
|
export function listRecord(query) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/record/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -56,18 +56,18 @@ export function changeUserStatus(id, status) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function changeBalance(id, balance) {
|
// 修改余额接口已关闭(与后端 AiUserController.changeBalance 一致停用)
|
||||||
const data = {
|
// export function changeBalance(id, balance) {
|
||||||
id,
|
// const data = {
|
||||||
balance
|
// id,
|
||||||
}
|
// balance
|
||||||
return request({
|
// }
|
||||||
url: '/ai/user/changeBalance',
|
// return request({
|
||||||
method: 'put',
|
// url: '/ai/user/changeBalance',
|
||||||
data: data
|
// method: 'put',
|
||||||
})
|
// data: data
|
||||||
}
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
// 用户状态修改
|
// 用户状态修改
|
||||||
export function updatePassword(id, newPassword) {
|
export function updatePassword(id, newPassword) {
|
||||||
|
|
@ -81,3 +81,30 @@ export function updatePassword(id, newPassword) {
|
||||||
data: data
|
data: data
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 分配归属部门,deptId 可省略或 null 表示清空 */
|
||||||
|
export function assignAiUserDept(data) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/user/dept',
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 部门积分下放至用户 */
|
||||||
|
export function issueDeptScore(data) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/user/dept-score/issue',
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 用户积分回收至部门 */
|
||||||
|
export function reclaimDeptScore(data) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/user/dept-score/reclaim',
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export function getSubteamOverview() {
|
||||||
|
return request({ url: '/subteam/overview', method: 'get' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listSubteamUser(query) {
|
||||||
|
return request({ url: '/subteam/user/list', method: 'get', params: query })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSubteamUser(id) {
|
||||||
|
const url = id != null && id !== '' ? '/subteam/user/' + id : '/subteam/user'
|
||||||
|
return request({ url, method: 'get' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addSubteamUser(data) {
|
||||||
|
return request({ url: '/subteam/user', method: 'post', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateSubteamUser(data) {
|
||||||
|
return request({ url: '/subteam/user', method: 'put', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function delSubteamUser(userIds) {
|
||||||
|
return request({ url: '/subteam/user/' + userIds, method: 'delete' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetSubteamUserPwd(data) {
|
||||||
|
return request({ url: '/subteam/user/resetPwd', method: 'put', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changeSubteamUserStatus(data) {
|
||||||
|
return request({ url: '/subteam/user/changeStatus', method: 'put', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 团队后台:部门积分下放至本团队用户 */
|
||||||
|
export function issueSubteamUserDeptScore(data) {
|
||||||
|
return request({ url: '/subteam/user/dept-score/issue', method: 'put', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 团队后台:本团队用户积分回收至部门 */
|
||||||
|
export function reclaimSubteamUserDeptScore(data) {
|
||||||
|
return request({ url: '/subteam/user/dept-score/reclaim', method: 'put', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 团队后台视频/生成订单列表:GET /subteam/video-order/list */
|
||||||
|
export function listSubteamVideoOrder(query) {
|
||||||
|
return request({ url: '/subteam/video-order/list', method: 'get', params: query })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出:POST /subteam/video-order/export(与列表相同查询参数 + 创建时间区间)。
|
||||||
|
* 页面一般使用 this.download('subteam/video-order/export', addDateRange(...), filename),勿用 GET。
|
||||||
|
*/
|
||||||
|
export const subteamVideoOrderExportUrl = 'subteam/video-order/export'
|
||||||
|
|
||||||
|
export function getSubteamVideoOrder(id) {
|
||||||
|
return request({ url: '/subteam/video-order/' + id, method: 'get' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listSubteamChargeOrder(query) {
|
||||||
|
return request({ url: '/subteam/charge-order/list', method: 'get', params: query })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出:POST /subteam/charge-order/export(与列表相同查询参数 + 创建时间区间) */
|
||||||
|
export const subteamChargeOrderExportUrl = 'subteam/charge-order/export'
|
||||||
|
|
||||||
|
export function getSubteamChargeOrder(id) {
|
||||||
|
return request({ url: '/subteam/charge-order/' + id, method: 'get' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listSubteamUserBalance(query) {
|
||||||
|
return request({ url: '/subteam/user-balance/list', method: 'get', params: query })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出:POST /subteam/user-balance/export(与列表相同查询参数 + beginTime/endTime) */
|
||||||
|
export const subteamUserBalanceExportUrl = 'subteam/user-balance/export'
|
||||||
|
|
||||||
|
export function getSubteamUserBalance(id) {
|
||||||
|
return request({ url: '/subteam/user-balance/' + id, method: 'get' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表:GET /subteam/consume-stat/list(参数与管理端 ai/data 一致:startDate、endDate、pageNum、pageSize) */
|
||||||
|
export function listSubteamConsumeStat(query) {
|
||||||
|
return request({ url: '/subteam/consume-stat/list', method: 'get', params: query })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出:POST /subteam/consume-stat/export */
|
||||||
|
export const subteamConsumeStatExportUrl = 'subteam/consume-stat/export'
|
||||||
|
|
||||||
|
export function listSubteamGroupBalance(query) {
|
||||||
|
return request({ url: '/subteam/group-balance/list', method: 'get', params: query })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出:POST /subteam/group-balance/export(与列表相同查询参数 + 创建时间区间) */
|
||||||
|
export const subteamGroupBalanceExportUrl = 'subteam/group-balance/export'
|
||||||
|
|
@ -25,6 +25,23 @@ export function getDept(deptId) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 部门火山引擎配置(解密后明文)
|
||||||
|
export function getDeptArk(deptId) {
|
||||||
|
return request({
|
||||||
|
url: '/system/dept/ark/' + deptId,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存部门火山引擎配置
|
||||||
|
export function updateDeptArk(data) {
|
||||||
|
return request({
|
||||||
|
url: '/system/dept/ark',
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 新增部门
|
// 新增部门
|
||||||
export function addDept(data) {
|
export function addDept(data) {
|
||||||
return request({
|
return request({
|
||||||
|
|
@ -50,3 +67,21 @@ export function delDept(deptId) {
|
||||||
method: 'delete'
|
method: 'delete'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 部门充值/退款
|
||||||
|
export function chargeRefundDept(data) {
|
||||||
|
return request({
|
||||||
|
url: '/system/dept/charge-refund',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 部门积分更正
|
||||||
|
export function editScore(data) {
|
||||||
|
return request({
|
||||||
|
url: '/system/dept/edit-score',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="top-right-btn" :style="style">
|
<div class="top-right-btn" :style="style" v-if="search || showRefresh || columns">
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top" v-if="search">
|
<el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top" v-if="search">
|
||||||
<el-button size="mini" circle icon="el-icon-search" @click="toggleSearch()" />
|
<el-button size="mini" circle icon="el-icon-search" @click="toggleSearch()" />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip class="item" effect="dark" content="刷新" placement="top">
|
<el-tooltip class="item" effect="dark" content="刷新" placement="top" v-if="showRefresh">
|
||||||
<el-button size="mini" circle icon="el-icon-refresh" @click="refresh()" />
|
<el-button size="mini" circle icon="el-icon-refresh" @click="refresh()" />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip class="item" effect="dark" content="显隐列" placement="top" v-if="columns">
|
<el-tooltip class="item" effect="dark" content="显隐列" placement="top" v-if="columns">
|
||||||
|
|
@ -59,10 +59,15 @@ export default {
|
||||||
columns: {
|
columns: {
|
||||||
type: Array
|
type: Array
|
||||||
},
|
},
|
||||||
/* 是否显示检索图标 */
|
/* 是否显示检索图标(隐藏搜索 / 显示搜索) */
|
||||||
search: {
|
search: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: false
|
||||||
|
},
|
||||||
|
/* 是否显示刷新 */
|
||||||
|
showRefresh: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
},
|
},
|
||||||
/* 显隐列类型(transfer穿梭框、checkbox复选框) */
|
/* 显隐列类型(transfer穿梭框、checkbox复选框) */
|
||||||
showColumnsType: {
|
showColumnsType: {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { listDept as listAiDept } from "@/api/ai/dept"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 与「统计数据」页(ai/data)一致:仅启用状态,且 ancestors 深度为 2 的团队,供筛选下拉使用。
|
||||||
|
* 系统团队页(system/dept)请覆写 secondLevelDeptListRequest,改为调用 /system/dept/list。
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
secondLevelDeptOptions: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/** @param {object} params 如 { status: '0' } */
|
||||||
|
secondLevelDeptListRequest(params) {
|
||||||
|
return listAiDept(params)
|
||||||
|
},
|
||||||
|
loadSecondLevelDeptOptions() {
|
||||||
|
this.secondLevelDeptListRequest({ status: "0" }).then(response => {
|
||||||
|
const allDeptList = response.data || []
|
||||||
|
this.secondLevelDeptOptions = allDeptList.filter(item => this.isSecondLevelDept(item))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
isSecondLevelDept(dept) {
|
||||||
|
if (!dept || !dept.ancestors) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return String(dept.ancestors).split(",").filter(Boolean).length === 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
/**
|
||||||
|
* 西式千分位(en-US):金额最多两位小数,积分为整数。
|
||||||
|
* 用于输入/粘贴后即时格式化展示。
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const WESTERN_MONEY_MAX = 10000000
|
||||||
|
export const WESTERN_INT_MAX = 100000000
|
||||||
|
|
||||||
|
const MONEY_MAX = WESTERN_MONEY_MAX
|
||||||
|
const INT_MAX = WESTERN_INT_MAX
|
||||||
|
|
||||||
|
/** 只保留数字与至多一个小数点,小数位最多 2 位 */
|
||||||
|
export function sanitizeMoneyDigits(raw) {
|
||||||
|
let s = String(raw == null ? '' : raw).replace(/,/g, '').replace(/[^\d.]/g, '')
|
||||||
|
const firstDot = s.indexOf('.')
|
||||||
|
if (firstDot !== -1) {
|
||||||
|
s = s.slice(0, firstDot + 1) + s.slice(firstDot + 1).replace(/\./g, '').slice(0, 2)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将规范化后的金额字符串格式化为 9,999,999.99 形式(输入过程中允许末尾为小数点) */
|
||||||
|
export function formatMoneyWesternDisplay(sanitized) {
|
||||||
|
if (!sanitized) return ''
|
||||||
|
const s = sanitized
|
||||||
|
const dot = s.indexOf('.')
|
||||||
|
const intRaw = dot === -1 ? s : s.slice(0, dot)
|
||||||
|
const decRaw = dot === -1 ? '' : s.slice(dot + 1).slice(0, 2)
|
||||||
|
const intNum = intRaw === '' ? 0 : parseInt(intRaw, 10)
|
||||||
|
const intFmt = (isNaN(intNum) ? 0 : intNum).toLocaleString('en-US')
|
||||||
|
if (dot === -1) return intFmt
|
||||||
|
if (s.endsWith('.') && decRaw === '') return intFmt + '.'
|
||||||
|
return intFmt + '.' + decRaw
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 规范化字符串 -> 金额数值(undefined 表示空) */
|
||||||
|
export function moneyStringToNumber(sanitized) {
|
||||||
|
if (sanitized === '' || sanitized === '.' || sanitized == null) return undefined
|
||||||
|
const n = parseFloat(sanitized)
|
||||||
|
if (isNaN(n)) return undefined
|
||||||
|
const rounded = Math.round(n * 100) / 100
|
||||||
|
if (rounded > MONEY_MAX) return MONEY_MAX
|
||||||
|
if (rounded < 0) return 0
|
||||||
|
return rounded
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 失焦时金额固定两位小数展示 */
|
||||||
|
export function formatMoneyWesternFinal(n) {
|
||||||
|
if (n === undefined || n === null || isNaN(n)) return ''
|
||||||
|
const v = Math.min(MONEY_MAX, Math.max(0, Math.round(Number(n) * 100) / 100))
|
||||||
|
return v.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 积分:仅数字,并限制范围 */
|
||||||
|
export function sanitizeIntDigits(raw) {
|
||||||
|
let d = String(raw == null ? '' : raw).replace(/,/g, '').replace(/\D/g, '')
|
||||||
|
if (d === '') return ''
|
||||||
|
let n = parseInt(d, 10)
|
||||||
|
if (isNaN(n)) return ''
|
||||||
|
n = Math.min(INT_MAX, Math.max(0, n))
|
||||||
|
return String(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatIntWesternDisplay(digits) {
|
||||||
|
if (!digits) return ''
|
||||||
|
const n = parseInt(digits, 10)
|
||||||
|
if (isNaN(n)) return ''
|
||||||
|
return n.toLocaleString('en-US')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function intStringToNumber(digits) {
|
||||||
|
if (!digits) return undefined
|
||||||
|
const n = parseInt(digits, 10)
|
||||||
|
if (isNaN(n)) return undefined
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 有符号积分:供可输入负数的整数字段,范围 [-INT_MAX, INT_MAX] */
|
||||||
|
export function sanitizeSignedIntDigits(raw) {
|
||||||
|
let t = String(raw == null ? '' : raw).replace(/,/g, '')
|
||||||
|
t = t.trim()
|
||||||
|
if (t === '' || t === '+') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if (t === '-') {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
const neg = t.startsWith('-')
|
||||||
|
let rest = neg ? t.slice(1) : t
|
||||||
|
rest = rest.replace(/\D/g, '')
|
||||||
|
if (rest === '') {
|
||||||
|
return neg ? '-' : ''
|
||||||
|
}
|
||||||
|
let n = parseInt(rest, 10)
|
||||||
|
if (isNaN(n)) {
|
||||||
|
return neg ? '-' : ''
|
||||||
|
}
|
||||||
|
n = Math.min(INT_MAX, Math.max(0, n))
|
||||||
|
return (neg ? '-' : '') + String(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSignedIntDisplay(s) {
|
||||||
|
if (s == null || s === '') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if (s === '-') {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
const n = parseInt(String(s).replace(/,/g, ''), 10)
|
||||||
|
if (isNaN(n)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return n.toLocaleString('en-US')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function signedIntStringToNumber(s) {
|
||||||
|
if (s == null || s === '' || s === '-') {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const n = parseInt(String(s).replace(/,/g, ''), 10)
|
||||||
|
if (isNaN(n)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,22 @@
|
||||||
@keyup.enter.native="handleQuery"
|
@keyup.enter.native="handleQuery"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="团队" prop="deptId">
|
||||||
|
<el-select
|
||||||
|
v-model="queryParams.deptId"
|
||||||
|
placeholder="全部团队(可选)"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
style="width: 220px"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in secondLevelDeptOptions"
|
||||||
|
:key="item.deptId"
|
||||||
|
:label="item.deptName"
|
||||||
|
:value="item.deptId"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="操作类型" prop="type">
|
<el-form-item label="操作类型" prop="type">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="queryParams.type"
|
v-model="queryParams.type"
|
||||||
|
|
@ -46,11 +62,6 @@
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<el-row :gutter="10" class="mb8">
|
|
||||||
<el-col :span="1.5">
|
|
||||||
<el-button
|
<el-button
|
||||||
type="warning"
|
type="warning"
|
||||||
plain
|
plain
|
||||||
|
|
@ -59,23 +70,32 @@
|
||||||
@click="handleExport"
|
@click="handleExport"
|
||||||
v-hasPermi="['ai:balanceChangeRecord:export']"
|
v-hasPermi="['ai:balanceChangeRecord:export']"
|
||||||
>导出</el-button>
|
>导出</el-button>
|
||||||
</el-col>
|
|
||||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||||
</el-row>
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
<el-table v-loading="loading" :data="recordList" @selection-change="handleSelectionChange">
|
<el-table v-loading="loading" :data="recordList">
|
||||||
<el-table-column type="selection" width="55" align="center" />
|
<el-table-column label="主键ID" align="center" prop="id" width="80"/>
|
||||||
<el-table-column label="主键ID" align="center" prop="id" />
|
<el-table-column label="用户ID" align="center" prop="uuid" width="100" show-overflow-tooltip />
|
||||||
<el-table-column label="用户ID" align="center" prop="uuid" />
|
<el-table-column label="用户昵称" align="center" prop="nickname" width="150" show-overflow-tooltip />
|
||||||
<el-table-column label="操作类型" align="center" prop="type" :formatter="formatType" />
|
<el-table-column label="团队名称" align="center" prop="deptName" width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column label="关联订单号" align="center" prop="orderNo" width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column label="操作类型" align="center" width="100" prop="type" :formatter="formatType" />
|
||||||
<el-table-column
|
<el-table-column
|
||||||
label="变更金额"
|
label="变更积分"
|
||||||
align="center"
|
align="center"
|
||||||
prop="changeAmount"
|
prop="changeAmount"
|
||||||
:formatter="formatChangeAmount"
|
:formatter="formatChangeAmount"
|
||||||
|
width="120"
|
||||||
/>
|
/>
|
||||||
<el-table-column label="变更后金额" align="center" prop="resultAmount" />
|
<el-table-column
|
||||||
<el-table-column label="操作时间" align="center" prop="createTime" />
|
label="变更后积分"
|
||||||
|
align="center"
|
||||||
|
prop="resultAmount"
|
||||||
|
:formatter="formatResultAmount"
|
||||||
|
width="120"
|
||||||
|
/>
|
||||||
|
<el-table-column label="操作时间" align="center" prop="createTime" width="150" />
|
||||||
<el-table-column label="备注" align="center" prop="remark" />
|
<el-table-column label="备注" align="center" prop="remark" />
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
|
|
@ -93,11 +113,11 @@
|
||||||
<el-form-item label="用户ID" prop="userId">
|
<el-form-item label="用户ID" prop="userId">
|
||||||
<el-input v-model="form.userId" placeholder="请输入用户ID" />
|
<el-input v-model="form.userId" placeholder="请输入用户ID" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="变更金额" prop="changeAmount">
|
<el-form-item label="变更积分" prop="changeAmount">
|
||||||
<el-input v-model="form.changeAmount" placeholder="请输入变更金额" />
|
<el-input v-model="form.changeAmount" placeholder="请输入变更积分" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="变更后金额" prop="resultAmount">
|
<el-form-item label="变更后积分" prop="resultAmount">
|
||||||
<el-input v-model="form.resultAmount" placeholder="请输入变更后金额" />
|
<el-input v-model="form.resultAmount" placeholder="请输入变更后积分" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="操作类型" prop="type">
|
<el-form-item label="操作类型" prop="type">
|
||||||
<el-select
|
<el-select
|
||||||
|
|
@ -137,27 +157,31 @@ import {
|
||||||
addRecord,
|
addRecord,
|
||||||
updateRecord
|
updateRecord
|
||||||
} from "@/api/ai/balanceChangeRecord";
|
} from "@/api/ai/balanceChangeRecord";
|
||||||
|
import secondLevelDeptFilter from "@/mixins/secondLevelDeptFilter";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Record",
|
name: "Record",
|
||||||
|
mixins: [secondLevelDeptFilter],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// 日期范围
|
// 日期范围
|
||||||
dateRange: [],
|
dateRange: [],
|
||||||
// 操作类型映射关系
|
// 操作类型映射关系
|
||||||
typeMap: {
|
typeMap: {
|
||||||
0: "充值",
|
// 0: "充值",
|
||||||
1: "返佣",
|
// 1: "返佣",
|
||||||
2: "充值赠送",
|
// 2: "充值赠送",
|
||||||
3: "体验金赠送",
|
// 3: "体验金赠送",
|
||||||
4: "体验金回收",
|
// 4: "体验金回收",
|
||||||
5: "一键脱衣",
|
// 5: "一键换衣",
|
||||||
6: "图生图2",
|
// 6: "图生图2",
|
||||||
7: "一键换脸",
|
// 7: "一键换脸",
|
||||||
8: "快捷生图",
|
// 8: "快捷生图",
|
||||||
9: "快捷生视频",
|
9: "快捷生视频",
|
||||||
10: "退款",
|
// 10: "退款",
|
||||||
11: "系统操作"
|
11: "系统操作",
|
||||||
|
12: "团队下发",
|
||||||
|
13: "团队收回"
|
||||||
},
|
},
|
||||||
// 遮罩层
|
// 遮罩层
|
||||||
loading: true,
|
loading: true,
|
||||||
|
|
@ -182,7 +206,9 @@ export default {
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
type: null,
|
type: null,
|
||||||
userId: null
|
uuid: null,
|
||||||
|
userId: null,
|
||||||
|
deptId: null
|
||||||
},
|
},
|
||||||
// 表单参数
|
// 表单参数
|
||||||
form: {},
|
form: {},
|
||||||
|
|
@ -195,17 +221,43 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
this.loadSecondLevelDeptOptions();
|
||||||
this.getList();
|
this.getList();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
formatChangeAmount(row, column) {
|
/** 西式千分位(en-US),积分为整数 */
|
||||||
const value = row.changeAmount;
|
formatPointsWestern(value) {
|
||||||
// 检查值是否为数字且大于 0
|
if (value === null || value === undefined || value === "") {
|
||||||
if (typeof value === "number" && value > 0) {
|
return "";
|
||||||
return `+${value}`;
|
|
||||||
}
|
}
|
||||||
// 对于小于等于 0 的值,直接返回原始值(或根据需要格式化)
|
const n = Number(value);
|
||||||
return value;
|
if (Number.isNaN(n)) {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
return n.toLocaleString("en-US", { maximumFractionDigits: 0 });
|
||||||
|
},
|
||||||
|
formatChangeAmount(row) {
|
||||||
|
const value = row.changeAmount;
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const n = Number(value);
|
||||||
|
if (Number.isNaN(n)) {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
const formatted = Math.abs(n).toLocaleString("en-US", {
|
||||||
|
maximumFractionDigits: 0
|
||||||
|
});
|
||||||
|
if (n > 0) {
|
||||||
|
return `+${formatted}`;
|
||||||
|
}
|
||||||
|
if (n < 0) {
|
||||||
|
return `-${formatted}`;
|
||||||
|
}
|
||||||
|
return "0";
|
||||||
|
},
|
||||||
|
formatResultAmount(row) {
|
||||||
|
return this.formatPointsWestern(row.resultAmount);
|
||||||
},
|
},
|
||||||
handleTypeChange(val) {
|
handleTypeChange(val) {
|
||||||
this.queryParams.type = val;
|
this.queryParams.type = val;
|
||||||
|
|
@ -318,9 +370,7 @@ export default {
|
||||||
handleExport() {
|
handleExport() {
|
||||||
this.download(
|
this.download(
|
||||||
"ai/balance-change-record/export",
|
"ai/balance-change-record/export",
|
||||||
{
|
this.addDateRange({ ...this.queryParams }, this.dateRange),
|
||||||
...this.queryParams
|
|
||||||
},
|
|
||||||
`record_${new Date().getTime()}.xlsx`
|
`record_${new Date().getTime()}.xlsx`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,270 @@
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
||||||
|
<el-form-item label="团队ID" prop="deptId">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.deptId"
|
||||||
|
placeholder="请输入团队ID"
|
||||||
|
clearable
|
||||||
|
@keyup.enter.native="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-row :gutter="10" class="mb8">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
icon="el-icon-plus"
|
||||||
|
size="mini"
|
||||||
|
@click="handleAdd"
|
||||||
|
v-hasPermi="['ai:config:add']"
|
||||||
|
>新增</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
plain
|
||||||
|
icon="el-icon-edit"
|
||||||
|
size="mini"
|
||||||
|
:disabled="single"
|
||||||
|
@click="handleUpdate"
|
||||||
|
v-hasPermi="['ai:config:edit']"
|
||||||
|
>修改</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
icon="el-icon-delete"
|
||||||
|
size="mini"
|
||||||
|
:disabled="multiple"
|
||||||
|
@click="handleDelete"
|
||||||
|
v-hasPermi="['ai:config:remove']"
|
||||||
|
>删除</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
icon="el-icon-download"
|
||||||
|
size="mini"
|
||||||
|
@click="handleExport"
|
||||||
|
v-hasPermi="['ai:config:export']"
|
||||||
|
>导出</el-button>
|
||||||
|
</el-col>
|
||||||
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="configList" @selection-change="handleSelectionChange">
|
||||||
|
<el-table-column type="selection" width="55" align="center" />
|
||||||
|
<el-table-column label="ID" align="center" prop="id" />
|
||||||
|
<el-table-column label="团队ID" align="center" prop="deptId" />
|
||||||
|
<el-table-column label="视频模型列表JSON(label+value)" align="center" prop="modelParm" />
|
||||||
|
<el-table-column label="Byte project,加密" align="center" prop="project" />
|
||||||
|
<el-table-column label="Byte API Key,加密" align="center" prop="byteApiKey" />
|
||||||
|
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-edit"
|
||||||
|
@click="handleUpdate(scope.row)"
|
||||||
|
v-hasPermi="['ai:config:edit']"
|
||||||
|
>修改</el-button>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
@click="handleDelete(scope.row)"
|
||||||
|
v-hasPermi="['ai:config:remove']"
|
||||||
|
>删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination
|
||||||
|
v-show="total>0"
|
||||||
|
:total="total"
|
||||||
|
:page.sync="queryParams.pageNum"
|
||||||
|
:limit.sync="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 添加或修改团队(部门)对应火山引擎配置对话框 -->
|
||||||
|
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
|
||||||
|
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
||||||
|
<el-form-item label="团队ID" prop="deptId">
|
||||||
|
<el-input v-model="form.deptId" placeholder="请输入团队ID" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="视频模型列表JSON(label+value)" prop="modelParm">
|
||||||
|
<el-input v-model="form.modelParm" type="textarea" placeholder="请输入内容" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Byte project,加密" prop="project">
|
||||||
|
<el-input v-model="form.project" placeholder="请输入Byte project,加密" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Byte API Key,加密" prop="byteApiKey">
|
||||||
|
<el-input v-model="form.byteApiKey" placeholder="请输入Byte API Key,加密" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||||
|
<el-button @click="cancel">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listConfig, getConfig, delConfig, addConfig, updateConfig } from "@/api/ai/config"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Config",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// 遮罩层
|
||||||
|
loading: true,
|
||||||
|
// 选中数组
|
||||||
|
ids: [],
|
||||||
|
// 非单个禁用
|
||||||
|
single: true,
|
||||||
|
// 非多个禁用
|
||||||
|
multiple: true,
|
||||||
|
// 显示搜索条件
|
||||||
|
showSearch: true,
|
||||||
|
// 总条数
|
||||||
|
total: 0,
|
||||||
|
// 团队(部门)对应火山引擎配置表格数据
|
||||||
|
configList: [],
|
||||||
|
// 弹出层标题
|
||||||
|
title: "",
|
||||||
|
// 是否显示弹出层
|
||||||
|
open: false,
|
||||||
|
// 查询参数
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
deptId: null,
|
||||||
|
modelParm: null,
|
||||||
|
project: null,
|
||||||
|
byteApiKey: null,
|
||||||
|
},
|
||||||
|
// 表单参数
|
||||||
|
form: {},
|
||||||
|
// 表单校验
|
||||||
|
rules: {
|
||||||
|
deptId: [
|
||||||
|
{ required: true, message: "团队ID不能为空", trigger: "blur" }
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/** 查询团队(部门)对应火山引擎配置列表 */
|
||||||
|
getList() {
|
||||||
|
this.loading = true
|
||||||
|
listConfig(this.queryParams).then(response => {
|
||||||
|
this.configList = response.rows
|
||||||
|
this.total = response.total
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 取消按钮
|
||||||
|
cancel() {
|
||||||
|
this.open = false
|
||||||
|
this.reset()
|
||||||
|
},
|
||||||
|
// 表单重置
|
||||||
|
reset() {
|
||||||
|
this.form = {
|
||||||
|
id: null,
|
||||||
|
deptId: null,
|
||||||
|
modelParm: null,
|
||||||
|
project: null,
|
||||||
|
byteApiKey: null,
|
||||||
|
createBy: null,
|
||||||
|
createTime: null,
|
||||||
|
updateBy: null,
|
||||||
|
updateTime: null
|
||||||
|
}
|
||||||
|
this.resetForm("form")
|
||||||
|
},
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
handleQuery() {
|
||||||
|
this.queryParams.pageNum = 1
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
resetQuery() {
|
||||||
|
this.resetForm("queryForm")
|
||||||
|
this.handleQuery()
|
||||||
|
},
|
||||||
|
// 多选框选中数据
|
||||||
|
handleSelectionChange(selection) {
|
||||||
|
this.ids = selection.map(item => item.id)
|
||||||
|
this.single = selection.length!==1
|
||||||
|
this.multiple = !selection.length
|
||||||
|
},
|
||||||
|
/** 新增按钮操作 */
|
||||||
|
handleAdd() {
|
||||||
|
this.reset()
|
||||||
|
this.open = true
|
||||||
|
this.title = "添加团队(团队)对应火山引擎配置"
|
||||||
|
},
|
||||||
|
/** 修改按钮操作 */
|
||||||
|
handleUpdate(row) {
|
||||||
|
this.reset()
|
||||||
|
const id = row.id || this.ids
|
||||||
|
getConfig(id).then(response => {
|
||||||
|
this.form = response.data
|
||||||
|
this.open = true
|
||||||
|
this.title = "修改团队(团队)对应火山引擎配置"
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/** 提交按钮 */
|
||||||
|
submitForm() {
|
||||||
|
this.$refs["form"].validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
if (this.form.id != null) {
|
||||||
|
updateConfig(this.form).then(response => {
|
||||||
|
this.$modal.msgSuccess("修改成功")
|
||||||
|
this.open = false
|
||||||
|
this.getList()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
addConfig(this.form).then(response => {
|
||||||
|
this.$modal.msgSuccess("新增成功")
|
||||||
|
this.open = false
|
||||||
|
this.getList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/** 删除按钮操作 */
|
||||||
|
handleDelete(row) {
|
||||||
|
const ids = row.id || this.ids
|
||||||
|
this.$modal.confirm('是否确认删除团队(团队)对应火山引擎配置编号为"' + ids + '"的数据项?').then(function() {
|
||||||
|
return delConfig(ids)
|
||||||
|
}).then(() => {
|
||||||
|
this.getList()
|
||||||
|
this.$modal.msgSuccess("删除成功")
|
||||||
|
}).catch(() => {})
|
||||||
|
},
|
||||||
|
/** 导出按钮操作 */
|
||||||
|
handleExport() {
|
||||||
|
this.download('ai/config/export', {
|
||||||
|
...this.queryParams
|
||||||
|
}, `config_${new Date().getTime()}.xlsx`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,283 @@
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="96px">
|
||||||
|
<el-form-item label="统计日期">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
unlink-panels
|
||||||
|
value-format="yyyy-MM-dd"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
style="width: 280px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="团队" prop="deptId">
|
||||||
|
<el-select
|
||||||
|
v-model="queryParams.deptId"
|
||||||
|
placeholder="全部团队(可选)"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
style="width: 220px"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in secondLevelDeptOptions"
|
||||||
|
:key="item.deptId"
|
||||||
|
:label="item.deptName"
|
||||||
|
:value="item.deptId"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
icon="el-icon-download"
|
||||||
|
size="mini"
|
||||||
|
@click="handleExport"
|
||||||
|
v-hasPermi="['ai:data:export']"
|
||||||
|
>导出</el-button>
|
||||||
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<p class="summary-line">
|
||||||
|
<template v-if="summaryState.mode === 'loading'">
|
||||||
|
<span> </span>
|
||||||
|
<span class="summary-prefix">汇总:</span><span class="summary-muted">加载中…</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="summaryState.mode === 'empty'">
|
||||||
|
<span> </span>
|
||||||
|
<span class="summary-prefix">汇总:</span><span class="summary-muted">—</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span> </span>
|
||||||
|
<span class="summary-prefix">汇总:</span>
|
||||||
|
<span class="summary-label">总计</span>
|
||||||
|
<span class="summary-num">{{ summaryState.totalStr }}</span>
|
||||||
|
<span class="summary-label">条记录,实际充值积分</span>
|
||||||
|
<span class="summary-num">{{ summaryState.recharge }}</span>
|
||||||
|
<span class="summary-unit">分</span>
|
||||||
|
<span class="summary-label">,消耗积分</span>
|
||||||
|
<span class="summary-num">{{ summaryState.score }}</span>
|
||||||
|
<span class="summary-unit">分</span>
|
||||||
|
<span class="summary-label">,实际订单数量(成功)</span>
|
||||||
|
<span class="summary-num">{{ summaryState.orders }}</span>
|
||||||
|
<span class="summary-unit">单</span>
|
||||||
|
<span class="summary-label">,三方消耗tokens数量</span>
|
||||||
|
<span class="summary-num">{{ summaryState.tokens }}</span>
|
||||||
|
<span class="summary-unit">。</span>
|
||||||
|
</template>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="dataList">
|
||||||
|
<el-table-column label="日期" align="center" prop="dateKey" min-width="110" />
|
||||||
|
<el-table-column label="团队ID" align="center" prop="deptId" width="100" />
|
||||||
|
<el-table-column label="团队名称" align="center" prop="deptName" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column
|
||||||
|
label="实际充值积分(充值-退款)"
|
||||||
|
align="center"
|
||||||
|
prop="rechargeScore"
|
||||||
|
min-width="200"
|
||||||
|
:formatter="formatWesternNumber"
|
||||||
|
/>
|
||||||
|
<el-table-column label="消耗积分" align="center" prop="score" width="120" :formatter="formatWesternNumber" />
|
||||||
|
<el-table-column
|
||||||
|
label="实际订单数量(成功)"
|
||||||
|
align="center"
|
||||||
|
prop="orderCount"
|
||||||
|
width="160"
|
||||||
|
:formatter="formatWesternNumber"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
label="三方消耗tokens数量"
|
||||||
|
align="center"
|
||||||
|
prop="useTokens"
|
||||||
|
width="170"
|
||||||
|
:formatter="formatWesternNumber"
|
||||||
|
/>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination
|
||||||
|
v-show="total>0"
|
||||||
|
:total="total"
|
||||||
|
:page.sync="queryParams.pageNum"
|
||||||
|
:limit.sync="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listData } from "@/api/ai/data"
|
||||||
|
import secondLevelDeptFilter from "@/mixins/secondLevelDeptFilter"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "TeamConsumeData",
|
||||||
|
mixins: [secondLevelDeptFilter],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
showSearch: true,
|
||||||
|
total: 0,
|
||||||
|
dataList: [],
|
||||||
|
aggregate: null,
|
||||||
|
dateRange: [],
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
startDate: null,
|
||||||
|
endDate: null,
|
||||||
|
deptId: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.loadSecondLevelDeptOptions()
|
||||||
|
this.initDefaultDateRange()
|
||||||
|
this.syncDateRangeToQuery()
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
summaryState() {
|
||||||
|
if (this.loading) {
|
||||||
|
return { mode: "loading" }
|
||||||
|
}
|
||||||
|
const a = this.aggregate
|
||||||
|
if (!a) {
|
||||||
|
return { mode: "empty" }
|
||||||
|
}
|
||||||
|
const fmt = (v) => this.formatWesternNumberValue(v)
|
||||||
|
const n = a.totalRows != null ? Number(a.totalRows) : 0
|
||||||
|
const totalStr = Number.isNaN(n) ? "0" : n.toLocaleString("en-US")
|
||||||
|
return {
|
||||||
|
mode: "ok",
|
||||||
|
totalStr,
|
||||||
|
recharge: fmt(a.sumRechargeScore),
|
||||||
|
score: fmt(a.sumScore),
|
||||||
|
orders: fmt(a.sumOrderCount),
|
||||||
|
tokens: fmt(a.sumUseTokens)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatWesternNumberValue(cellValue) {
|
||||||
|
if (cellValue === null || cellValue === undefined || cellValue === "") {
|
||||||
|
return (0).toLocaleString("en-US")
|
||||||
|
}
|
||||||
|
const n = Number(cellValue)
|
||||||
|
if (Number.isNaN(n)) {
|
||||||
|
return (0).toLocaleString("en-US")
|
||||||
|
}
|
||||||
|
return n.toLocaleString("en-US")
|
||||||
|
},
|
||||||
|
/** null/空 显示 0;数字按 en-US 千分位(如 99,999,999) */
|
||||||
|
formatWesternNumber(row, column, cellValue) {
|
||||||
|
return this.formatWesternNumberValue(cellValue)
|
||||||
|
},
|
||||||
|
buildLastMonthDateRange() {
|
||||||
|
const end = new Date()
|
||||||
|
const start = new Date(end.getTime())
|
||||||
|
start.setMonth(start.getMonth() - 1)
|
||||||
|
return [this.formatYmd(start), this.formatYmd(end)]
|
||||||
|
},
|
||||||
|
formatYmd(d) {
|
||||||
|
const y = d.getFullYear()
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, "0")
|
||||||
|
const day = String(d.getDate()).padStart(2, "0")
|
||||||
|
return `${y}-${m}-${day}`
|
||||||
|
},
|
||||||
|
initDefaultDateRange() {
|
||||||
|
this.dateRange = this.buildLastMonthDateRange()
|
||||||
|
},
|
||||||
|
syncDateRangeToQuery() {
|
||||||
|
if (Array.isArray(this.dateRange) && this.dateRange.length === 2) {
|
||||||
|
this.queryParams.startDate = this.dateRange[0]
|
||||||
|
this.queryParams.endDate = this.dateRange[1]
|
||||||
|
} else {
|
||||||
|
this.queryParams.startDate = null
|
||||||
|
this.queryParams.endDate = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getList() {
|
||||||
|
this.syncDateRangeToQuery()
|
||||||
|
this.loading = true
|
||||||
|
listData(this.queryParams)
|
||||||
|
.then(response => {
|
||||||
|
this.dataList = response.rows || []
|
||||||
|
this.total = response.total != null ? response.total : 0
|
||||||
|
this.aggregate =
|
||||||
|
response.param && response.param.aggregate != null ? response.param.aggregate : null
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleQuery() {
|
||||||
|
this.queryParams.pageNum = 1
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
resetQuery() {
|
||||||
|
this.initDefaultDateRange()
|
||||||
|
this.resetForm("queryForm")
|
||||||
|
this.queryParams.pageNum = 1
|
||||||
|
this.syncDateRangeToQuery()
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
handleExport() {
|
||||||
|
this.syncDateRangeToQuery()
|
||||||
|
this.download(
|
||||||
|
"ai/data/export",
|
||||||
|
{
|
||||||
|
startDate: this.queryParams.startDate,
|
||||||
|
endDate: this.queryParams.endDate,
|
||||||
|
deptId: this.queryParams.deptId
|
||||||
|
},
|
||||||
|
`team_consume_${new Date().getTime()}.xlsx`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.summary-line {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
padding-left: 8px;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-prefix {
|
||||||
|
color: #909399;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-muted {
|
||||||
|
color: #c0c4cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-label {
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-unit {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 1px;
|
||||||
|
margin-right: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-num {
|
||||||
|
color: #409eff;
|
||||||
|
font-weight: 600;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,473 @@
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch">
|
||||||
|
<el-form-item label="团队" prop="deptId">
|
||||||
|
<el-select
|
||||||
|
v-model="queryParams.deptId"
|
||||||
|
placeholder="全部团队(可选)"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
style="width: 220px"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in secondLevelDeptOptions"
|
||||||
|
:key="item.deptId"
|
||||||
|
:label="item.deptName"
|
||||||
|
:value="item.deptId"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-select v-model="queryParams.status" placeholder="团队状态" clearable>
|
||||||
|
<el-option
|
||||||
|
v-for="dict in dict.type.sys_normal_disable"
|
||||||
|
:key="dict.value"
|
||||||
|
:label="dict.label"
|
||||||
|
:value="dict.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-row :gutter="10" class="mb8">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
icon="el-icon-plus"
|
||||||
|
size="mini"
|
||||||
|
@click="handleAdd"
|
||||||
|
v-hasPermi="['ai:dept:add']"
|
||||||
|
>新增</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="info"
|
||||||
|
plain
|
||||||
|
icon="el-icon-sort"
|
||||||
|
size="mini"
|
||||||
|
@click="toggleExpandAll"
|
||||||
|
>展开/折叠</el-button>
|
||||||
|
</el-col>
|
||||||
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
v-if="refreshTable"
|
||||||
|
v-loading="loading"
|
||||||
|
:data="deptList"
|
||||||
|
row-key="deptId"
|
||||||
|
:default-expand-all="isExpandAll"
|
||||||
|
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
|
||||||
|
>
|
||||||
|
<el-table-column prop="deptName" label="团队名称" width="260"></el-table-column>
|
||||||
|
<el-table-column prop="orderNum" label="排序" width="200"></el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<dict-tag :options="dict.type.sys_normal_disable" :value="scope.row.status"/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建时间" align="center" prop="createTime" width="200">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-edit"
|
||||||
|
@click="handleUpdate(scope.row)"
|
||||||
|
v-hasPermi="['ai:dept:edit']"
|
||||||
|
>修改</el-button>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-plus"
|
||||||
|
@click="handleAdd(scope.row)"
|
||||||
|
v-hasPermi="['ai:dept:add']"
|
||||||
|
>新增</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="scope.row.parentId != 0"
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
@click="handleDelete(scope.row)"
|
||||||
|
v-hasPermi="['ai:dept:remove']"
|
||||||
|
>删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-dialog :title="title" :visible.sync="open" width="760px" append-to-body>
|
||||||
|
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24" v-if="form.parentId !== 0">
|
||||||
|
<el-form-item label="上级团队" prop="parentId">
|
||||||
|
<treeselect v-model="form.parentId" :options="deptOptions" :normalizer="normalizer" placeholder="选择上级团队" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="团队名称" prop="deptName">
|
||||||
|
<el-input v-model="form.deptName" placeholder="请输入团队名称" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="显示排序" prop="orderNum">
|
||||||
|
<el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="负责人" prop="leader">
|
||||||
|
<el-input v-model="form.leader" placeholder="请输入负责人" maxlength="20" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="联系电话" prop="phone">
|
||||||
|
<el-input v-model="form.phone" placeholder="请输入联系电话" maxlength="11" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="邮箱" prop="email">
|
||||||
|
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="团队状态">
|
||||||
|
<el-radio-group v-model="form.status">
|
||||||
|
<el-radio
|
||||||
|
v-for="dict in dict.type.sys_normal_disable"
|
||||||
|
:key="dict.value"
|
||||||
|
:label="dict.value"
|
||||||
|
>{{dict.label}}</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row v-if="isSecondLevelCompanyForm">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="ApiKey">
|
||||||
|
<el-input
|
||||||
|
v-model="form.byteApiKey"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
placeholder="选填"
|
||||||
|
maxlength="255"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row v-if="isSecondLevelCompanyForm">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="视频模型">
|
||||||
|
<div class="model-parm-block">
|
||||||
|
<div
|
||||||
|
v-for="(row, idx) in modelParamRows"
|
||||||
|
:key="'mp-' + idx"
|
||||||
|
class="model-parm-row"
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
v-model="row.label"
|
||||||
|
placeholder="显示名称(如 Seedance 2.0)"
|
||||||
|
class="model-parm-input-label"
|
||||||
|
/>
|
||||||
|
<el-input
|
||||||
|
v-model="row.value"
|
||||||
|
placeholder="Endpoint / 模型 ID(ep-…)"
|
||||||
|
class="model-parm-input-value"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
:disabled="modelParamRows.length <= 1"
|
||||||
|
@click="removeModelParamRow(idx)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<el-button type="text" icon="el-icon-plus" @click="addModelParamRow">添加模型</el-button>
|
||||||
|
<p class="model-parm-hint">
|
||||||
|
保存为 JSON 写入库表 model_parm;门户「视频生成」按用户所属二级团队读取。
|
||||||
|
留空或未配置时,使用配置文件 portal.video.models。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||||
|
<el-button @click="cancel">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.model-parm-block {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.model-parm-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.model-parm-input-label {
|
||||||
|
width: 38%;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.model-parm-input-value {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.model-parm-hint {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/ai/dept"
|
||||||
|
import Treeselect from "@riophae/vue-treeselect"
|
||||||
|
import "@riophae/vue-treeselect/dist/vue-treeselect.css"
|
||||||
|
import secondLevelDeptFilter from "@/mixins/secondLevelDeptFilter"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "AiDept",
|
||||||
|
dicts: ['sys_normal_disable'],
|
||||||
|
components: { Treeselect },
|
||||||
|
mixins: [secondLevelDeptFilter],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
showSearch: true,
|
||||||
|
deptList: [],
|
||||||
|
deptOptions: [],
|
||||||
|
title: "",
|
||||||
|
open: false,
|
||||||
|
isExpandAll: true,
|
||||||
|
refreshTable: true,
|
||||||
|
queryParams: {
|
||||||
|
deptId: null,
|
||||||
|
status: undefined
|
||||||
|
},
|
||||||
|
form: {},
|
||||||
|
modelParamRows: [{ label: '', value: '' }],
|
||||||
|
rules: {
|
||||||
|
parentId: [
|
||||||
|
{ required: true, message: "上级团队不能为空", trigger: "blur" }
|
||||||
|
],
|
||||||
|
deptName: [
|
||||||
|
{ required: true, message: "团队名称不能为空", trigger: "blur" }
|
||||||
|
],
|
||||||
|
orderNum: [
|
||||||
|
{ required: true, message: "显示排序不能为空", trigger: "blur" }
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{
|
||||||
|
type: "email",
|
||||||
|
message: "请输入正确的邮箱地址",
|
||||||
|
trigger: ["blur", "change"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
phone: [
|
||||||
|
{
|
||||||
|
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
|
||||||
|
message: "请输入正确的手机号码",
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.loadSecondLevelDeptOptions()
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isSecondLevelCompanyForm() {
|
||||||
|
if (this.form.ancestors === "0,100") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const pid = this.form.parentId
|
||||||
|
if (pid !== undefined && pid !== null && Number(pid) === 100) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getList() {
|
||||||
|
this.loading = true
|
||||||
|
const { deptId, ...deptListQuery } = this.queryParams
|
||||||
|
listDept(deptListQuery).then(response => {
|
||||||
|
let raw = response.data || []
|
||||||
|
if (deptId != null && deptId !== "") {
|
||||||
|
const rootId = Number(deptId)
|
||||||
|
raw = raw.filter(d => this.deptRowInSubtree(d, rootId))
|
||||||
|
}
|
||||||
|
this.deptList = this.handleTree(raw, "deptId")
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deptRowInSubtree(d, rootId) {
|
||||||
|
if (d.deptId != null && Number(d.deptId) === rootId) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (!d.ancestors) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return String(d.ancestors)
|
||||||
|
.split(",")
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(s => Number(s))
|
||||||
|
.includes(rootId)
|
||||||
|
},
|
||||||
|
normalizer(node) {
|
||||||
|
if (node.children && !node.children.length) {
|
||||||
|
delete node.children
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: node.deptId,
|
||||||
|
label: node.deptName,
|
||||||
|
children: node.children
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
this.open = false
|
||||||
|
this.reset()
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
this.form = {
|
||||||
|
deptId: undefined,
|
||||||
|
parentId: undefined,
|
||||||
|
ancestors: undefined,
|
||||||
|
deptName: undefined,
|
||||||
|
orderNum: undefined,
|
||||||
|
leader: undefined,
|
||||||
|
phone: undefined,
|
||||||
|
email: undefined,
|
||||||
|
byteApiKey: undefined,
|
||||||
|
modelParm: undefined,
|
||||||
|
status: "0"
|
||||||
|
}
|
||||||
|
this.modelParamRows = [{ label: '', value: '' }]
|
||||||
|
this.resetForm("form")
|
||||||
|
},
|
||||||
|
handleQuery() {
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
resetQuery() {
|
||||||
|
this.resetForm("queryForm")
|
||||||
|
this.handleQuery()
|
||||||
|
},
|
||||||
|
handleAdd(row) {
|
||||||
|
this.reset()
|
||||||
|
if (row != undefined) {
|
||||||
|
this.form.parentId = row.deptId
|
||||||
|
}
|
||||||
|
this.open = true
|
||||||
|
this.title = "添加团队"
|
||||||
|
listDept().then(response => {
|
||||||
|
this.deptOptions = this.handleTree(response.data, "deptId")
|
||||||
|
})
|
||||||
|
},
|
||||||
|
toggleExpandAll() {
|
||||||
|
this.refreshTable = false
|
||||||
|
this.isExpandAll = !this.isExpandAll
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.refreshTable = true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
syncModelRowsFromForm() {
|
||||||
|
this.modelParamRows = [{ label: '', value: '' }]
|
||||||
|
const raw = this.form.modelParm
|
||||||
|
if (!raw || String(raw).trim() === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const arr = JSON.parse(raw)
|
||||||
|
if (Array.isArray(arr) && arr.length) {
|
||||||
|
this.modelParamRows = arr.map(x => ({
|
||||||
|
label: (x && x.label) ? String(x.label) : '',
|
||||||
|
value: (x && x.value) ? String(x.value) : ''
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.modelParamRows = [{ label: '', value: '' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buildModelParmPayload() {
|
||||||
|
if (!this.isSecondLevelCompanyForm) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const rows = (this.modelParamRows || []).filter(r => r.label && r.value)
|
||||||
|
this.form.modelParm = rows.length ? JSON.stringify(rows) : ''
|
||||||
|
},
|
||||||
|
addModelParamRow() {
|
||||||
|
this.modelParamRows.push({ label: '', value: '' })
|
||||||
|
},
|
||||||
|
removeModelParamRow(idx) {
|
||||||
|
if (this.modelParamRows.length <= 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.modelParamRows.splice(idx, 1)
|
||||||
|
},
|
||||||
|
handleUpdate(row) {
|
||||||
|
this.reset()
|
||||||
|
getDept(row.deptId).then(response => {
|
||||||
|
this.form = response.data
|
||||||
|
this.syncModelRowsFromForm()
|
||||||
|
this.open = true
|
||||||
|
this.title = "修改团队"
|
||||||
|
listDeptExcludeChild(row.deptId).then(response => {
|
||||||
|
this.deptOptions = this.handleTree(response.data, "deptId")
|
||||||
|
if (this.deptOptions.length == 0) {
|
||||||
|
const noResultsOptions = { deptId: this.form.parentId, deptName: this.form.parentName, children: [] }
|
||||||
|
this.deptOptions.push(noResultsOptions)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
submitForm: function() {
|
||||||
|
this.$refs["form"].validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
this.buildModelParmPayload()
|
||||||
|
if (this.form.deptId != undefined) {
|
||||||
|
updateDept(this.form).then(response => {
|
||||||
|
this.$modal.msgSuccess("修改成功")
|
||||||
|
this.open = false
|
||||||
|
this.getList()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
addDept(this.form).then(response => {
|
||||||
|
this.$modal.msgSuccess("新增成功")
|
||||||
|
this.open = false
|
||||||
|
this.getList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleDelete(row) {
|
||||||
|
this.$modal.confirm('是否确认删除名称为"' + row.deptName + '"的数据项?').then(function() {
|
||||||
|
return delDept(row.deptId)
|
||||||
|
}).then(() => {
|
||||||
|
this.getList()
|
||||||
|
this.$modal.msgSuccess("删除成功")
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,321 @@
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="70px">
|
||||||
|
<el-form-item label="订单号" prop="orderNum">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.orderNum"
|
||||||
|
placeholder="支持模糊搜索"
|
||||||
|
clearable
|
||||||
|
@keyup.enter.native="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="团队" prop="deptId">
|
||||||
|
<el-select
|
||||||
|
v-model="queryParams.deptId"
|
||||||
|
placeholder="全部团队(可选)"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
style="width: 220px"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in secondLevelDeptOptions"
|
||||||
|
:key="item.deptId"
|
||||||
|
:label="item.deptName"
|
||||||
|
:value="item.deptId"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="类型" prop="orderType">
|
||||||
|
<el-select v-model="queryParams.orderType" placeholder="全部" clearable style="width: 110px">
|
||||||
|
<el-option
|
||||||
|
v-for="item in orderTypeQueryOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="创建时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
style="width: 240px"
|
||||||
|
value-format="yyyy-MM-dd"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="-"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
icon="el-icon-download"
|
||||||
|
size="mini"
|
||||||
|
@click="handleExport"
|
||||||
|
v-hasPermi="['ai:groupChargeOrder:export']"
|
||||||
|
>导出</el-button>
|
||||||
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="groupChargeOrderList">
|
||||||
|
<el-table-column label="订单号" align="center" prop="orderNum" width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column label="团队名称" align="center" prop="deptName" width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column label="类型" align="center" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ orderTypeLabel(scope.row.orderType) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="金额(元)" align="center" prop="money" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ formatTableMoney(scope.row.money) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="积分" align="center" prop="amount" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ formatTableAmount(scope.row.amount) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" align="center" width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ statusLabel(scope.row.status) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip />
|
||||||
|
<el-table-column label="创建时间" align="center" prop="createTime" width="150">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination
|
||||||
|
v-show="total > 0"
|
||||||
|
:total="total"
|
||||||
|
:page.sync="queryParams.pageNum"
|
||||||
|
:limit.sync="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-dialog :title="title" :visible.sync="open" width="560px" append-to-body>
|
||||||
|
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
|
||||||
|
<el-form-item label="团队ID" prop="deptId">
|
||||||
|
<el-input v-model="form.deptId" placeholder="请输入团队ID" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="类型" prop="orderType">
|
||||||
|
<el-select v-model="form.orderType" placeholder="请选择" style="width: 100%">
|
||||||
|
<el-option v-for="item in orderTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="金额(元)" prop="money">
|
||||||
|
<el-input v-model="form.money" placeholder="可选,对账用金额" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="积分" prop="amount">
|
||||||
|
<el-input v-model="form.amount" placeholder="充值/退款填正数;手动修改可填负数表示扣减" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-select v-model="form.status" placeholder="请选择" style="width: 100%">
|
||||||
|
<el-option label="进行中" :value="0" />
|
||||||
|
<el-option label="已完成" :value="1" />
|
||||||
|
<el-option label="失败" :value="2" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="订单编号" prop="orderNum" v-if="form.id != null">
|
||||||
|
<el-input v-model="form.orderNum" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注" prop="remark">
|
||||||
|
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="对账说明、打款信息等" maxlength="500" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||||
|
<el-button @click="cancel">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
listGroupChargeOrder,
|
||||||
|
getGroupChargeOrder,
|
||||||
|
delGroupChargeOrder,
|
||||||
|
addGroupChargeOrder,
|
||||||
|
updateGroupChargeOrder
|
||||||
|
} from "@/api/ai/groupChargeOrder"
|
||||||
|
import secondLevelDeptFilter from "@/mixins/secondLevelDeptFilter"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "GroupChargeOrder",
|
||||||
|
mixins: [secondLevelDeptFilter],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
showSearch: true,
|
||||||
|
dateRange: [],
|
||||||
|
total: 0,
|
||||||
|
groupChargeOrderList: [],
|
||||||
|
title: "",
|
||||||
|
open: false,
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
orderNum: null,
|
||||||
|
deptId: null,
|
||||||
|
orderType: null
|
||||||
|
},
|
||||||
|
form: {},
|
||||||
|
rules: {
|
||||||
|
deptId: [{ required: true, message: "请输入团队ID", trigger: "blur" }],
|
||||||
|
orderType: [{ required: true, message: "请选择类型", trigger: "change" }],
|
||||||
|
amount: [{ required: true, message: "请填写积分", trigger: "blur" }],
|
||||||
|
status: [{ required: true, message: "请选择状态", trigger: "change" }]
|
||||||
|
},
|
||||||
|
orderTypeQueryOptions: [
|
||||||
|
{ label: "充值", value: 0 },
|
||||||
|
{ label: "退款", value: 1 }
|
||||||
|
],
|
||||||
|
orderTypeOptions: [
|
||||||
|
{ label: "充值", value: 0 },
|
||||||
|
{ label: "退款", value: 1 },
|
||||||
|
{ label: "手动修改", value: 2 }
|
||||||
|
],
|
||||||
|
statusOptions: [
|
||||||
|
{ label: "进行中", value: 0 },
|
||||||
|
{ label: "已完成", value: 1 },
|
||||||
|
{ label: "失败", value: 2 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.loadSecondLevelDeptOptions()
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/** 列表金额:西式千分位,最多两位小数 */
|
||||||
|
formatTableMoney(value) {
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
return "—"
|
||||||
|
}
|
||||||
|
const n = Number(value)
|
||||||
|
if (Number.isNaN(n)) {
|
||||||
|
return "—"
|
||||||
|
}
|
||||||
|
return n.toLocaleString("en-US", { maximumFractionDigits: 2, minimumFractionDigits: 0 })
|
||||||
|
},
|
||||||
|
/** 列表积分:西式千分位整数 */
|
||||||
|
formatTableAmount(value) {
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
return "—"
|
||||||
|
}
|
||||||
|
const n = Number(value)
|
||||||
|
if (Number.isNaN(n)) {
|
||||||
|
return "—"
|
||||||
|
}
|
||||||
|
return n.toLocaleString("en-US", { maximumFractionDigits: 0 })
|
||||||
|
},
|
||||||
|
orderTypeLabel(orderType) {
|
||||||
|
const n = orderType !== undefined && orderType !== null ? Number(orderType) : null
|
||||||
|
const hit = this.orderTypeOptions.find(o => o.value === n)
|
||||||
|
return hit ? hit.label : "—"
|
||||||
|
},
|
||||||
|
statusLabel(status) {
|
||||||
|
const n = status !== undefined && status !== null ? Number(status) : null
|
||||||
|
const hit = this.statusOptions.find(o => o.value === n)
|
||||||
|
return hit ? hit.label : "—"
|
||||||
|
},
|
||||||
|
getList() {
|
||||||
|
this.loading = true
|
||||||
|
listGroupChargeOrder(this.addDateRange(this.queryParams, this.dateRange, "CreateTime")).then(response => {
|
||||||
|
this.groupChargeOrderList = response.rows
|
||||||
|
this.total = response.total
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
this.open = false
|
||||||
|
this.reset()
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
this.form = {
|
||||||
|
id: null,
|
||||||
|
deptId: undefined,
|
||||||
|
orderType: 0,
|
||||||
|
money: undefined,
|
||||||
|
amount: undefined,
|
||||||
|
remark: undefined,
|
||||||
|
status: 1,
|
||||||
|
orderNum: undefined
|
||||||
|
}
|
||||||
|
this.resetForm("form")
|
||||||
|
},
|
||||||
|
handleQuery() {
|
||||||
|
this.queryParams.pageNum = 1
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
resetQuery() {
|
||||||
|
this.dateRange = []
|
||||||
|
this.resetForm("queryForm")
|
||||||
|
this.handleQuery()
|
||||||
|
},
|
||||||
|
handleAdd() {
|
||||||
|
this.reset()
|
||||||
|
this.open = true
|
||||||
|
this.title = "新增团队充值退款订单"
|
||||||
|
},
|
||||||
|
handleUpdate(row) {
|
||||||
|
this.reset()
|
||||||
|
const id = row && row.id
|
||||||
|
if (!id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
getGroupChargeOrder(id).then(response => {
|
||||||
|
this.form = response.data
|
||||||
|
this.open = true
|
||||||
|
this.title = "修改团队充值退款订单"
|
||||||
|
})
|
||||||
|
},
|
||||||
|
submitForm() {
|
||||||
|
this.$refs["form"].validate(valid => {
|
||||||
|
if (!valid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const payload = { ...this.form }
|
||||||
|
if (payload.id == null) {
|
||||||
|
addGroupChargeOrder(payload).then(() => {
|
||||||
|
this.$modal.msgSuccess("新增成功")
|
||||||
|
this.open = false
|
||||||
|
this.getList()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
updateGroupChargeOrder(payload).then(() => {
|
||||||
|
this.$modal.msgSuccess("修改成功")
|
||||||
|
this.open = false
|
||||||
|
this.getList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleDelete(row) {
|
||||||
|
const id = row && row.id
|
||||||
|
if (!id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$modal.confirm("是否确认删除所选团队充值退款订单?").then(() => {
|
||||||
|
return delGroupChargeOrder(id)
|
||||||
|
}).then(() => {
|
||||||
|
this.getList()
|
||||||
|
this.$modal.msgSuccess("删除成功")
|
||||||
|
}).catch(() => {})
|
||||||
|
},
|
||||||
|
handleExport() {
|
||||||
|
this.download("ai/group/chargeorder/export", {
|
||||||
|
...this.queryParams
|
||||||
|
}, `group_charge_order_${new Date().getTime()}.xlsx`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -29,11 +29,6 @@
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<el-row :gutter="10" class="mb8">
|
|
||||||
<el-col :span="1.5">
|
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
plain
|
plain
|
||||||
|
|
@ -42,8 +37,6 @@
|
||||||
@click="handleAdd"
|
@click="handleAdd"
|
||||||
v-hasPermi="['ai:manager:add']"
|
v-hasPermi="['ai:manager:add']"
|
||||||
>新增</el-button>
|
>新增</el-button>
|
||||||
</el-col>
|
|
||||||
<el-col :span="1.5">
|
|
||||||
<el-button
|
<el-button
|
||||||
type="success"
|
type="success"
|
||||||
plain
|
plain
|
||||||
|
|
@ -53,19 +46,6 @@
|
||||||
@click="handleUpdate"
|
@click="handleUpdate"
|
||||||
v-hasPermi="['ai:manager:edit']"
|
v-hasPermi="['ai:manager:edit']"
|
||||||
>修改</el-button>
|
>修改</el-button>
|
||||||
</el-col>
|
|
||||||
<!-- <el-col :span="1.5">
|
|
||||||
<el-button
|
|
||||||
type="danger"
|
|
||||||
plain
|
|
||||||
icon="el-icon-delete"
|
|
||||||
size="mini"
|
|
||||||
:disabled="multiple"
|
|
||||||
@click="handleDelete"
|
|
||||||
v-hasPermi="['ai:manager:remove']"
|
|
||||||
>删除</el-button>
|
|
||||||
</el-col>-->
|
|
||||||
<el-col :span="1.5">
|
|
||||||
<el-button
|
<el-button
|
||||||
type="warning"
|
type="warning"
|
||||||
plain
|
plain
|
||||||
|
|
@ -74,8 +54,11 @@
|
||||||
@click="handleExport"
|
@click="handleExport"
|
||||||
v-hasPermi="['ai:manager:export']"
|
v-hasPermi="['ai:manager:export']"
|
||||||
>导出</el-button>
|
>导出</el-button>
|
||||||
</el-col>
|
|
||||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-row :gutter="10" class="mb8">
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-table v-loading="loading" :data="managerList" @selection-change="handleSelectionChange">
|
<el-table v-loading="loading" :data="managerList" @selection-change="handleSelectionChange">
|
||||||
|
|
@ -93,9 +76,9 @@
|
||||||
></el-switch>
|
></el-switch>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<!--
|
||||||
<el-table-column label="提示词" align="center" prop="prompt">
|
<el-table-column label="提示词" align="center" prop="prompt">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<!-- 核心:给 popover 加 ref,触发元素绑定 v-popover -->
|
|
||||||
<el-popover
|
<el-popover
|
||||||
ref="promptPopover"
|
ref="promptPopover"
|
||||||
placement="top"
|
placement="top"
|
||||||
|
|
@ -103,7 +86,6 @@
|
||||||
trigger="hover"
|
trigger="hover"
|
||||||
:content="scope.row.prompt || '无提示词'"
|
:content="scope.row.prompt || '无提示词'"
|
||||||
></el-popover>
|
></el-popover>
|
||||||
<!-- 触发元素:绑定 v-popover 到 popover 的 ref -->
|
|
||||||
<div
|
<div
|
||||||
class="text-ellipsis-two-lines"
|
class="text-ellipsis-two-lines"
|
||||||
v-popover:promptPopover
|
v-popover:promptPopover
|
||||||
|
|
@ -111,6 +93,7 @@
|
||||||
>{{ scope.row.prompt}}</div>
|
>{{ scope.row.prompt}}</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
-->
|
||||||
<el-table-column label="类型" align="center" prop="type">
|
<el-table-column label="类型" align="center" prop="type">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<dict-tag :options="dict.type.ai_function_type" :value="scope.row.type" />
|
<dict-tag :options="dict.type.ai_function_type" :value="scope.row.type" />
|
||||||
|
|
@ -119,13 +102,13 @@
|
||||||
<el-table-column label="备注" align="center" prop="remark" />
|
<el-table-column label="备注" align="center" prop="remark" />
|
||||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button
|
<!--<el-button
|
||||||
size="mini"
|
size="mini"
|
||||||
type="text"
|
type="text"
|
||||||
icon="el-icon-edit"
|
icon="el-icon-edit"
|
||||||
@click="tagManager(scope.row)"
|
@click="tagManager(scope.row)"
|
||||||
v-hasPermi="['ai:manager:edit']"
|
v-hasPermi="['ai:manager:edit']"
|
||||||
>标签管理</el-button>
|
>标签管理</el-button>-->
|
||||||
<el-button
|
<el-button
|
||||||
size="mini"
|
size="mini"
|
||||||
type="text"
|
type="text"
|
||||||
|
|
|
||||||
|
|
@ -1,65 +1,52 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<el-form
|
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="88px">
|
||||||
:model="queryParams"
|
<el-form-item label="订单号" prop="orderNum">
|
||||||
ref="queryForm"
|
|
||||||
size="small"
|
|
||||||
:inline="true"
|
|
||||||
v-show="showSearch"
|
|
||||||
label-width="68px"
|
|
||||||
>
|
|
||||||
<el-form-item label="订单编号" prop="orderNum">
|
|
||||||
<el-input
|
<el-input
|
||||||
v-model="queryParams.orderNum"
|
v-model="queryParams.orderNum"
|
||||||
placeholder="请输入订单编号"
|
placeholder="请输入订单号"
|
||||||
clearable
|
clearable
|
||||||
@keyup.enter.native="handleQuery"
|
@keyup.enter.native="handleQuery"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="用户ID" prop="uuid">
|
<el-form-item label="三方订单号" prop="thirdPartyOrderNum">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="queryParams.uuid"
|
v-model="queryParams.thirdPartyOrderNum"
|
||||||
placeholder="请输入用户ID"
|
placeholder="支持模糊搜索"
|
||||||
|
style="width: 240px"
|
||||||
clearable
|
clearable
|
||||||
@keyup.enter.native="handleQuery"
|
@keyup.enter.native="handleQuery"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="操作类型" prop="type">
|
<el-form-item label="团队" prop="deptId">
|
||||||
<el-select v-model="queryParams.type" placeholder="请选择操作类型" clearable>
|
|
||||||
<el-option
|
|
||||||
v-for="dict in dict.type.ai_function_type"
|
|
||||||
:key="dict.value"
|
|
||||||
:label="dict.label"
|
|
||||||
:value="dict.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="是否置顶" prop="isTop">
|
|
||||||
<el-select v-model="queryParams.isTop" placeholder="请选择" clearable>
|
|
||||||
<el-option
|
|
||||||
v-for="dict in dict.type.sys_yes_no"
|
|
||||||
:key="dict.value"
|
|
||||||
:label="dict.label"
|
|
||||||
:value="dict.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="状态" prop="status">
|
|
||||||
<el-select
|
<el-select
|
||||||
v-model="queryParams.status"
|
v-model="queryParams.deptId"
|
||||||
placeholder="请选择状态"
|
placeholder="全部团队(可选)"
|
||||||
filterable
|
|
||||||
clearable
|
clearable
|
||||||
style="width: 200px; margin-bottom: 16px;"
|
filterable
|
||||||
|
style="width: 220px"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="(label, value) in statusMap"
|
v-for="item in secondLevelDeptOptions"
|
||||||
:key="value"
|
:key="item.deptId"
|
||||||
:label="label"
|
:label="item.deptName"
|
||||||
:value="Number(value)"
|
:value="item.deptId"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="用户名称" prop="userName">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.userName"
|
||||||
|
placeholder="支持模糊搜索"
|
||||||
|
clearable
|
||||||
|
@keyup.enter.native="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width: 207px">
|
||||||
|
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="创建时间">
|
<el-form-item label="创建时间">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="dateRange"
|
v-model="dateRange"
|
||||||
|
|
@ -69,95 +56,67 @@
|
||||||
range-separator="-"
|
range-separator="-"
|
||||||
start-placeholder="开始日期"
|
start-placeholder="开始日期"
|
||||||
end-placeholder="结束日期"
|
end-placeholder="结束日期"
|
||||||
></el-date-picker>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
|
|
||||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<el-row :gutter="10" class="mb8">
|
|
||||||
<el-col :span="1.5">
|
|
||||||
<el-button
|
<el-button
|
||||||
type="warning"
|
type="warning"
|
||||||
plain
|
plain
|
||||||
icon="el-icon-download"
|
icon="el-icon-download"
|
||||||
size="mini"
|
size="mini"
|
||||||
@click="handleExport"
|
@click="handleExport"
|
||||||
v-hasPermi="['ai:order:export']"
|
v-hasPermi="['/ai/video/order/']"
|
||||||
>导出</el-button>
|
>导出</el-button>
|
||||||
</el-col>
|
|
||||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||||
</el-row>
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
<el-table v-loading="loading" :data="orderList" @selection-change="handleSelectionChange">
|
<el-table v-loading="loading" :data="orderList">
|
||||||
<el-table-column type="selection" width="55" align="center" />
|
<el-table-column label="订单号" align="center" prop="orderNum" width="200" show-overflow-tooltip />
|
||||||
<el-table-column label="主键ID" align="center" prop="id" />
|
<el-table-column label="三方订单号" align="center" prop="thirdPartyOrderNum" width="200" show-overflow-tooltip />
|
||||||
<!-- <el-table-column label="备注" align="center" prop="remark" /> -->
|
<el-table-column label="团队名称" align="center" prop="deptName" width="140" show-overflow-tooltip />
|
||||||
<el-table-column label="订单编号" align="center" prop="orderNum" />
|
<el-table-column label="用户名称" align="center" prop="userName" width="120" show-overflow-tooltip />
|
||||||
<el-table-column label="用户ID" align="center" prop="uuid" />
|
<el-table-column label="消耗积分" align="center" prop="amount" width="120">
|
||||||
<el-table-column label="操作类型" align="center" prop="type">
|
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<dict-tag :options="dict.type.ai_function_type" :value="scope.row.type" />
|
<span>{{ formatThousands(scope.row.amount) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="金额" align="center" prop="amount" />
|
<el-table-column label="使用tokens" align="center" prop="totalUsage" width="120">
|
||||||
<el-table-column label="生成结果" align="center">
|
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<!-- 判断是否为链接 -->
|
<span>{{ formatThousands(scope.row.totalUsage) }}</span>
|
||||||
<template v-if="isUrl(scope.row.result)">
|
</template>
|
||||||
<!-- 图片链接 -->
|
</el-table-column>
|
||||||
<img
|
<el-table-column label="状态" align="center" width="100">
|
||||||
v-if="isImage(scope.row.result)"
|
<template slot-scope="scope">
|
||||||
:src="scope.row.result"
|
<span>{{ statusLabel(scope.row.status) }}</span>
|
||||||
class="preview-img"
|
</template>
|
||||||
@click="viewImage(scope.row.result)"
|
</el-table-column>
|
||||||
alt="预览图片"
|
<el-table-column label="结果预览" min-width="185">
|
||||||
/>
|
<template slot-scope="scope">
|
||||||
<!-- 视频链接 -->
|
<div v-if="extractVideoUrl(scope.row.result)">
|
||||||
<video
|
<video
|
||||||
v-else-if="isVideo(scope.row.result)"
|
:src="extractVideoUrl(scope.row.result)"
|
||||||
:src="scope.row.result"
|
controls
|
||||||
class="preview-video"
|
preload="metadata"
|
||||||
@click="playVideo(scope.row.result)"
|
style="width: 180px; max-height: 100px; border-radius: 4px;"
|
||||||
/>
|
/>
|
||||||
</template>
|
</div>
|
||||||
<!-- 非链接内容 -->
|
<span v-else>-</span>
|
||||||
<span v-else @click="handleOtherEvent(scope.row)">{{ scope.row.result }}</span>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="文字描述" align="center" prop="text">
|
<el-table-column label="创建时间" align="center" prop="createTime" width="150">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<!-- 核心:给 popover 加 ref,触发元素绑定 v-popover -->
|
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||||
<el-popover
|
|
||||||
ref="promptPopover"
|
|
||||||
placement="top"
|
|
||||||
width="400"
|
|
||||||
trigger="hover"
|
|
||||||
:content="scope.row.text || '无提示词'"
|
|
||||||
></el-popover>
|
|
||||||
<!-- 触发元素:绑定 v-popover 到 popover 的 ref -->
|
|
||||||
<div
|
|
||||||
class="text-ellipsis-two-lines"
|
|
||||||
v-popover:promptPopover
|
|
||||||
style="cursor: pointer;"
|
|
||||||
>{{ scope.row.text}}</div>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="置顶" align="center" key="status">
|
<el-table-column label="操作" width="90" fixed="right">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-switch
|
<el-button type="text" size="mini" @click="handleDetail(scope.row)">详情</el-button>
|
||||||
v-model="scope.row.isTop"
|
|
||||||
active-value="Y"
|
|
||||||
inactive-value="N"
|
|
||||||
@change="handleIsTopChange(scope.row)"
|
|
||||||
></el-switch>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="创建时间" align="center" prop="createTime" />
|
|
||||||
<el-table-column label="状态" align="center" prop="status" :formatter="formatStatus" />
|
|
||||||
<el-table-column label="来源" align="center" prop="source" />
|
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<pagination
|
<pagination
|
||||||
|
|
@ -168,366 +127,170 @@
|
||||||
@pagination="getList"
|
@pagination="getList"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 添加或修改订单管理对话框 -->
|
<el-dialog title="订单详情" :visible.sync="detailVisible" width="720px" append-to-body>
|
||||||
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
|
<el-descriptions :column="1" border size="small" :labelStyle="{ width: '150px', minWidth: '150px' }">
|
||||||
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
<el-descriptions-item v-for="(value, key) in detailData" :key="key" :label="key">
|
||||||
<el-form-item label="订单编号" prop="orderNum">
|
<pre v-if="isObjectValue(value)" style="margin: 0; white-space: pre-wrap;">{{ formatJson(value) }}</pre>
|
||||||
<el-input v-model="form.orderNum" placeholder="请输入订单编号" />
|
<span v-else>{{ value === null || value === undefined || value === "" ? "-" : value }}</span>
|
||||||
</el-form-item>
|
</el-descriptions-item>
|
||||||
<el-form-item label="用户ID" prop="userId">
|
</el-descriptions>
|
||||||
<el-input v-model="form.userId" placeholder="请输入用户ID" />
|
<span slot="footer">
|
||||||
</el-form-item>
|
<el-button size="mini" @click="detailVisible = false">关闭</el-button>
|
||||||
<el-form-item label="状态" prop="status">
|
</span>
|
||||||
<el-select
|
|
||||||
v-model="form.status"
|
|
||||||
placeholder="请选择状态"
|
|
||||||
filterable
|
|
||||||
clearable
|
|
||||||
style="width: 200px;"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="(label, value) in statusMap"
|
|
||||||
:key="value"
|
|
||||||
:label="label"
|
|
||||||
:value="Number(value)"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="金额" prop="amount">
|
|
||||||
<el-input v-model="form.amount" placeholder="请输入金额" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="生成结果" prop="result">
|
|
||||||
<el-input v-model="form.result" placeholder="请输入生成结果" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="备注" prop="remark">
|
|
||||||
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<div slot="footer" class="dialog-footer">
|
|
||||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
|
||||||
<el-button @click="cancel">取 消</el-button>
|
|
||||||
</div>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- 图片预览弹窗 -->
|
|
||||||
<el-dialog :visible.sync="imageDialogVisible" title="图片预览" width="60%">
|
|
||||||
<img :src="previewImageUrl" class="dialog-img" alt="大图预览" />
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- 新增视频弹窗 -->
|
|
||||||
<el-dialog :visible.sync="videoDialogVisible" title="视频播放" width="60%">
|
|
||||||
<video :src="previewVideoUrl" controls class="dialog-video"></video>
|
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { listOrder } from "@/api/ai/order"
|
||||||
listOrder,
|
import secondLevelDeptFilter from "@/mixins/secondLevelDeptFilter"
|
||||||
getOrder,
|
|
||||||
delOrder,
|
|
||||||
addOrder,
|
|
||||||
downloadVideo,
|
|
||||||
changeIsTop,
|
|
||||||
updateOrder
|
|
||||||
} from "@/api/ai/order";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Order",
|
name: "TeamChargeOrder",
|
||||||
dicts: ["ai_function_type", "sys_yes_no"],
|
mixins: [secondLevelDeptFilter],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
imageDialogVisible: false,
|
|
||||||
videoDialogVisible: false,
|
|
||||||
previewVideoUrl: "",
|
|
||||||
previewImageUrl: "",
|
|
||||||
dateRange: [],
|
|
||||||
statusMap: {
|
|
||||||
0: "进行中",
|
|
||||||
1: "已完成",
|
|
||||||
2: "已失败"
|
|
||||||
},
|
|
||||||
// 遮罩层
|
|
||||||
loading: true,
|
loading: true,
|
||||||
// 选中数组
|
|
||||||
ids: [],
|
|
||||||
// 非单个禁用
|
|
||||||
single: true,
|
|
||||||
// 非多个禁用
|
|
||||||
multiple: true,
|
|
||||||
// 显示搜索条件
|
|
||||||
showSearch: true,
|
showSearch: true,
|
||||||
// 总条数
|
|
||||||
total: 0,
|
total: 0,
|
||||||
// 订单管理表格数据
|
|
||||||
orderList: [],
|
orderList: [],
|
||||||
// 弹出层标题
|
detailVisible: false,
|
||||||
title: "",
|
detailData: {},
|
||||||
// 是否显示弹出层
|
orderTypeOptions: [
|
||||||
open: false,
|
{ label: "充值", value: 0 },
|
||||||
// 查询参数
|
{ label: "退款", value: 1 }
|
||||||
|
],
|
||||||
|
statusOptions: [
|
||||||
|
{ label: "进行中", value: 0 },
|
||||||
|
{ label: "完成", value: 1 },
|
||||||
|
{ label: "失败", value: 2 }
|
||||||
|
],
|
||||||
|
dateRange: [],
|
||||||
queryParams: {
|
queryParams: {
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
orderNum: null,
|
orderNum: null,
|
||||||
userId: null,
|
thirdPartyOrderNum: null,
|
||||||
type: null,
|
userName: null,
|
||||||
amount: null,
|
deptId: null,
|
||||||
result: null,
|
orderType: null,
|
||||||
status: null
|
status: null
|
||||||
},
|
|
||||||
// 表单参数
|
|
||||||
form: {},
|
|
||||||
// 表单校验
|
|
||||||
rules: {
|
|
||||||
delFlag: [
|
|
||||||
{ required: true, message: "删除标志不能为空", trigger: "blur" }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.getList();
|
this.loadSecondLevelDeptOptions()
|
||||||
|
this.initDefaultDateRange()
|
||||||
|
this.getList()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleIsTopChange(row) {
|
/** 千位分节(en-US),整数;空为 — */
|
||||||
let text = row.isTop === "Y" ? "置顶" : "取消置顶";
|
formatThousands(value) {
|
||||||
this.$modal
|
if (value === null || value === undefined || value === "") {
|
||||||
.confirm('确认要"' + text + '""' + row.id + '"作品吗?')
|
return "—"
|
||||||
.then(function() {
|
}
|
||||||
return changeIsTop(row.id, row.isTop);
|
const n = Number(value)
|
||||||
})
|
if (Number.isNaN(n)) {
|
||||||
.then(() => {
|
return String(value)
|
||||||
this.$modal.msgSuccess(text + "成功");
|
}
|
||||||
})
|
return n.toLocaleString("en-US", { maximumFractionDigits: 0 })
|
||||||
.catch(function() {
|
|
||||||
row.isTop = row.isTop === "Y" ? "N" : "Y";
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
// 判断是否为URL
|
initDefaultDateRange() {
|
||||||
isUrl(str) {
|
this.dateRange = this.buildLastMonthDateRange()
|
||||||
const urlRegex = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([\/\w.-]*)*\/?$/;
|
|
||||||
return urlRegex.test(str);
|
|
||||||
},
|
},
|
||||||
// 判断是否为图片链接
|
buildLastMonthDateRange() {
|
||||||
isImage(url) {
|
const end = new Date()
|
||||||
console.log(url)
|
const start = new Date(end.getTime())
|
||||||
const imageExtensions = /\.(jpg|jpeg|png|gif|bmp|webp)$/i;
|
start.setMonth(start.getMonth() - 1)
|
||||||
var b = imageExtensions.test(url);
|
return [this.formatYmd(start), this.formatYmd(end)]
|
||||||
console.log(b)
|
|
||||||
return imageExtensions.test(url);
|
|
||||||
},
|
},
|
||||||
// 判断是否为视频链接
|
formatYmd(d) {
|
||||||
isVideo(url) {
|
const y = d.getFullYear()
|
||||||
const videoExtensions = /\.(mp4|avi|mov|wmv|flv|webm)$/i;
|
const m = String(d.getMonth() + 1).padStart(2, "0")
|
||||||
return videoExtensions.test(url);
|
const day = String(d.getDate()).padStart(2, "0")
|
||||||
|
return `${y}-${m}-${day}`
|
||||||
},
|
},
|
||||||
// 查看图片
|
orderTypeLabel(orderType) {
|
||||||
viewImage(url) {
|
const n = orderType !== undefined && orderType !== null ? Number(orderType) : null
|
||||||
this.previewImageUrl = url;
|
const hit = this.orderTypeOptions.find(o => o.value === n)
|
||||||
this.imageDialogVisible = true;
|
return hit ? hit.label : "—"
|
||||||
},
|
},
|
||||||
// 播放视频
|
statusLabel(status) {
|
||||||
playVideo(url) {
|
const n = status !== undefined && status !== null ? Number(status) : null
|
||||||
this.previewVideoUrl = url;
|
const hit = this.statusOptions.find(o => o.value === n)
|
||||||
this.videoDialogVisible = true;
|
return hit ? hit.label : "—"
|
||||||
},
|
},
|
||||||
// 非链接内容点击事件
|
extractVideoUrl(result) {
|
||||||
async handleOtherEvent(row) {
|
if (!result) return ""
|
||||||
// 防止重复点击
|
if (typeof result === "string") {
|
||||||
if (row.isDownloading) return;
|
const value = result.trim()
|
||||||
const originalResult = row.result;
|
if (!value) return ""
|
||||||
|
if (/^https?:\/\/|^\/\//.test(value)) return value
|
||||||
try {
|
try {
|
||||||
// 标记为下载中状态
|
const parsed = JSON.parse(value)
|
||||||
row.isDownloading = true;
|
return this.extractVideoUrl(parsed)
|
||||||
// 保存原始内容
|
} catch (e) {
|
||||||
|
return ""
|
||||||
// 修改显示内容为“下载中...”
|
|
||||||
row.result = "下载中...";
|
|
||||||
|
|
||||||
// 执行下载操作
|
|
||||||
const resultId = originalResult; // 假设result存储的是ID
|
|
||||||
var res = await downloadVideo(resultId);
|
|
||||||
if (res.status === "succeeded") {
|
|
||||||
row.result = res.content.video_url;
|
|
||||||
} else {
|
|
||||||
row.result = "视频生成中"
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
if (Array.isArray(result)) {
|
||||||
console.error('下载失败:', error);
|
for (let i = 0; i < result.length; i += 1) {
|
||||||
// 失败后恢复原始内容
|
const url = this.extractVideoUrl(result[i])
|
||||||
row.result = originalResult || row.result.replace("下载中...", "");
|
if (url) return url
|
||||||
this.$message.error('下载失败,请重试');
|
}
|
||||||
} finally {
|
return ""
|
||||||
// 重置下载状态
|
}
|
||||||
row.isDownloading = false;
|
if (typeof result === "object") {
|
||||||
|
const directKeys = ["videoUrl", "url", "resultUrl", "playUrl", "output", "fileUrl"]
|
||||||
|
for (let i = 0; i < directKeys.length; i += 1) {
|
||||||
|
const key = directKeys[i]
|
||||||
|
if (result[key]) {
|
||||||
|
const url = this.extractVideoUrl(result[key])
|
||||||
|
if (url) return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
handleDetail(row) {
|
||||||
|
this.detailData = row || {}
|
||||||
|
this.detailVisible = true
|
||||||
|
},
|
||||||
|
isObjectValue(value) {
|
||||||
|
return value !== null && typeof value === "object"
|
||||||
|
},
|
||||||
|
formatJson(value) {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value, null, 2)
|
||||||
|
} catch (e) {
|
||||||
|
return String(value)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
formatStatus(row) {
|
|
||||||
// 如果有匹配的类型则显示对应文字,否则显示"未知"
|
|
||||||
return this.statusMap[row.status] || "未知";
|
|
||||||
},
|
|
||||||
/** 查询订单管理列表 */
|
|
||||||
getList() {
|
getList() {
|
||||||
this.loading = true;
|
this.loading = true
|
||||||
listOrder(this.addDateRange(this.queryParams, this.dateRange)).then(
|
listOrder(this.addDateRange(this.queryParams, this.dateRange, "CreateTime")).then(response => {
|
||||||
response => {
|
this.orderList = response.rows
|
||||||
this.orderList = response.rows;
|
this.total = response.total
|
||||||
this.total = response.total;
|
this.loading = false
|
||||||
this.loading = false;
|
})
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
// 取消按钮
|
|
||||||
cancel() {
|
|
||||||
this.open = false;
|
|
||||||
this.reset();
|
|
||||||
},
|
|
||||||
// 表单重置
|
|
||||||
reset() {
|
|
||||||
this.form = {
|
|
||||||
id: null,
|
|
||||||
delFlag: null,
|
|
||||||
createBy: null,
|
|
||||||
createTime: null,
|
|
||||||
updateBy: null,
|
|
||||||
updateTime: null,
|
|
||||||
remark: null,
|
|
||||||
orderNum: null,
|
|
||||||
userId: null,
|
|
||||||
type: null,
|
|
||||||
amount: null,
|
|
||||||
result: null,
|
|
||||||
status: null
|
|
||||||
};
|
|
||||||
this.resetForm("form");
|
|
||||||
},
|
|
||||||
/** 搜索按钮操作 */
|
|
||||||
handleQuery() {
|
handleQuery() {
|
||||||
this.queryParams.pageNum = 1;
|
this.queryParams.pageNum = 1
|
||||||
this.getList();
|
this.getList()
|
||||||
},
|
},
|
||||||
/** 重置按钮操作 */
|
|
||||||
resetQuery() {
|
resetQuery() {
|
||||||
this.dateRange = [];
|
this.initDefaultDateRange()
|
||||||
this.resetForm("queryForm");
|
this.resetForm("queryForm")
|
||||||
this.handleQuery();
|
this.handleQuery()
|
||||||
},
|
},
|
||||||
// 多选框选中数据
|
|
||||||
handleSelectionChange(selection) {
|
|
||||||
this.ids = selection.map(item => item.id);
|
|
||||||
this.single = selection.length !== 1;
|
|
||||||
this.multiple = !selection.length;
|
|
||||||
},
|
|
||||||
/** 新增按钮操作 */
|
|
||||||
handleAdd() {
|
|
||||||
this.reset();
|
|
||||||
this.open = true;
|
|
||||||
this.title = "添加订单管理";
|
|
||||||
},
|
|
||||||
/** 修改按钮操作 */
|
|
||||||
handleUpdate(row) {
|
|
||||||
this.reset();
|
|
||||||
const id = row.id || this.ids;
|
|
||||||
getOrder(id).then(response => {
|
|
||||||
this.form = response.data;
|
|
||||||
this.open = true;
|
|
||||||
this.title = "修改订单管理";
|
|
||||||
});
|
|
||||||
},
|
|
||||||
/** 提交按钮 */
|
|
||||||
submitForm() {
|
|
||||||
this.$refs["form"].validate(valid => {
|
|
||||||
if (valid) {
|
|
||||||
if (this.form.id != null) {
|
|
||||||
updateOrder(this.form).then(response => {
|
|
||||||
this.$modal.msgSuccess("修改成功");
|
|
||||||
this.open = false;
|
|
||||||
this.getList();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
addOrder(this.form).then(response => {
|
|
||||||
this.$modal.msgSuccess("新增成功");
|
|
||||||
this.open = false;
|
|
||||||
this.getList();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
/** 删除按钮操作 */
|
|
||||||
handleDelete(row) {
|
|
||||||
const ids = row.id || this.ids;
|
|
||||||
this.$modal
|
|
||||||
.confirm('是否确认删除订单管理编号为"' + ids + '"的数据项?')
|
|
||||||
.then(function() {
|
|
||||||
return delOrder(ids);
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.getList();
|
|
||||||
this.$modal.msgSuccess("删除成功");
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
},
|
|
||||||
/** 导出按钮操作 */
|
|
||||||
handleExport() {
|
handleExport() {
|
||||||
|
const q = { ...this.queryParams }
|
||||||
this.download(
|
this.download(
|
||||||
"ai/order/export",
|
"ai/video/order/export",
|
||||||
{
|
this.addDateRange(q, this.dateRange, "CreateTime"),
|
||||||
...this.queryParams
|
`team_orders_${new Date().getTime()}.xlsx`
|
||||||
},
|
)
|
||||||
`order_${new Date().getTime()}.xlsx`
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.dialog-video {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-img {
|
|
||||||
width: 80px;
|
|
||||||
height: 60px;
|
|
||||||
object-fit: cover;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-video {
|
|
||||||
width: 120px;
|
|
||||||
height: 80px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-img {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-ellipsis-two-lines {
|
|
||||||
/* 必须设置宽度(继承表格列宽,也可手动指定) */
|
|
||||||
width: 100%;
|
|
||||||
/* 核心属性:多行文本溢出隐藏 */
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 2; /* 显示2行 */
|
|
||||||
line-clamp: 2; /* 标准属性,兼容现代浏览器 */
|
|
||||||
overflow: hidden;
|
|
||||||
/* 可选:调整行高,优化显示 */
|
|
||||||
line-height: 1.5;
|
|
||||||
max-height: 3em; /* 行高1.5 × 2行 = 3em,防止高度溢出 */
|
|
||||||
/* 可选:文本对齐 */
|
|
||||||
text-align: center;
|
|
||||||
/* 解决scoped样式穿透(如果需要) */
|
|
||||||
/deep/ & {
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 0 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="90px">
|
||||||
|
<el-form-item label="关联订单号" prop="relationOrderNo">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.relationOrderNo"
|
||||||
|
placeholder="支持模糊查询,充值/退款订单号"
|
||||||
|
clearable
|
||||||
|
style="width: 240px"
|
||||||
|
@keyup.enter.native="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="团队" prop="deptId">
|
||||||
|
<el-select
|
||||||
|
v-model="queryParams.deptId"
|
||||||
|
placeholder="全部团队(可选)"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
style="width: 220px"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in secondLevelDeptOptions"
|
||||||
|
:key="item.deptId"
|
||||||
|
:label="item.deptName"
|
||||||
|
:value="item.deptId"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="类型" prop="type">
|
||||||
|
<el-select v-model="queryParams.type" clearable placeholder="全部类型" style="width: 112px">
|
||||||
|
<el-option v-for="item in typeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="创建时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
style="width: 240px"
|
||||||
|
value-format="yyyy-MM-dd"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="-"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
icon="el-icon-download"
|
||||||
|
size="mini"
|
||||||
|
@click="handleExport"
|
||||||
|
v-hasPermi="['ai:record:export']"
|
||||||
|
>导出</el-button>
|
||||||
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="recordList">
|
||||||
|
<el-table-column label="ID" align="center" prop="id" width="100"/>
|
||||||
|
<el-table-column label="关联订单号" align="center" prop="relationOrderNo" width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column label="团队名称" align="center" prop="deptName" width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column label="操作类型" align="center" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ typeLabel(scope.row.type) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
label="变更积分"
|
||||||
|
align="center"
|
||||||
|
prop="changeAmount"
|
||||||
|
width="120"
|
||||||
|
:formatter="formatChangeAmount"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
label="变更后积分"
|
||||||
|
align="center"
|
||||||
|
prop="resultAmount"
|
||||||
|
width="120"
|
||||||
|
:formatter="formatResultAmount"
|
||||||
|
/>
|
||||||
|
<el-table-column label="备注" align="center" prop="remark" />
|
||||||
|
<el-table-column label="创建时间" align="center" prop="createTime" width="160">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination
|
||||||
|
v-show="total>0"
|
||||||
|
:total="total"
|
||||||
|
:page.sync="queryParams.pageNum"
|
||||||
|
:limit.sync="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listRecord } from "@/api/ai/record"
|
||||||
|
import secondLevelDeptFilter from "@/mixins/secondLevelDeptFilter"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Record",
|
||||||
|
mixins: [secondLevelDeptFilter],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
showSearch: true,
|
||||||
|
dateRange: [],
|
||||||
|
total: 0,
|
||||||
|
recordList: [],
|
||||||
|
typeOptions: [
|
||||||
|
{ label: "充值", value: 0 },
|
||||||
|
{ label: "退款", value: 1 },
|
||||||
|
{ label: "下发", value: 2 },
|
||||||
|
{ label: "回收", value: 3 },
|
||||||
|
{ label: "手动修改", value: 4 }
|
||||||
|
],
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
relationOrderNo: null,
|
||||||
|
deptId: null,
|
||||||
|
type: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.loadSecondLevelDeptOptions()
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/** 西式千分位,积分为整数 */
|
||||||
|
formatPointsWestern(value) {
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
const n = Number(value)
|
||||||
|
if (Number.isNaN(n)) {
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
return n.toLocaleString("en-US", { maximumFractionDigits: 0 })
|
||||||
|
},
|
||||||
|
formatChangeAmount(row) {
|
||||||
|
const value = row.changeAmount
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
const n = Number(value)
|
||||||
|
if (Number.isNaN(n)) {
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
const formatted = Math.abs(n).toLocaleString("en-US", { maximumFractionDigits: 0 })
|
||||||
|
if (n > 0) {
|
||||||
|
return `+${formatted}`
|
||||||
|
}
|
||||||
|
if (n < 0) {
|
||||||
|
return `-${formatted}`
|
||||||
|
}
|
||||||
|
return "0"
|
||||||
|
},
|
||||||
|
formatResultAmount(row) {
|
||||||
|
return this.formatPointsWestern(row.resultAmount)
|
||||||
|
},
|
||||||
|
typeLabel(type) {
|
||||||
|
const n = type !== undefined && type !== null ? Number(type) : null
|
||||||
|
const hit = this.typeOptions.find(item => item.value === n)
|
||||||
|
return hit ? hit.label : "-"
|
||||||
|
},
|
||||||
|
getList() {
|
||||||
|
this.loading = true
|
||||||
|
listRecord(this.addDateRange(this.queryParams, this.dateRange, "CreateTime")).then(response => {
|
||||||
|
this.recordList = response.rows
|
||||||
|
this.total = response.total
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleQuery() {
|
||||||
|
this.queryParams.pageNum = 1
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
resetQuery() {
|
||||||
|
this.dateRange = []
|
||||||
|
this.resetForm("queryForm")
|
||||||
|
this.handleQuery()
|
||||||
|
},
|
||||||
|
handleExport() {
|
||||||
|
this.download(
|
||||||
|
'ai/record/export',
|
||||||
|
this.addDateRange({ ...this.queryParams }, this.dateRange, 'CreateTime'),
|
||||||
|
`record_${new Date().getTime()}.xlsx`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -8,40 +8,26 @@
|
||||||
v-show="showSearch"
|
v-show="showSearch"
|
||||||
label-width="68px"
|
label-width="68px"
|
||||||
>
|
>
|
||||||
<el-form-item label="主键ID" prop="id">
|
|
||||||
<el-input
|
|
||||||
v-model="queryParams.id"
|
|
||||||
placeholder="请输入主键ID"
|
|
||||||
clearable
|
|
||||||
@keyup.enter.native="handleQuery"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="用户ID" prop="userId">
|
<el-form-item label="用户ID" prop="userId">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="queryParams.userId"
|
v-model="queryParams.userId"
|
||||||
placeholder="请输入用户ID"
|
placeholder="请输入用户ID"
|
||||||
clearable
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
@keyup.enter.native="handleQuery"
|
@keyup.enter.native="handleQuery"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="用户账号" prop="username">
|
<el-form-item label="账号" prop="username">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="queryParams.username"
|
v-model="queryParams.username"
|
||||||
placeholder="请输入用户账号"
|
placeholder="请输入账号"
|
||||||
clearable
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
@keyup.enter.native="handleQuery"
|
@keyup.enter.native="handleQuery"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="上级ID" prop="superisuperiorUuidorId">
|
<el-form-item label="状态" prop="status" label-width="40px">
|
||||||
<el-input
|
<el-select v-model="queryParams.status" placeholder="用户状态" clearable style="width: 80px">
|
||||||
v-model="queryParams.superiorUuid"
|
|
||||||
placeholder="请输入上级ID"
|
|
||||||
clearable
|
|
||||||
@keyup.enter.native="handleQuery"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="状态" prop="status">
|
|
||||||
<el-select v-model="queryParams.status" placeholder="用户状态" clearable style="width: 240px">
|
|
||||||
<el-option
|
<el-option
|
||||||
v-for="dict in dict.type.sys_normal_disable"
|
v-for="dict in dict.type.sys_normal_disable"
|
||||||
:key="dict.value"
|
:key="dict.value"
|
||||||
|
|
@ -50,14 +36,36 @@
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="归属团队" prop="deptId">
|
||||||
|
<treeselect
|
||||||
|
v-model="queryParams.deptId"
|
||||||
|
:options="deptOptions"
|
||||||
|
:normalizer="deptNormalizer"
|
||||||
|
placeholder="全部"
|
||||||
|
clearable
|
||||||
|
style="width: 220px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
</el-form-item>
|
<el-button
|
||||||
</el-form>
|
type="primary"
|
||||||
|
plain
|
||||||
<el-row :gutter="10" class="mb8">
|
icon="el-icon-plus"
|
||||||
<el-col :span="1.5">
|
size="mini"
|
||||||
|
@click="handleAdd"
|
||||||
|
v-hasPermi="['ai:user:add']"
|
||||||
|
>新增</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
icon="el-icon-delete"
|
||||||
|
size="mini"
|
||||||
|
:disabled="multiple"
|
||||||
|
@click="handleDelete()"
|
||||||
|
v-hasPermi="['ai:user:remove']"
|
||||||
|
>删除</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="warning"
|
type="warning"
|
||||||
plain
|
plain
|
||||||
|
|
@ -66,18 +74,22 @@
|
||||||
@click="handleExport"
|
@click="handleExport"
|
||||||
v-hasPermi="['ai:user:export']"
|
v-hasPermi="['ai:user:export']"
|
||||||
>导出</el-button>
|
>导出</el-button>
|
||||||
</el-col>
|
|
||||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||||
</el-row>
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
<el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
|
<el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
|
||||||
<el-table-column type="selection" width="55" align="center" />
|
<el-table-column type="selection" width="50" align="center" />
|
||||||
<el-table-column label="主键ID" align="center" prop="id" width="90" />
|
<el-table-column label="主键ID" align="center" prop="id" width="60" />
|
||||||
<el-table-column label="用户ID" align="center" prop="userId" />
|
<el-table-column label="用户ID" align="center" prop="userId" />
|
||||||
<el-table-column label="用户账号" align="center" prop="username" />
|
<el-table-column label="账号" align="center" prop="username" />
|
||||||
|
<!--
|
||||||
<el-table-column label="上级ID" align="center" prop="superiorUuid" />
|
<el-table-column label="上级ID" align="center" prop="superiorUuid" />
|
||||||
<el-table-column label="上级账号" align="center" prop="superiorName" />
|
<el-table-column label="上级账号" align="center" prop="superiorName" />
|
||||||
|
-->
|
||||||
<el-table-column label="用户昵称" align="center" prop="nickname" />
|
<el-table-column label="用户昵称" align="center" prop="nickname" />
|
||||||
|
<el-table-column label="归属团队" align="center" prop="deptName" width="120" show-overflow-tooltip />
|
||||||
|
<!--
|
||||||
<el-table-column label="邮箱" align="center" prop="email" />
|
<el-table-column label="邮箱" align="center" prop="email" />
|
||||||
<el-table-column label="性别" align="center" prop="gender">
|
<el-table-column label="性别" align="center" prop="gender">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
|
|
@ -85,7 +97,8 @@
|
||||||
<el-tag v-else type="danger">女</el-tag>
|
<el-tag v-else type="danger">女</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="状态" align="center" key="status">
|
-->
|
||||||
|
<el-table-column label="状态" align="center" key="status" width="60">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="scope.row.status"
|
v-model="scope.row.status"
|
||||||
|
|
@ -95,16 +108,28 @@
|
||||||
></el-switch>
|
></el-switch>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<!--
|
||||||
<el-table-column label="邀请码" align="center" prop="invitationCode" />
|
<el-table-column label="邀请码" align="center" prop="invitationCode" />
|
||||||
|
-->
|
||||||
<el-table-column label="登录时间" align="center" prop="loginTime" width="150">
|
<el-table-column label="登录时间" align="center" prop="loginTime" width="150">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<span>{{ parseTime(scope.row.loginTime, '{y}-{m}-{d}') }}</span>
|
<span>{{ parseTime(scope.row.loginTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="余额" align="center" prop="balance" />
|
<el-table-column label="剩余积分" align="center" prop="balance" width="120">
|
||||||
<el-table-column label="source" align="center" prop="source" />
|
|
||||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="180">
|
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
|
<span>{{ formatThousands(scope.row.balance) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="460">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-office-building"
|
||||||
|
@click="handleOpenAssignDept(scope.row)"
|
||||||
|
v-hasPermi="['ai:user:edit']"
|
||||||
|
>分配团队</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
size="mini"
|
size="mini"
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -112,6 +137,21 @@
|
||||||
@click="updatePassword(scope.row)"
|
@click="updatePassword(scope.row)"
|
||||||
v-hasPermi="['ai:user:edit']"
|
v-hasPermi="['ai:user:edit']"
|
||||||
>修改密码</el-button>
|
>修改密码</el-button>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-download"
|
||||||
|
@click="openDeptScoreDialog(scope.row, 'issue')"
|
||||||
|
v-hasPermi="['ai:user:deptScoreIssue']"
|
||||||
|
>下放积分</el-button>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-upload2"
|
||||||
|
@click="openDeptScoreDialog(scope.row, 'reclaim')"
|
||||||
|
v-hasPermi="['ai:user:deptScoreReclaim']"
|
||||||
|
>回收积分</el-button>
|
||||||
|
<!-- 修改余额功能已关闭
|
||||||
<el-button
|
<el-button
|
||||||
size="mini"
|
size="mini"
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -119,6 +159,21 @@
|
||||||
@click="updateBalance(scope.row)"
|
@click="updateBalance(scope.row)"
|
||||||
v-hasPermi="['ai:user:remove']"
|
v-hasPermi="['ai:user:remove']"
|
||||||
>修改余额</el-button>
|
>修改余额</el-button>
|
||||||
|
-->
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-edit-outline"
|
||||||
|
@click="handleUpdate(scope.row)"
|
||||||
|
v-hasPermi="['ai:user:edit']"
|
||||||
|
>修改</el-button>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
@click="handleDelete(scope.row)"
|
||||||
|
v-hasPermi="['ai:user:remove']"
|
||||||
|
>删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
@ -132,8 +187,24 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 添加或修改ai-用户信息对话框 -->
|
<!-- 添加或修改ai-用户信息对话框 -->
|
||||||
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
|
<el-dialog :title="title" :visible.sync="open" width="520px" append-to-body>
|
||||||
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
<el-form ref="form" :model="form" :rules="rules" label-width="88px">
|
||||||
|
<el-form-item label="用户账号" prop="username">
|
||||||
|
<el-input
|
||||||
|
v-model="form.username"
|
||||||
|
placeholder="门户登录账号,必填"
|
||||||
|
:disabled="form.id != null"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码" prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="form.password"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
:placeholder="form.id != null ? '不填表示不修改密码' : '请输入登录密码'"
|
||||||
|
autocomplete="new-password"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="用户昵称" prop="nickname">
|
<el-form-item label="用户昵称" prop="nickname">
|
||||||
<el-input v-model="form.nickname" placeholder="请输入用户昵称" />
|
<el-input v-model="form.nickname" placeholder="请输入用户昵称" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
@ -155,13 +226,16 @@
|
||||||
placeholder="请选择生日"
|
placeholder="请选择生日"
|
||||||
></el-date-picker>
|
></el-date-picker>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="余额" prop="balance">
|
<el-form-item v-if="form.id != null" label="剩余积分" prop="balance">
|
||||||
<el-input v-model="form.balance" placeholder="请输入余额" />
|
<el-input v-model="form.balance" disabled placeholder="-" />
|
||||||
|
<div style="color: #909399; font-size: 12px; line-height: 1.5; margin-top: 4px">
|
||||||
|
剩余积分仅可通过「下放积分」「回收积分」调整
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
|
||||||
<el-form-item label="备注" prop="remark">
|
<el-form-item label="备注" prop="remark">
|
||||||
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
|
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
<div slot="footer" class="dialog-footer">
|
<div slot="footer" class="dialog-footer">
|
||||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||||
<el-button @click="cancel">取 消</el-button>
|
<el-button @click="cancel">取 消</el-button>
|
||||||
|
|
@ -182,7 +256,69 @@
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 修改余额对话框 -->
|
<!-- 分配团队 -->
|
||||||
|
<el-dialog title="分配归属团队" :visible.sync="assignDeptOpen" width="480px" append-to-body @close="cancelAssignDept">
|
||||||
|
<el-form label-width="88px">
|
||||||
|
<el-form-item label="归属团队">
|
||||||
|
<treeselect
|
||||||
|
v-model="assignForm.deptId"
|
||||||
|
:options="deptOptions"
|
||||||
|
:normalizer="deptNormalizer"
|
||||||
|
placeholder="不选则不归属任何团队"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitAssignDept">确 定</el-button>
|
||||||
|
<el-button @click="cancelAssignDept">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 部门积分下放 / 回收 -->
|
||||||
|
<el-dialog
|
||||||
|
:title="deptScoreMode === 'issue' ? '下放积分' : '回收积分'"
|
||||||
|
:visible.sync="deptScoreOpen"
|
||||||
|
width="480px"
|
||||||
|
append-to-body
|
||||||
|
@close="cancelDeptScore"
|
||||||
|
>
|
||||||
|
<el-form ref="deptScoreFormRef" :model="deptScoreForm" :rules="deptScoreRules" label-width="88px">
|
||||||
|
<el-form-item label="所属团队">
|
||||||
|
<span>{{ deptScoreForm.deptName || "—" }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="用户账号">
|
||||||
|
<span>{{ deptScoreForm.username }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="积分数量" prop="amount">
|
||||||
|
<el-input-number
|
||||||
|
v-model="deptScoreForm.amount"
|
||||||
|
:min="1"
|
||||||
|
:max="100000000"
|
||||||
|
:precision="0"
|
||||||
|
:step="1"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注" prop="remark">
|
||||||
|
<el-input
|
||||||
|
v-model="deptScoreForm.remark"
|
||||||
|
type="textarea"
|
||||||
|
:rows="2"
|
||||||
|
maxlength="50"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="选填,最多50字"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitDeptScore">确 定</el-button>
|
||||||
|
<el-button @click="cancelDeptScore">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 修改余额功能已关闭
|
||||||
<el-dialog :title="title" :visible.sync="openUpdateBalance" width="500px" append-to-body>
|
<el-dialog :title="title" :visible.sync="openUpdateBalance" width="500px" append-to-body>
|
||||||
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
||||||
<el-form-item label="余额" prop="balance">
|
<el-form-item label="余额" prop="balance">
|
||||||
|
|
@ -195,6 +331,7 @@
|
||||||
<el-button @click="cancelBalance">取 消</el-button>
|
<el-button @click="cancelBalance">取 消</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
-->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -205,14 +342,20 @@ import {
|
||||||
delUser,
|
delUser,
|
||||||
addUser,
|
addUser,
|
||||||
updateUser,
|
updateUser,
|
||||||
changeBalance,
|
|
||||||
changeUserStatus,
|
changeUserStatus,
|
||||||
updatePassword
|
updatePassword,
|
||||||
|
assignAiUserDept,
|
||||||
|
issueDeptScore,
|
||||||
|
reclaimDeptScore
|
||||||
} from "@/api/ai/user";
|
} from "@/api/ai/user";
|
||||||
|
import { listDept } from "@/api/ai/dept";
|
||||||
|
import Treeselect from "@riophae/vue-treeselect";
|
||||||
|
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "User",
|
name: "User",
|
||||||
dicts: ["sys_normal_disable", "sys_user_sex"],
|
dicts: ["sys_normal_disable", "sys_user_sex"],
|
||||||
|
components: { Treeselect },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// 遮罩层
|
// 遮罩层
|
||||||
|
|
@ -234,7 +377,34 @@ export default {
|
||||||
// 是否显示弹出层
|
// 是否显示弹出层
|
||||||
open: false,
|
open: false,
|
||||||
openUpdatePassword: false,
|
openUpdatePassword: false,
|
||||||
openUpdateBalance: false,
|
deptScoreOpen: false,
|
||||||
|
deptScoreMode: "issue",
|
||||||
|
deptScoreForm: {
|
||||||
|
userId: null,
|
||||||
|
username: "",
|
||||||
|
deptName: "",
|
||||||
|
amount: undefined,
|
||||||
|
remark: ""
|
||||||
|
},
|
||||||
|
deptScoreRules: {
|
||||||
|
amount: [
|
||||||
|
{ required: true, message: "请输入积分数量", trigger: "blur" },
|
||||||
|
{
|
||||||
|
type: "number",
|
||||||
|
min: 1,
|
||||||
|
max: 100000000,
|
||||||
|
message: "请输入 1~100000000 的整数",
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
remark: [{ max: 50, message: "备注最多50个字", trigger: "blur" }]
|
||||||
|
},
|
||||||
|
assignDeptOpen: false,
|
||||||
|
deptOptions: [],
|
||||||
|
assignForm: {
|
||||||
|
id: null,
|
||||||
|
deptId: null
|
||||||
|
},
|
||||||
// 查询参数
|
// 查询参数
|
||||||
queryParams: {
|
queryParams: {
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
|
|
@ -245,41 +415,100 @@ export default {
|
||||||
phone: null,
|
phone: null,
|
||||||
password: null,
|
password: null,
|
||||||
openid: null,
|
openid: null,
|
||||||
status: null,
|
status: "0",
|
||||||
email: null,
|
email: null,
|
||||||
birthday: null,
|
birthday: null,
|
||||||
invitationCode: null,
|
invitationCode: null,
|
||||||
paymentUrl: null,
|
paymentUrl: null,
|
||||||
loginTime: null,
|
loginTime: null,
|
||||||
balance: null,
|
balance: null,
|
||||||
superiorName: null
|
superiorName: null,
|
||||||
|
deptId: null
|
||||||
},
|
},
|
||||||
// 表单参数
|
// 表单参数
|
||||||
form: {},
|
form: {},
|
||||||
// 表单校验
|
// 表单校验
|
||||||
rules: {
|
rules: {
|
||||||
delFlag: [
|
username: [{ required: true, message: "用户账号不能为空", trigger: "blur" }],
|
||||||
{ required: true, message: "删除标志不能为空", trigger: "blur" }
|
password: [
|
||||||
],
|
{
|
||||||
phone: [
|
validator: (rule, value, callback) => {
|
||||||
{ required: true, message: "手机号码不能为空", trigger: "blur" }
|
if (this.form.id == null && (!value || !String(value).trim())) {
|
||||||
|
callback(new Error("密码不能为空"));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
this.loadDeptTree();
|
||||||
this.getList();
|
this.getList();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** 千位分节(en-US),整数;空为 — */
|
||||||
|
formatThousands(value) {
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
const n = Number(value);
|
||||||
|
if (Number.isNaN(n)) {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
return n.toLocaleString("en-US", { maximumFractionDigits: 0 });
|
||||||
|
},
|
||||||
|
loadDeptTree() {
|
||||||
|
listDept().then(res => {
|
||||||
|
this.deptOptions = this.handleTree(res.data, "deptId");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deptNormalizer(node) {
|
||||||
|
if (node.children && !node.children.length) {
|
||||||
|
delete node.children;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: node.deptId,
|
||||||
|
label: node.deptName,
|
||||||
|
children: node.children
|
||||||
|
};
|
||||||
|
},
|
||||||
|
handleOpenAssignDept(row) {
|
||||||
|
getUser(row.id).then(res => {
|
||||||
|
this.assignForm = {
|
||||||
|
id: res.data.id,
|
||||||
|
deptId: res.data.deptId
|
||||||
|
};
|
||||||
|
this.assignDeptOpen = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
submitAssignDept() {
|
||||||
|
const payload = {
|
||||||
|
id: this.assignForm.id,
|
||||||
|
deptId: this.assignForm.deptId != null ? this.assignForm.deptId : null
|
||||||
|
};
|
||||||
|
assignAiUserDept(payload).then(() => {
|
||||||
|
this.$modal.msgSuccess("已保存");
|
||||||
|
this.assignDeptOpen = false;
|
||||||
|
this.getList();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
cancelAssignDept() {
|
||||||
|
this.assignDeptOpen = false;
|
||||||
|
this.assignForm = { id: null, deptId: null };
|
||||||
|
},
|
||||||
// 更多操作触发
|
// 更多操作触发
|
||||||
handleCommand(command, row) {
|
handleCommand(command, row) {
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case "updatePassword":
|
case "updatePassword":
|
||||||
this.updatePassword(row);
|
this.updatePassword(row);
|
||||||
break;
|
break;
|
||||||
case "updateBalance":
|
// case "updateBalance":
|
||||||
this.updateBalance(row);
|
// this.updateBalance(row);
|
||||||
break;
|
// break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -297,15 +526,63 @@ export default {
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
updateBalance(row) {
|
openDeptScoreDialog(row, mode) {
|
||||||
this.reset();
|
if (!row.deptId) {
|
||||||
const id = row.id || this.ids;
|
this.$modal.msgWarning("请先分配归属团队后再操作积分");
|
||||||
getUser(id).then(response => {
|
return;
|
||||||
this.form = response.data;
|
}
|
||||||
this.openUpdateBalance = true;
|
this.deptScoreMode = mode;
|
||||||
this.title = "修改余额";
|
this.deptScoreForm = {
|
||||||
|
userId: row.id,
|
||||||
|
username: row.username || row.userId || "",
|
||||||
|
deptName: row.deptName || "",
|
||||||
|
amount: undefined,
|
||||||
|
remark: ""
|
||||||
|
};
|
||||||
|
this.deptScoreOpen = true;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.deptScoreFormRef) {
|
||||||
|
this.$refs.deptScoreFormRef.clearValidate();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
submitDeptScore() {
|
||||||
|
this.$refs.deptScoreFormRef.validate(valid => {
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const raw = this.deptScoreForm.amount;
|
||||||
|
const amount = typeof raw === "number" ? Math.trunc(raw) : parseInt(String(raw), 10);
|
||||||
|
if (!Number.isFinite(amount) || amount < 1 || amount > 100000000) {
|
||||||
|
this.$modal.msgWarning("请输入 1~100000000 的整数积分");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const payload = {
|
||||||
|
userId: this.deptScoreForm.userId,
|
||||||
|
amount,
|
||||||
|
remark: this.deptScoreForm.remark || undefined
|
||||||
|
};
|
||||||
|
const req = this.deptScoreMode === "issue" ? issueDeptScore : reclaimDeptScore;
|
||||||
|
req(payload).then(() => {
|
||||||
|
this.$modal.msgSuccess("操作成功");
|
||||||
|
this.deptScoreOpen = false;
|
||||||
|
this.getList();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
cancelDeptScore() {
|
||||||
|
this.deptScoreOpen = false;
|
||||||
|
this.deptScoreForm = { userId: null, username: "", deptName: "", amount: undefined, remark: "" };
|
||||||
|
},
|
||||||
|
// updateBalance(row) {
|
||||||
|
// this.reset();
|
||||||
|
// const id = row.id || this.ids;
|
||||||
|
// getUser(id).then(response => {
|
||||||
|
// this.form = response.data;
|
||||||
|
// this.openUpdateBalance = true;
|
||||||
|
// this.title = "修改余额";
|
||||||
|
// });
|
||||||
|
// },
|
||||||
/** 查询ai-用户信息列表 */
|
/** 查询ai-用户信息列表 */
|
||||||
getList() {
|
getList() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
@ -335,10 +612,10 @@ export default {
|
||||||
this.open = false;
|
this.open = false;
|
||||||
this.reset();
|
this.reset();
|
||||||
},
|
},
|
||||||
cancelBalance() {
|
// cancelBalance() {
|
||||||
this.openUpdateBalance = false;
|
// this.openUpdateBalance = false;
|
||||||
this.reset();
|
// this.reset();
|
||||||
},
|
// },
|
||||||
cancelPassword() {
|
cancelPassword() {
|
||||||
this.openUpdatePassword = false;
|
this.openUpdatePassword = false;
|
||||||
this.reset();
|
this.reset();
|
||||||
|
|
@ -353,6 +630,7 @@ export default {
|
||||||
updateBy: null,
|
updateBy: null,
|
||||||
updateTime: null,
|
updateTime: null,
|
||||||
remark: null,
|
remark: null,
|
||||||
|
username: null,
|
||||||
nickname: null,
|
nickname: null,
|
||||||
gender: null,
|
gender: null,
|
||||||
avatar: null,
|
avatar: null,
|
||||||
|
|
@ -407,13 +685,19 @@ export default {
|
||||||
this.$refs["form"].validate(valid => {
|
this.$refs["form"].validate(valid => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
if (this.form.id != null) {
|
if (this.form.id != null) {
|
||||||
updateUser(this.form).then(response => {
|
const payload = { ...this.form };
|
||||||
|
if (!payload.password || !String(payload.password).trim()) {
|
||||||
|
delete payload.password;
|
||||||
|
}
|
||||||
|
updateUser(payload).then(() => {
|
||||||
this.$modal.msgSuccess("修改成功");
|
this.$modal.msgSuccess("修改成功");
|
||||||
this.open = false;
|
this.open = false;
|
||||||
this.getList();
|
this.getList();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
addUser(this.form).then(response => {
|
const payload = { ...this.form };
|
||||||
|
delete payload.balance;
|
||||||
|
addUser(payload).then(() => {
|
||||||
this.$modal.msgSuccess("新增成功");
|
this.$modal.msgSuccess("新增成功");
|
||||||
this.open = false;
|
this.open = false;
|
||||||
this.getList();
|
this.getList();
|
||||||
|
|
@ -422,23 +706,28 @@ export default {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleChangeBalance() {
|
// handleChangeBalance() {
|
||||||
changeBalance;
|
// this.$refs["form"].validate(valid => {
|
||||||
this.$refs["form"].validate(valid => {
|
// if (!valid) return;
|
||||||
changeBalance(this.form.id, this.form.balance).then(response => {
|
// changeBalance(this.form.id, this.form.balance).then(() => {
|
||||||
this.$modal.msgSuccess("修改成功");
|
// this.$modal.msgSuccess("修改成功");
|
||||||
this.openUpdateBalance = false;
|
// this.openUpdateBalance = false;
|
||||||
this.getList();
|
// this.getList();
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
},
|
// },
|
||||||
/** 删除按钮操作 */
|
/** 删除按钮操作 */
|
||||||
handleDelete(row) {
|
handleDelete(row) {
|
||||||
const ids = row.id || this.ids;
|
const ids = row && row.id != null ? row.id : this.ids;
|
||||||
|
if (ids == null || (Array.isArray(ids) && !ids.length)) {
|
||||||
|
this.$modal.msgWarning("请选择要删除的数据");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const idParam = Array.isArray(ids) ? ids.join(",") : ids;
|
||||||
this.$modal
|
this.$modal
|
||||||
.confirm('是否确认删除ai-用户信息编号为"' + ids + '"的数据项?')
|
.confirm('是否确认删除ai-用户信息编号为"' + idParam + '"的数据项?')
|
||||||
.then(function() {
|
.then(() => {
|
||||||
return delUser(ids);
|
return delUser(idParam);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.getList();
|
this.getList();
|
||||||
|
|
|
||||||
|
|
@ -218,7 +218,7 @@
|
||||||
<!-- </el-collapse-item>-->
|
<!-- </el-collapse-item>-->
|
||||||
<!-- <el-collapse-item title="v3.8.7 - 2023-12-08">-->
|
<!-- <el-collapse-item title="v3.8.7 - 2023-12-08">-->
|
||||||
<!-- <ol>-->
|
<!-- <ol>-->
|
||||||
<!-- <li>操作日志记录部门名称</li>-->
|
<!-- <li>操作日志记录团队名称</li>-->
|
||||||
<!-- <li>全局数据存储用户编号</li>-->
|
<!-- <li>全局数据存储用户编号</li>-->
|
||||||
<!-- <li>新增编程式判断资源访问权限</li>-->
|
<!-- <li>新增编程式判断资源访问权限</li>-->
|
||||||
<!-- <li>操作日志列表新增IP地址查询</li>-->
|
<!-- <li>操作日志列表新增IP地址查询</li>-->
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="会话编号" align="center" prop="tokenId" :show-overflow-tooltip="true" />
|
<el-table-column label="会话编号" align="center" prop="tokenId" :show-overflow-tooltip="true" />
|
||||||
<el-table-column label="登录名称" align="center" prop="userName" :show-overflow-tooltip="true" />
|
<el-table-column label="登录名称" align="center" prop="userName" :show-overflow-tooltip="true" />
|
||||||
<el-table-column label="部门名称" align="center" prop="deptName" />
|
<el-table-column label="团队名称" align="center" prop="deptName" />
|
||||||
<el-table-column label="主机" align="center" prop="ipaddr" :show-overflow-tooltip="true" />
|
<el-table-column label="主机" align="center" prop="ipaddr" :show-overflow-tooltip="true" />
|
||||||
<el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
|
<el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
|
||||||
<el-table-column label="浏览器" align="center" prop="browser" />
|
<el-table-column label="浏览器" align="center" prop="browser" />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,182 @@
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="70px">
|
||||||
|
<el-form-item label="订单号" prop="orderNum">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.orderNum"
|
||||||
|
placeholder="支持模糊搜索"
|
||||||
|
clearable
|
||||||
|
@keyup.enter.native="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="类型" prop="orderType">
|
||||||
|
<el-select v-model="queryParams.orderType" placeholder="全部" clearable style="width: 110px">
|
||||||
|
<el-option
|
||||||
|
v-for="item in orderTypeQueryOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="创建时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
style="width: 240px"
|
||||||
|
value-format="yyyy-MM-dd"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="-"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
icon="el-icon-download"
|
||||||
|
size="mini"
|
||||||
|
@click="handleExport"
|
||||||
|
v-hasPermi="['subteam:charge:export']"
|
||||||
|
>导出</el-button>
|
||||||
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="groupChargeOrderList">
|
||||||
|
<el-table-column label="订单号" align="center" prop="orderNum" width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column label="类型" align="center" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ orderTypeLabel(scope.row.orderType) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="金额(元)" align="center" prop="money" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ formatTableMoney(scope.row.money) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="积分" align="center" prop="amount" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ formatTableAmount(scope.row.amount) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" align="center" width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ statusLabel(scope.row.status) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip />
|
||||||
|
<el-table-column label="创建时间" align="center" prop="createTime" width="150">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination
|
||||||
|
v-show="total > 0"
|
||||||
|
:total="total"
|
||||||
|
:page.sync="queryParams.pageNum"
|
||||||
|
:limit.sync="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listSubteamChargeOrder, subteamChargeOrderExportUrl } from '@/api/subteam'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SubteamChargeOrder',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
showSearch: true,
|
||||||
|
dateRange: [],
|
||||||
|
total: 0,
|
||||||
|
groupChargeOrderList: [],
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
orderNum: null,
|
||||||
|
orderType: null
|
||||||
|
},
|
||||||
|
orderTypeQueryOptions: [
|
||||||
|
{ label: '充值', value: 0 },
|
||||||
|
{ label: '退款', value: 1 }
|
||||||
|
],
|
||||||
|
orderTypeOptions: [
|
||||||
|
{ label: '充值', value: 0 },
|
||||||
|
{ label: '退款', value: 1 },
|
||||||
|
{ label: '手动修改', value: 2 }
|
||||||
|
],
|
||||||
|
statusOptions: [
|
||||||
|
{ label: '进行中', value: 0 },
|
||||||
|
{ label: '已完成', value: 1 },
|
||||||
|
{ label: '失败', value: 2 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatTableMoney(value) {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return '—'
|
||||||
|
}
|
||||||
|
const n = Number(value)
|
||||||
|
if (Number.isNaN(n)) {
|
||||||
|
return '—'
|
||||||
|
}
|
||||||
|
return n.toLocaleString('en-US', { maximumFractionDigits: 2, minimumFractionDigits: 0 })
|
||||||
|
},
|
||||||
|
formatTableAmount(value) {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return '—'
|
||||||
|
}
|
||||||
|
const n = Number(value)
|
||||||
|
if (Number.isNaN(n)) {
|
||||||
|
return '—'
|
||||||
|
}
|
||||||
|
return n.toLocaleString('en-US', { maximumFractionDigits: 0 })
|
||||||
|
},
|
||||||
|
orderTypeLabel(orderType) {
|
||||||
|
const n = orderType !== undefined && orderType !== null ? Number(orderType) : null
|
||||||
|
const hit = this.orderTypeOptions.find(o => o.value === n)
|
||||||
|
return hit ? hit.label : '—'
|
||||||
|
},
|
||||||
|
statusLabel(status) {
|
||||||
|
const n = status !== undefined && status !== null ? Number(status) : null
|
||||||
|
const hit = this.statusOptions.find(o => o.value === n)
|
||||||
|
return hit ? hit.label : '—'
|
||||||
|
},
|
||||||
|
getList() {
|
||||||
|
this.loading = true
|
||||||
|
listSubteamChargeOrder(this.addDateRange(this.queryParams, this.dateRange, 'CreateTime')).then(response => {
|
||||||
|
this.groupChargeOrderList = response.rows
|
||||||
|
this.total = response.total
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleQuery() {
|
||||||
|
this.queryParams.pageNum = 1
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
resetQuery() {
|
||||||
|
this.dateRange = []
|
||||||
|
this.resetForm('queryForm')
|
||||||
|
this.handleQuery()
|
||||||
|
},
|
||||||
|
handleExport() {
|
||||||
|
this.download(
|
||||||
|
subteamChargeOrderExportUrl,
|
||||||
|
this.addDateRange({ ...this.queryParams }, this.dateRange, 'CreateTime'),
|
||||||
|
`group_charge_order_${new Date().getTime()}.xlsx`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,258 @@
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="96px">
|
||||||
|
<el-form-item label="统计日期">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
unlink-panels
|
||||||
|
value-format="yyyy-MM-dd"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
style="width: 280px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
icon="el-icon-download"
|
||||||
|
size="mini"
|
||||||
|
@click="handleExport"
|
||||||
|
v-hasPermi="['subteam:consume:export']"
|
||||||
|
>导出</el-button>
|
||||||
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<p class="summary-line">
|
||||||
|
<template v-if="summaryState.mode === 'loading'">
|
||||||
|
<span> </span>
|
||||||
|
<span class="summary-prefix">汇总:</span><span class="summary-muted">加载中…</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="summaryState.mode === 'empty'">
|
||||||
|
<span> </span>
|
||||||
|
<span class="summary-prefix">汇总:</span><span class="summary-muted">—</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span> </span>
|
||||||
|
<span class="summary-prefix">汇总:</span>
|
||||||
|
<span class="summary-label">总计</span>
|
||||||
|
<span class="summary-num">{{ summaryState.totalStr }}</span>
|
||||||
|
<span class="summary-label">条记录,实际充值积分</span>
|
||||||
|
<span class="summary-num">{{ summaryState.recharge }}</span>
|
||||||
|
<span class="summary-unit">分</span>
|
||||||
|
<span class="summary-label">,消耗积分</span>
|
||||||
|
<span class="summary-num">{{ summaryState.score }}</span>
|
||||||
|
<span class="summary-unit">分</span>
|
||||||
|
<span class="summary-label">,实际订单数量(成功)</span>
|
||||||
|
<span class="summary-num">{{ summaryState.orders }}</span>
|
||||||
|
<span class="summary-unit">单</span>
|
||||||
|
<span class="summary-label">,三方消耗tokens数量</span>
|
||||||
|
<span class="summary-num">{{ summaryState.tokens }}</span>
|
||||||
|
<span class="summary-unit">。</span>
|
||||||
|
</template>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="dataList">
|
||||||
|
<el-table-column label="日期" align="center" prop="dateKey" min-width="110" />
|
||||||
|
<el-table-column
|
||||||
|
label="实际充值积分(充值-退款)"
|
||||||
|
align="center"
|
||||||
|
prop="rechargeScore"
|
||||||
|
min-width="200"
|
||||||
|
:formatter="formatWesternNumber"
|
||||||
|
/>
|
||||||
|
<el-table-column label="消耗积分" align="center" prop="score" width="120" :formatter="formatWesternNumber" />
|
||||||
|
<el-table-column
|
||||||
|
label="实际订单数量(成功)"
|
||||||
|
align="center"
|
||||||
|
prop="orderCount"
|
||||||
|
width="160"
|
||||||
|
:formatter="formatWesternNumber"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
label="三方消耗tokens数量"
|
||||||
|
align="center"
|
||||||
|
prop="useTokens"
|
||||||
|
width="170"
|
||||||
|
:formatter="formatWesternNumber"
|
||||||
|
/>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination
|
||||||
|
v-show="total>0"
|
||||||
|
:total="total"
|
||||||
|
:page.sync="queryParams.pageNum"
|
||||||
|
:limit.sync="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listSubteamConsumeStat, subteamConsumeStatExportUrl } from '@/api/subteam'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SubteamConsumeStat',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
showSearch: true,
|
||||||
|
total: 0,
|
||||||
|
dataList: [],
|
||||||
|
aggregate: null,
|
||||||
|
dateRange: [],
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
startDate: null,
|
||||||
|
endDate: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.initDefaultDateRange()
|
||||||
|
this.syncDateRangeToQuery()
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
summaryState() {
|
||||||
|
if (this.loading) {
|
||||||
|
return { mode: 'loading' }
|
||||||
|
}
|
||||||
|
const a = this.aggregate
|
||||||
|
if (!a) {
|
||||||
|
return { mode: 'empty' }
|
||||||
|
}
|
||||||
|
const fmt = (v) => this.formatWesternNumberValue(v)
|
||||||
|
const n = a.totalRows != null ? Number(a.totalRows) : 0
|
||||||
|
const totalStr = Number.isNaN(n) ? '0' : n.toLocaleString('en-US')
|
||||||
|
return {
|
||||||
|
mode: 'ok',
|
||||||
|
totalStr,
|
||||||
|
recharge: fmt(a.sumRechargeScore),
|
||||||
|
score: fmt(a.sumScore),
|
||||||
|
orders: fmt(a.sumOrderCount),
|
||||||
|
tokens: fmt(a.sumUseTokens)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatWesternNumberValue(cellValue) {
|
||||||
|
if (cellValue === null || cellValue === undefined || cellValue === '') {
|
||||||
|
return (0).toLocaleString('en-US')
|
||||||
|
}
|
||||||
|
const n = Number(cellValue)
|
||||||
|
if (Number.isNaN(n)) {
|
||||||
|
return (0).toLocaleString('en-US')
|
||||||
|
}
|
||||||
|
return n.toLocaleString('en-US')
|
||||||
|
},
|
||||||
|
formatWesternNumber(row, column, cellValue) {
|
||||||
|
return this.formatWesternNumberValue(cellValue)
|
||||||
|
},
|
||||||
|
buildLastMonthDateRange() {
|
||||||
|
const end = new Date()
|
||||||
|
const start = new Date(end.getTime())
|
||||||
|
start.setMonth(start.getMonth() - 1)
|
||||||
|
return [this.formatYmd(start), this.formatYmd(end)]
|
||||||
|
},
|
||||||
|
formatYmd(d) {
|
||||||
|
const y = d.getFullYear()
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(d.getDate()).padStart(2, '0')
|
||||||
|
return `${y}-${m}-${day}`
|
||||||
|
},
|
||||||
|
initDefaultDateRange() {
|
||||||
|
this.dateRange = this.buildLastMonthDateRange()
|
||||||
|
},
|
||||||
|
syncDateRangeToQuery() {
|
||||||
|
if (Array.isArray(this.dateRange) && this.dateRange.length === 2) {
|
||||||
|
this.queryParams.startDate = this.dateRange[0]
|
||||||
|
this.queryParams.endDate = this.dateRange[1]
|
||||||
|
} else {
|
||||||
|
this.queryParams.startDate = null
|
||||||
|
this.queryParams.endDate = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getList() {
|
||||||
|
this.syncDateRangeToQuery()
|
||||||
|
this.loading = true
|
||||||
|
listSubteamConsumeStat(this.queryParams)
|
||||||
|
.then(response => {
|
||||||
|
this.dataList = response.rows || []
|
||||||
|
this.total = response.total != null ? response.total : 0
|
||||||
|
this.aggregate =
|
||||||
|
response.param && response.param.aggregate != null ? response.param.aggregate : null
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleQuery() {
|
||||||
|
this.queryParams.pageNum = 1
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
resetQuery() {
|
||||||
|
this.initDefaultDateRange()
|
||||||
|
this.resetForm('queryForm')
|
||||||
|
this.queryParams.pageNum = 1
|
||||||
|
this.syncDateRangeToQuery()
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
handleExport() {
|
||||||
|
this.syncDateRangeToQuery()
|
||||||
|
this.download(
|
||||||
|
subteamConsumeStatExportUrl,
|
||||||
|
{
|
||||||
|
startDate: this.queryParams.startDate,
|
||||||
|
endDate: this.queryParams.endDate
|
||||||
|
},
|
||||||
|
`team_consume_${new Date().getTime()}.xlsx`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.summary-line {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
padding-left: 8px;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-prefix {
|
||||||
|
color: #909399;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-muted {
|
||||||
|
color: #c0c4cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-label {
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-unit {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 1px;
|
||||||
|
margin-right: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-num {
|
||||||
|
color: #409eff;
|
||||||
|
font-weight: 600;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="90px">
|
||||||
|
<el-form-item label="关联订单号" prop="relationOrderNo">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.relationOrderNo"
|
||||||
|
placeholder="支持模糊查询,充值/退款订单号"
|
||||||
|
clearable
|
||||||
|
style="width: 240px"
|
||||||
|
@keyup.enter.native="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="类型" prop="type">
|
||||||
|
<el-select v-model="queryParams.type" clearable placeholder="全部类型" style="width: 112px">
|
||||||
|
<el-option v-for="item in typeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="创建时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
style="width: 240px"
|
||||||
|
value-format="yyyy-MM-dd"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="-"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
icon="el-icon-download"
|
||||||
|
size="mini"
|
||||||
|
@click="handleExport"
|
||||||
|
v-hasPermi="['subteam:groupBalance:export']"
|
||||||
|
>导出</el-button>
|
||||||
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="recordList">
|
||||||
|
<el-table-column label="ID" align="center" prop="id" width="100" />
|
||||||
|
<el-table-column label="关联订单号" align="center" prop="relationOrderNo" width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column label="操作类型" align="center" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ typeLabel(scope.row.type) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
label="变更积分"
|
||||||
|
align="center"
|
||||||
|
prop="changeAmount"
|
||||||
|
width="120"
|
||||||
|
:formatter="formatChangeAmount"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
label="变更后积分"
|
||||||
|
align="center"
|
||||||
|
prop="resultAmount"
|
||||||
|
width="120"
|
||||||
|
:formatter="formatResultAmount"
|
||||||
|
/>
|
||||||
|
<el-table-column label="备注" align="center" prop="remark" />
|
||||||
|
<el-table-column label="创建时间" align="center" prop="createTime" width="160">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination
|
||||||
|
v-show="total > 0"
|
||||||
|
:total="total"
|
||||||
|
:page.sync="queryParams.pageNum"
|
||||||
|
:limit.sync="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listSubteamGroupBalance, subteamGroupBalanceExportUrl } from '@/api/subteam'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SubteamGroupBalance',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
showSearch: true,
|
||||||
|
dateRange: [],
|
||||||
|
total: 0,
|
||||||
|
recordList: [],
|
||||||
|
typeOptions: [
|
||||||
|
{ label: '充值', value: 0 },
|
||||||
|
{ label: '退款', value: 1 },
|
||||||
|
{ label: '下发', value: 2 },
|
||||||
|
{ label: '回收', value: 3 },
|
||||||
|
{ label: '手动修改', value: 4 }
|
||||||
|
],
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
relationOrderNo: null,
|
||||||
|
type: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatPointsWestern(value) {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const n = Number(value)
|
||||||
|
if (Number.isNaN(n)) {
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
return n.toLocaleString('en-US', { maximumFractionDigits: 0 })
|
||||||
|
},
|
||||||
|
formatChangeAmount(row) {
|
||||||
|
const value = row.changeAmount
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const n = Number(value)
|
||||||
|
if (Number.isNaN(n)) {
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
const formatted = Math.abs(n).toLocaleString('en-US', { maximumFractionDigits: 0 })
|
||||||
|
if (n > 0) {
|
||||||
|
return `+${formatted}`
|
||||||
|
}
|
||||||
|
if (n < 0) {
|
||||||
|
return `-${formatted}`
|
||||||
|
}
|
||||||
|
return '0'
|
||||||
|
},
|
||||||
|
formatResultAmount(row) {
|
||||||
|
return this.formatPointsWestern(row.resultAmount)
|
||||||
|
},
|
||||||
|
typeLabel(type) {
|
||||||
|
const n = type !== undefined && type !== null ? Number(type) : null
|
||||||
|
const hit = this.typeOptions.find(item => item.value === n)
|
||||||
|
return hit ? hit.label : '-'
|
||||||
|
},
|
||||||
|
getList() {
|
||||||
|
this.loading = true
|
||||||
|
listSubteamGroupBalance(this.addDateRange(this.queryParams, this.dateRange, 'CreateTime')).then(response => {
|
||||||
|
this.recordList = response.rows
|
||||||
|
this.total = response.total
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleQuery() {
|
||||||
|
this.queryParams.pageNum = 1
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
resetQuery() {
|
||||||
|
this.dateRange = []
|
||||||
|
this.resetForm('queryForm')
|
||||||
|
this.handleQuery()
|
||||||
|
},
|
||||||
|
handleExport() {
|
||||||
|
this.download(
|
||||||
|
subteamGroupBalanceExportUrl,
|
||||||
|
this.addDateRange({ ...this.queryParams }, this.dateRange, 'CreateTime'),
|
||||||
|
`group_balance_record_${new Date().getTime()}.xlsx`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<div class="overview-toolbar">
|
||||||
|
<el-tooltip content="刷新" placement="top">
|
||||||
|
<el-button type="default" size="mini" circle icon="el-icon-refresh" @click="load" />
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<el-row :gutter="16" v-loading="loading">
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover"><div class="metric-title">团队 ID</div><div class="metric-val">{{ info.deptId }}</div></el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover"><div class="metric-title">团队名称</div><div class="metric-val">{{ info.deptName }}</div></el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover"><div class="metric-title">团队剩余积分</div><div class="metric-val">{{ info.balance }}</div></el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="16" style="margin-top:16px">
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover"><div class="metric-title">AI用户数(实时)</div><div class="metric-val">{{ aiUserCountText }}</div></el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover"><div class="metric-title">近 7 日消耗积分 <span class="hint">(5 分钟缓存)</span></div><div class="metric-val">{{ info.last7DaysConsumeScore }}</div></el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover"><div class="metric-title">近 7 日成功订单数 <span class="hint">(5 分钟缓存)</span></div><div class="metric-val">{{ info.last7DaysOrderCount }}</div></el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getSubteamOverview } from '@/api/subteam'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SubteamOverview',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
info: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.load()
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
aiUserCountText() {
|
||||||
|
const cur = this.info.aiUserCount
|
||||||
|
const max = this.info.aiUserMaxCount
|
||||||
|
const left = cur != null && cur !== '' ? cur : 0
|
||||||
|
if (max == null || max <= 0) {
|
||||||
|
return left + '/无限制'
|
||||||
|
}
|
||||||
|
return left + '/' + max
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
load() {
|
||||||
|
this.loading = true
|
||||||
|
getSubteamOverview()
|
||||||
|
.then(res => {
|
||||||
|
this.info = res.data || {}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.overview-toolbar {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.metric-title { color: #909399; font-size: 13px; }
|
||||||
|
.metric-val { font-size: 22px; margin-top: 8px; font-weight: 600; }
|
||||||
|
.hint { font-size: 12px; color: #909399; }
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,325 @@
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-form v-show="showSearch" ref="queryForm" :model="queryParams" size="small" :inline="true">
|
||||||
|
<el-form-item label="账号" prop="username">
|
||||||
|
<el-input v-model="queryParams.username" placeholder="账号" clearable @keyup.enter.native="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="手机号" prop="phone">
|
||||||
|
<el-input v-model="queryParams.phone" placeholder="手机号" clearable @keyup.enter.native="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-select v-model="queryParams.status" placeholder="状态" clearable>
|
||||||
|
<el-option label="正常" :value="0" />
|
||||||
|
<el-option label="停用" :value="1" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="创建时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
style="width: 240px"
|
||||||
|
value-format="yyyy-MM-dd"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="-"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
|
<el-button v-hasPermi="['subteam:user:add']" type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
|
||||||
|
<right-toolbar :show-search.sync="showSearch" @queryTable="getList" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="userList">
|
||||||
|
<el-table-column label="主键ID" prop="id" width="100" />
|
||||||
|
<el-table-column label="用户ID" prop="userId" width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column label="账号" prop="username" width="150"/>
|
||||||
|
<el-table-column label="昵称" prop="nickname"/>
|
||||||
|
<el-table-column label="登录时间" prop="loginTime" width="160" align="center" header-align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ parseTime(scope.row.loginTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="剩余积分" prop="balance" width="150" align="center" header-align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ formatThousands(scope.row.balance) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="手机" prop="phone" width="160" />
|
||||||
|
<el-table-column label="状态" width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<dict-tag :options="dict.type.sys_normal_disable" :value="String(scope.row.status)" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="400">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-download"
|
||||||
|
@click="openDeptScoreDialog(scope.row, 'issue')"
|
||||||
|
v-hasPermi="['subteam:user:deptScoreIssue']"
|
||||||
|
>下放积分</el-button>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-upload2"
|
||||||
|
@click="openDeptScoreDialog(scope.row, 'reclaim')"
|
||||||
|
v-hasPermi="['subteam:user:deptScoreReclaim']"
|
||||||
|
>回收积分</el-button>
|
||||||
|
<el-button v-hasPermi="['subteam:user:edit']" size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">修改</el-button>
|
||||||
|
<el-button v-hasPermi="['subteam:user:resetPwd']" size="mini" type="text" icon="el-icon-key" @click="handleResetPwd(scope.row)">密码</el-button>
|
||||||
|
<el-button v-hasPermi="['subteam:user:remove']" size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
|
||||||
|
|
||||||
|
<el-dialog :title="title" :visible.sync="open" width="640px" append-to-body>
|
||||||
|
<el-form ref="form" :model="form" :rules="rules" label-width="88px">
|
||||||
|
<el-form-item label="昵称" prop="nickname"><el-input v-model="form.nickname" /></el-form-item>
|
||||||
|
<el-row v-if="!form.id">
|
||||||
|
<el-col :span="12"><el-form-item label="账号" prop="username"><el-input v-model="form.username" /></el-form-item></el-col>
|
||||||
|
<el-col :span="12"><el-form-item label="密码" prop="password"><el-input v-model="form.password" type="password" show-password /></el-form-item></el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-form-item label="手机" prop="phone"><el-input v-model="form.phone" /></el-form-item>
|
||||||
|
<el-form-item label="邮箱" prop="email"><el-input v-model="form.email" /></el-form-item>
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-radio-group v-model="form.status">
|
||||||
|
<el-radio :label="0">正常</el-radio>
|
||||||
|
<el-radio :label="1">停用</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注"><el-input v-model="form.remark" type="textarea" /></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||||
|
<el-button @click="cancel">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
<el-dialog
|
||||||
|
:title="deptScoreMode === 'issue' ? '下放积分' : '回收积分'"
|
||||||
|
:visible.sync="deptScoreOpen"
|
||||||
|
width="480px"
|
||||||
|
append-to-body
|
||||||
|
@close="cancelDeptScore"
|
||||||
|
>
|
||||||
|
<el-form ref="deptScoreFormRef" :model="deptScoreForm" :rules="deptScoreRules" label-width="88px">
|
||||||
|
<el-form-item label="所属团队">
|
||||||
|
<span>{{ deptScoreForm.deptName || '—' }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="用户账号">
|
||||||
|
<span>{{ deptScoreForm.username }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="积分数量" prop="amount">
|
||||||
|
<el-input-number
|
||||||
|
v-model="deptScoreForm.amount"
|
||||||
|
:min="1"
|
||||||
|
:max="100000000"
|
||||||
|
:precision="0"
|
||||||
|
:step="1"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注" prop="remark">
|
||||||
|
<el-input
|
||||||
|
v-model="deptScoreForm.remark"
|
||||||
|
type="textarea"
|
||||||
|
:rows="2"
|
||||||
|
maxlength="50"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="选填,最多50字"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitDeptScore">确 定</el-button>
|
||||||
|
<el-button @click="cancelDeptScore">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
listSubteamUser, getSubteamUser, addSubteamUser, updateSubteamUser, delSubteamUser,
|
||||||
|
resetSubteamUserPwd, issueSubteamUserDeptScore, reclaimSubteamUserDeptScore
|
||||||
|
} from '@/api/subteam'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SubteamUser',
|
||||||
|
dicts: ['sys_normal_disable'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
showSearch: true,
|
||||||
|
dateRange: [],
|
||||||
|
userList: [],
|
||||||
|
total: 0,
|
||||||
|
ids: [],
|
||||||
|
single: true,
|
||||||
|
multiple: true,
|
||||||
|
title: '',
|
||||||
|
open: false,
|
||||||
|
deptScoreOpen: false,
|
||||||
|
deptScoreMode: 'issue',
|
||||||
|
deptScoreForm: {
|
||||||
|
userId: null,
|
||||||
|
username: '',
|
||||||
|
deptName: '',
|
||||||
|
amount: undefined,
|
||||||
|
remark: ''
|
||||||
|
},
|
||||||
|
deptScoreRules: {
|
||||||
|
amount: [
|
||||||
|
{ required: true, message: '请输入积分数量', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 100000000,
|
||||||
|
message: '请输入 1~100000000 的整数',
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
remark: [{ max: 50, message: '备注最多50个字', trigger: 'blur' }]
|
||||||
|
},
|
||||||
|
queryParams: { pageNum: 1, pageSize: 10, username: undefined, phone: undefined, status: undefined },
|
||||||
|
form: {},
|
||||||
|
rules: {
|
||||||
|
nickname: [{ required: true, message: '必填', trigger: 'blur' }],
|
||||||
|
username: [{ required: true, message: '必填', trigger: 'blur' }],
|
||||||
|
password: [{ required: true, message: '必填', trigger: 'blur' }],
|
||||||
|
email: [{ type: 'email', message: '邮箱格式', trigger: ['blur', 'change'] }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatThousands(value) {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return '—'
|
||||||
|
}
|
||||||
|
const n = Number(value)
|
||||||
|
if (Number.isNaN(n)) {
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
return n.toLocaleString('en-US', { maximumFractionDigits: 0 })
|
||||||
|
},
|
||||||
|
getList() {
|
||||||
|
this.loading = true
|
||||||
|
listSubteamUser(this.addDateRange(this.queryParams, this.dateRange, 'CreateTime')).then(res => {
|
||||||
|
this.userList = res.rows
|
||||||
|
this.total = res.total
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleQuery() {
|
||||||
|
this.queryParams.pageNum = 1
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
resetQuery() {
|
||||||
|
this.dateRange = []
|
||||||
|
this.resetForm('queryForm')
|
||||||
|
this.handleQuery()
|
||||||
|
},
|
||||||
|
handleSelectionChange(rows) {
|
||||||
|
this.ids = rows.map(r => r.id)
|
||||||
|
this.single = rows.length !== 1
|
||||||
|
this.multiple = !rows.length
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
this.form = { id: undefined, username: undefined, nickname: undefined, password: undefined,
|
||||||
|
phone: undefined, email: undefined, status: 0, remark: undefined }
|
||||||
|
this.resetForm('form')
|
||||||
|
},
|
||||||
|
handleAdd() {
|
||||||
|
this.reset()
|
||||||
|
this.open = true
|
||||||
|
this.title = '新增AI用户'
|
||||||
|
},
|
||||||
|
handleUpdate(row) {
|
||||||
|
this.reset()
|
||||||
|
const id = row.id || this.ids[0]
|
||||||
|
getSubteamUser(id).then(r => {
|
||||||
|
this.form = r.data || {}
|
||||||
|
this.form.status = Number(this.form.status || 0)
|
||||||
|
this.open = true
|
||||||
|
this.title = '修改AI用户'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
submitForm() {
|
||||||
|
this.$refs.form.validate(valid => {
|
||||||
|
if (!valid) return
|
||||||
|
if (this.form.id) {
|
||||||
|
updateSubteamUser(this.form).then(() => { this.$modal.msgSuccess('成功'); this.open = false; this.getList() })
|
||||||
|
} else {
|
||||||
|
addSubteamUser(this.form).then(() => { this.$modal.msgSuccess('成功'); this.open = false; this.getList() })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
cancel() { this.open = false },
|
||||||
|
handleDelete(row) {
|
||||||
|
const userIds = row.id || this.ids
|
||||||
|
this.$modal.confirm('确认删除?').then(() => delSubteamUser(userIds)).then(() => { this.getList(); this.$modal.msgSuccess('已删除') }).catch(() => {})
|
||||||
|
},
|
||||||
|
handleResetPwd(row) {
|
||||||
|
this.$prompt('新密码', '重置', { inputType: 'password' }).then(({ value }) => {
|
||||||
|
resetSubteamUserPwd({ id: row.id, password: value }).then(() => this.$modal.msgSuccess('已重置'))
|
||||||
|
}).catch(() => {})
|
||||||
|
},
|
||||||
|
openDeptScoreDialog(row, mode) {
|
||||||
|
if (!row.deptId) {
|
||||||
|
this.$modal.msgWarning('请先分配归属部门后再操作积分')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.deptScoreMode = mode
|
||||||
|
this.deptScoreForm = {
|
||||||
|
userId: row.id,
|
||||||
|
username: row.username || row.userId || '',
|
||||||
|
deptName: row.deptName || '',
|
||||||
|
amount: undefined,
|
||||||
|
remark: ''
|
||||||
|
}
|
||||||
|
this.deptScoreOpen = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.deptScoreFormRef) {
|
||||||
|
this.$refs.deptScoreFormRef.clearValidate()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
submitDeptScore() {
|
||||||
|
this.$refs.deptScoreFormRef.validate(valid => {
|
||||||
|
if (!valid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const raw = this.deptScoreForm.amount
|
||||||
|
const amount = typeof raw === 'number' ? Math.trunc(raw) : parseInt(String(raw), 10)
|
||||||
|
if (!Number.isFinite(amount) || amount < 1 || amount > 100000000) {
|
||||||
|
this.$modal.msgWarning('请输入 1~100000000 的整数积分')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const payload = {
|
||||||
|
userId: this.deptScoreForm.userId,
|
||||||
|
amount,
|
||||||
|
remark: this.deptScoreForm.remark || undefined
|
||||||
|
}
|
||||||
|
const req = this.deptScoreMode === 'issue' ? issueSubteamUserDeptScore : reclaimSubteamUserDeptScore
|
||||||
|
req(payload).then(() => {
|
||||||
|
this.$modal.msgSuccess('操作成功')
|
||||||
|
this.deptScoreOpen = false
|
||||||
|
this.getList()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
cancelDeptScore() {
|
||||||
|
this.deptScoreOpen = false
|
||||||
|
this.deptScoreForm = { userId: null, username: '', deptName: '', amount: undefined, remark: '' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,189 @@
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-form
|
||||||
|
:model="queryParams"
|
||||||
|
ref="queryForm"
|
||||||
|
size="small"
|
||||||
|
:inline="true"
|
||||||
|
v-show="showSearch"
|
||||||
|
label-width="68px"
|
||||||
|
>
|
||||||
|
<el-form-item label="用户ID" prop="uuid">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.uuid"
|
||||||
|
placeholder="请输入用户ID"
|
||||||
|
clearable
|
||||||
|
@keyup.enter.native="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="操作类型" prop="type">
|
||||||
|
<el-select
|
||||||
|
v-model="queryParams.type"
|
||||||
|
placeholder="请选择操作类型"
|
||||||
|
filterable
|
||||||
|
clearable
|
||||||
|
style="width: 200px; margin-bottom: 16px;"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="(label, value) in typeMap"
|
||||||
|
:key="value"
|
||||||
|
:label="label"
|
||||||
|
:value="Number(value)"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="创建时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
style="width: 240px"
|
||||||
|
value-format="yyyy-MM-dd"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="-"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
icon="el-icon-download"
|
||||||
|
size="mini"
|
||||||
|
@click="handleExport"
|
||||||
|
v-hasPermi="['subteam:userBalance:export']"
|
||||||
|
>导出</el-button>
|
||||||
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="recordList">
|
||||||
|
<el-table-column label="主键ID" align="center" prop="id" width="80" />
|
||||||
|
<el-table-column label="用户ID" align="center" prop="uuid" width="100" show-overflow-tooltip />
|
||||||
|
<el-table-column label="用户昵称" align="center" prop="nickname" width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column label="关联订单号" align="center" prop="orderNo" width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column label="操作类型" align="center" width="100" prop="type" :formatter="formatType" />
|
||||||
|
<el-table-column
|
||||||
|
label="变更积分"
|
||||||
|
align="center"
|
||||||
|
prop="changeAmount"
|
||||||
|
:formatter="formatChangeAmount"
|
||||||
|
width="120"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
label="变更后积分"
|
||||||
|
align="center"
|
||||||
|
prop="resultAmount"
|
||||||
|
:formatter="formatResultAmount"
|
||||||
|
width="120"
|
||||||
|
/>
|
||||||
|
<el-table-column label="操作时间" align="center" prop="createTime" width="150" />
|
||||||
|
<el-table-column label="备注" align="center" prop="remark" />
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination
|
||||||
|
v-show="total > 0"
|
||||||
|
:total="total"
|
||||||
|
:page.sync="queryParams.pageNum"
|
||||||
|
:limit.sync="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listSubteamUserBalance, subteamUserBalanceExportUrl } from "@/api/subteam";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "SubteamUserBalance",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dateRange: [],
|
||||||
|
typeMap: {
|
||||||
|
9: "快捷生视频",
|
||||||
|
11: "系统操作",
|
||||||
|
12: "团队下发",
|
||||||
|
13: "团队收回"
|
||||||
|
},
|
||||||
|
loading: true,
|
||||||
|
showSearch: true,
|
||||||
|
total: 0,
|
||||||
|
recordList: [],
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
type: null,
|
||||||
|
uuid: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatPointsWestern(value) {
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const n = Number(value);
|
||||||
|
if (Number.isNaN(n)) {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
return n.toLocaleString("en-US", { maximumFractionDigits: 0 });
|
||||||
|
},
|
||||||
|
formatChangeAmount(row) {
|
||||||
|
const value = row.changeAmount;
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const n = Number(value);
|
||||||
|
if (Number.isNaN(n)) {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
const formatted = Math.abs(n).toLocaleString("en-US", {
|
||||||
|
maximumFractionDigits: 0
|
||||||
|
});
|
||||||
|
if (n > 0) {
|
||||||
|
return `+${formatted}`;
|
||||||
|
}
|
||||||
|
if (n < 0) {
|
||||||
|
return `-${formatted}`;
|
||||||
|
}
|
||||||
|
return "0";
|
||||||
|
},
|
||||||
|
formatResultAmount(row) {
|
||||||
|
return this.formatPointsWestern(row.resultAmount);
|
||||||
|
},
|
||||||
|
formatType(row) {
|
||||||
|
return this.typeMap[row.type] || "未知";
|
||||||
|
},
|
||||||
|
getList() {
|
||||||
|
this.loading = true;
|
||||||
|
listSubteamUserBalance(
|
||||||
|
this.addDateRange(this.queryParams, this.dateRange)
|
||||||
|
).then(response => {
|
||||||
|
this.recordList = response.rows;
|
||||||
|
this.total = response.total;
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleQuery() {
|
||||||
|
this.queryParams.pageNum = 1;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
resetQuery() {
|
||||||
|
this.dateRange = [];
|
||||||
|
this.resetForm("queryForm");
|
||||||
|
this.handleQuery();
|
||||||
|
},
|
||||||
|
handleExport() {
|
||||||
|
this.download(
|
||||||
|
subteamUserBalanceExportUrl,
|
||||||
|
this.addDateRange({ ...this.queryParams }, this.dateRange),
|
||||||
|
`user_balance_${new Date().getTime()}.xlsx`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,270 @@
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="88px">
|
||||||
|
<el-form-item label="订单号" prop="orderNum">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.orderNum"
|
||||||
|
placeholder="请输入订单号"
|
||||||
|
style="width: 140px"
|
||||||
|
clearable
|
||||||
|
@keyup.enter.native="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="三方订单号" prop="thirdPartyOrderNum">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.thirdPartyOrderNum"
|
||||||
|
placeholder="支持模糊搜索"
|
||||||
|
style="width: 210px"
|
||||||
|
clearable
|
||||||
|
@keyup.enter.native="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="用户名称" prop="userName">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.userName"
|
||||||
|
placeholder="支持模糊搜索"
|
||||||
|
style="width: 120px"
|
||||||
|
clearable
|
||||||
|
@keyup.enter.native="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width: 100px">
|
||||||
|
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="创建时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
style="width: 240px"
|
||||||
|
value-format="yyyy-MM-dd"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="-"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
icon="el-icon-download"
|
||||||
|
size="mini"
|
||||||
|
@click="handleExport"
|
||||||
|
v-hasPermi="['subteam:videoOrder:export']"
|
||||||
|
>导出</el-button>
|
||||||
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="orderList">
|
||||||
|
<el-table-column label="订单号" align="center" prop="orderNum" width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column label="三方订单号" align="center" prop="thirdPartyOrderNum" width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column label="用户名称" align="center" prop="userName" width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column label="消耗积分" align="center" prop="amount" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ formatThousands(scope.row.amount) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="使用tokens" align="center" prop="totalUsage" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ formatThousands(scope.row.totalUsage) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" align="center" width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ statusLabel(scope.row.status) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="结果预览" min-width="185">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<div v-if="extractVideoUrl(scope.row.result)">
|
||||||
|
<video
|
||||||
|
:src="extractVideoUrl(scope.row.result)"
|
||||||
|
controls
|
||||||
|
preload="metadata"
|
||||||
|
style="width: 180px; max-height: 100px; border-radius: 4px;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建时间" align="center" prop="createTime" width="150">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="90" fixed="right">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button type="text" size="mini" @click="handleDetail(scope.row)">详情</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination
|
||||||
|
v-show="total>0"
|
||||||
|
:total="total"
|
||||||
|
:page.sync="queryParams.pageNum"
|
||||||
|
:limit.sync="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-dialog title="订单详情" :visible.sync="detailVisible" width="800px" append-to-body>
|
||||||
|
<el-descriptions :column="1" border size="small" :labelStyle="{ width: '150px', minWidth: '150px' }">
|
||||||
|
<el-descriptions-item v-for="(value, key) in detailData" :key="key" :label="key">
|
||||||
|
<pre v-if="isObjectValue(value)" style="margin: 0; white-space: pre-wrap;">{{ formatJson(value) }}</pre>
|
||||||
|
<span v-else>{{ value === null || value === undefined || value === '' ? '-' : value }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
<span slot="footer">
|
||||||
|
<el-button size="mini" @click="detailVisible = false">关闭</el-button>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listSubteamVideoOrder, subteamVideoOrderExportUrl } from '@/api/subteam'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SubteamVideoOrder',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
showSearch: true,
|
||||||
|
total: 0,
|
||||||
|
orderList: [],
|
||||||
|
detailVisible: false,
|
||||||
|
detailData: {},
|
||||||
|
statusOptions: [
|
||||||
|
{ label: '进行中', value: 0 },
|
||||||
|
{ label: '完成', value: 1 },
|
||||||
|
{ label: '失败', value: 2 }
|
||||||
|
],
|
||||||
|
dateRange: [],
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
orderNum: null,
|
||||||
|
thirdPartyOrderNum: null,
|
||||||
|
userName: null,
|
||||||
|
status: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.initDefaultDateRange()
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatThousands(value) {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return '—'
|
||||||
|
}
|
||||||
|
const n = Number(value)
|
||||||
|
if (Number.isNaN(n)) {
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
return n.toLocaleString('en-US', { maximumFractionDigits: 0 })
|
||||||
|
},
|
||||||
|
initDefaultDateRange() {
|
||||||
|
this.dateRange = this.buildLastMonthDateRange()
|
||||||
|
},
|
||||||
|
buildLastMonthDateRange() {
|
||||||
|
const end = new Date()
|
||||||
|
const start = new Date(end.getTime())
|
||||||
|
start.setMonth(start.getMonth() - 1)
|
||||||
|
return [this.formatYmd(start), this.formatYmd(end)]
|
||||||
|
},
|
||||||
|
formatYmd(d) {
|
||||||
|
const y = d.getFullYear()
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(d.getDate()).padStart(2, '0')
|
||||||
|
return `${y}-${m}-${day}`
|
||||||
|
},
|
||||||
|
statusLabel(status) {
|
||||||
|
const n = status !== undefined && status !== null ? Number(status) : null
|
||||||
|
const hit = this.statusOptions.find(o => o.value === n)
|
||||||
|
return hit ? hit.label : '—'
|
||||||
|
},
|
||||||
|
extractVideoUrl(result) {
|
||||||
|
if (!result) return ''
|
||||||
|
if (typeof result === 'string') {
|
||||||
|
const value = result.trim()
|
||||||
|
if (!value) return ''
|
||||||
|
if (/^https?:\/\/|^\/\//.test(value)) return value
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value)
|
||||||
|
return this.extractVideoUrl(parsed)
|
||||||
|
} catch (e) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
for (let i = 0; i < result.length; i += 1) {
|
||||||
|
const url = this.extractVideoUrl(result[i])
|
||||||
|
if (url) return url
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if (typeof result === 'object') {
|
||||||
|
const directKeys = ['videoUrl', 'url', 'resultUrl', 'playUrl', 'output', 'fileUrl']
|
||||||
|
for (let i = 0; i < directKeys.length; i += 1) {
|
||||||
|
const key = directKeys[i]
|
||||||
|
if (result[key]) {
|
||||||
|
const url = this.extractVideoUrl(result[key])
|
||||||
|
if (url) return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
},
|
||||||
|
handleDetail(row) {
|
||||||
|
const copy = row && typeof row === 'object' ? { ...row } : {}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(copy, 'deptName')) {
|
||||||
|
delete copy.deptName
|
||||||
|
}
|
||||||
|
this.detailData = copy
|
||||||
|
this.detailVisible = true
|
||||||
|
},
|
||||||
|
isObjectValue(value) {
|
||||||
|
return value !== null && typeof value === 'object'
|
||||||
|
},
|
||||||
|
formatJson(value) {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value, null, 2)
|
||||||
|
} catch (e) {
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getList() {
|
||||||
|
this.loading = true
|
||||||
|
listSubteamVideoOrder(this.addDateRange(this.queryParams, this.dateRange, 'CreateTime')).then(response => {
|
||||||
|
this.orderList = response.rows
|
||||||
|
this.total = response.total
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleQuery() {
|
||||||
|
this.queryParams.pageNum = 1
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
resetQuery() {
|
||||||
|
this.initDefaultDateRange()
|
||||||
|
this.resetForm('queryForm')
|
||||||
|
this.handleQuery()
|
||||||
|
},
|
||||||
|
handleExport() {
|
||||||
|
const q = { ...this.queryParams }
|
||||||
|
this.download(
|
||||||
|
subteamVideoOrderExportUrl,
|
||||||
|
this.addDateRange(q, this.dateRange, 'CreateTime'),
|
||||||
|
`subteam_video_orders_${new Date().getTime()}.xlsx`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -1,16 +1,24 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch">
|
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch">
|
||||||
<el-form-item label="部门名称" prop="deptName">
|
<el-form-item label="团队" prop="deptId">
|
||||||
<el-input
|
<el-select
|
||||||
v-model="queryParams.deptName"
|
v-model="queryParams.deptId"
|
||||||
placeholder="请输入部门名称"
|
placeholder="全部团队(可选)"
|
||||||
clearable
|
clearable
|
||||||
@keyup.enter.native="handleQuery"
|
filterable
|
||||||
|
style="width: 220px"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in secondLevelDeptOptions"
|
||||||
|
:key="item.deptId"
|
||||||
|
:label="item.deptName"
|
||||||
|
:value="item.deptId"
|
||||||
/>
|
/>
|
||||||
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="状态" prop="status">
|
<el-form-item label="状态" prop="status">
|
||||||
<el-select v-model="queryParams.status" placeholder="部门状态" clearable>
|
<el-select v-model="queryParams.status" placeholder="团队状态" clearable>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="dict in dict.type.sys_normal_disable"
|
v-for="dict in dict.type.sys_normal_disable"
|
||||||
:key="dict.value"
|
:key="dict.value"
|
||||||
|
|
@ -22,21 +30,6 @@
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<el-row :gutter="10" class="mb8">
|
|
||||||
<el-col :span="1.5">
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
plain
|
|
||||||
icon="el-icon-plus"
|
|
||||||
size="mini"
|
|
||||||
@click="handleAdd"
|
|
||||||
v-hasPermi="['system:dept:add']"
|
|
||||||
>新增</el-button>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="1.5">
|
|
||||||
<el-button
|
<el-button
|
||||||
type="info"
|
type="info"
|
||||||
plain
|
plain
|
||||||
|
|
@ -44,9 +37,9 @@
|
||||||
size="mini"
|
size="mini"
|
||||||
@click="toggleExpandAll"
|
@click="toggleExpandAll"
|
||||||
>展开/折叠</el-button>
|
>展开/折叠</el-button>
|
||||||
</el-col>
|
|
||||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||||
</el-row>
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
v-if="refreshTable"
|
v-if="refreshTable"
|
||||||
|
|
@ -56,8 +49,18 @@
|
||||||
:default-expand-all="isExpandAll"
|
:default-expand-all="isExpandAll"
|
||||||
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
|
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
|
||||||
>
|
>
|
||||||
<el-table-column prop="deptName" label="部门名称" width="260"></el-table-column>
|
<el-table-column prop="deptName" label="团队名称" width="260"></el-table-column>
|
||||||
<el-table-column prop="orderNum" label="排序" width="200"></el-table-column>
|
<el-table-column prop="orderNum" label="排序" width="200"></el-table-column>
|
||||||
|
<el-table-column prop="maxUserCount" label="账号上限" width="100" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.maxUserCount != null && scope.row.maxUserCount > 0 ? scope.row.maxUserCount : '不限制' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="balance" label="剩余积分" width="120" align="right">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ formatDeptBalance(scope.row.balance) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="status" label="状态" width="100">
|
<el-table-column prop="status" label="状态" width="100">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<dict-tag :options="dict.type.sys_normal_disable" :value="scope.row.status"/>
|
<dict-tag :options="dict.type.sys_normal_disable" :value="scope.row.status"/>
|
||||||
|
|
@ -68,8 +71,32 @@
|
||||||
<span>{{ parseTime(scope.row.createTime) }}</span>
|
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
<el-table-column label="操作" align="center" width="480" class-name="small-padding fixed-width">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
v-if="isFirstLevelRow(scope.row) || isSecondLevelRow(scope.row)"
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-setting"
|
||||||
|
@click="handleArkConfig(scope.row)"
|
||||||
|
v-hasPermi="['system:dept:query', 'system:dept:edit']"
|
||||||
|
>火山引擎</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="isFirstLevelRow(scope.row) || isSecondLevelRow(scope.row)"
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-wallet"
|
||||||
|
@click="handleChargeRefund(scope.row)"
|
||||||
|
v-hasPermi="['system:dept:chargeRefund']"
|
||||||
|
>充值/退款</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="isFirstLevelRow(scope.row) || isSecondLevelRow(scope.row)"
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-s-operation"
|
||||||
|
@click="handleEditScore(scope.row)"
|
||||||
|
v-hasPermi="['system:dept:editScore']"
|
||||||
|
>积分更正</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
size="mini"
|
size="mini"
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -82,6 +109,7 @@
|
||||||
type="text"
|
type="text"
|
||||||
icon="el-icon-plus"
|
icon="el-icon-plus"
|
||||||
@click="handleAdd(scope.row)"
|
@click="handleAdd(scope.row)"
|
||||||
|
v-if="isFirstLevelRow(scope.row)"
|
||||||
v-hasPermi="['system:dept:add']"
|
v-hasPermi="['system:dept:add']"
|
||||||
>新增</el-button>
|
>新增</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
|
|
@ -96,49 +124,49 @@
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<!-- 添加或修改部门对话框 -->
|
<!-- 添加或修改团队对话框 -->
|
||||||
<el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
|
<el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
|
||||||
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="24" v-if="form.parentId !== 0">
|
<el-col :span="24" v-if="form.parentId !== 0">
|
||||||
<el-form-item label="上级部门" prop="parentId">
|
<el-form-item label="上级团队" prop="parentId">
|
||||||
<treeselect v-model="form.parentId" :options="deptOptions" :normalizer="normalizer" placeholder="选择上级部门" />
|
<treeselect v-model="form.parentId" :options="deptOptions" :normalizer="normalizer" placeholder="选择上级团队" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="部门名称" prop="deptName">
|
<el-form-item label="团队名称" prop="deptName">
|
||||||
<el-input v-model="form.deptName" placeholder="请输入部门名称" />
|
<el-input v-model="form.deptName" placeholder="请输入团队名称" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="显示排序" prop="orderNum">
|
<el-form-item label="显示排序" prop="orderNum">
|
||||||
<el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
|
<el-input-number v-model="form.orderNum" controls-position="right" :min="0" :disabled="isFirstLevelEditForm" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="负责人" prop="leader">
|
<el-form-item label="负责人" prop="leader">
|
||||||
<el-input v-model="form.leader" placeholder="请输入负责人" maxlength="20" />
|
<el-input v-model="form.leader" placeholder="请输入负责人" maxlength="20" :disabled="isFirstLevelEditForm" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="联系电话" prop="phone">
|
<el-form-item label="联系电话" prop="phone">
|
||||||
<el-input v-model="form.phone" placeholder="请输入联系电话" maxlength="11" />
|
<el-input v-model="form.phone" placeholder="请输入联系电话" maxlength="11" :disabled="isFirstLevelEditForm" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="邮箱" prop="email">
|
<el-form-item label="邮箱" prop="email">
|
||||||
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
|
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" :disabled="isFirstLevelEditForm" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="部门状态">
|
<el-form-item label="团队状态">
|
||||||
<el-radio-group v-model="form.status">
|
<el-radio-group v-model="form.status" :disabled="isFirstLevelEditForm">
|
||||||
<el-radio
|
<el-radio
|
||||||
v-for="dict in dict.type.sys_normal_disable"
|
v-for="dict in dict.type.sys_normal_disable"
|
||||||
:key="dict.value"
|
:key="dict.value"
|
||||||
|
|
@ -148,24 +176,244 @@
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="账号上限" prop="maxUserCount">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.maxUserCount"
|
||||||
|
:min="0"
|
||||||
|
:max="999999"
|
||||||
|
:precision="0"
|
||||||
|
controls-position="right"
|
||||||
|
placeholder="0 表示不限制"
|
||||||
|
style="width: 100%"
|
||||||
|
:disabled="isFirstLevelEditForm"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<p class="model-parm-hint" style="margin: 0; padding-top: 8px"> 限制本团队下「启用」状态账号数量;0 表示不限制。</p>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div slot="footer" class="dialog-footer">
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<span v-if="isFirstLevelEditForm" class="form-tip">一级团队仅允许修改名称</span>
|
||||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||||
<el-button @click="cancel">取 消</el-button>
|
<el-button @click="cancel">取 消</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog title="火山引擎" :visible.sync="arkOpen" width="800px" append-to-body @close="resetArkForm">
|
||||||
|
<el-form ref="arkFormRef" :model="arkForm" label-width="120px">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="ApiKey">
|
||||||
|
<el-input
|
||||||
|
v-model="arkForm.byteApiKey"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
placeholder="选填"
|
||||||
|
maxlength="255"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="火山项目">
|
||||||
|
<el-input
|
||||||
|
v-model="arkForm.project"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
placeholder="选填"
|
||||||
|
maxlength="255"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="视频模型">
|
||||||
|
<div class="model-parm-block">
|
||||||
|
<div
|
||||||
|
v-for="(row, idx) in arkModelParamRows"
|
||||||
|
:key="'ark-mp-' + idx"
|
||||||
|
class="model-parm-row"
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
v-model="row.label"
|
||||||
|
placeholder="显示名称(如 Seedance 2.0)"
|
||||||
|
class="model-parm-input-label"
|
||||||
|
/>
|
||||||
|
<el-input
|
||||||
|
v-model="row.value"
|
||||||
|
placeholder="Endpoint / 模型 ID(ep-…)"
|
||||||
|
class="model-parm-input-value"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
:disabled="arkModelParamRows.length <= 1"
|
||||||
|
@click="removeArkModelParamRow(idx)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<el-button type="text" icon="el-icon-plus" @click="addArkModelParamRow">添加模型</el-button>
|
||||||
|
<p class="model-parm-hint">
|
||||||
|
保存为 JSON 写入 ai_dept_ark_config.model_parm;门户「视频生成」按 ai_user.dept_id 与本团队配置读取。
|
||||||
|
留空则使用 portal.video.models。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitArkForm" v-hasPermi="['system:dept:edit']">确 定</el-button>
|
||||||
|
<el-button @click="arkOpen = false">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
:title="'充值/退款 — ' + (chargeRefundForm.deptName || '')"
|
||||||
|
:visible.sync="chargeRefundOpen"
|
||||||
|
width="520px"
|
||||||
|
append-to-body
|
||||||
|
@close="resetChargeRefund"
|
||||||
|
>
|
||||||
|
<el-form ref="chargeRefundFormRef" :model="chargeRefundForm" :rules="chargeRefundRules" label-width="88px">
|
||||||
|
<el-form-item label="类型" prop="orderType">
|
||||||
|
<el-radio-group v-model="chargeRefundForm.orderType">
|
||||||
|
<el-radio :label="0">充值</el-radio>
|
||||||
|
<el-radio :label="1">退款</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="金额" prop="money">
|
||||||
|
<el-input
|
||||||
|
:value="chargeRefundMoneyDisplay"
|
||||||
|
placeholder="财务记录(元),如 9,999,999.00"
|
||||||
|
clearable
|
||||||
|
@input="onChargeRefundMoneyInput"
|
||||||
|
@blur="onChargeRefundMoneyBlur"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="积分" prop="amount">
|
||||||
|
<el-input
|
||||||
|
:value="chargeRefundAmountDisplay"
|
||||||
|
placeholder="变动积分,如 99,999,999"
|
||||||
|
clearable
|
||||||
|
@input="onChargeRefundAmountInput"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注" prop="remark">
|
||||||
|
<el-input
|
||||||
|
v-model="chargeRefundForm.remark"
|
||||||
|
type="textarea"
|
||||||
|
:rows="2"
|
||||||
|
maxlength="50"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="选填,最多50字"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitChargeRefund">确 定</el-button>
|
||||||
|
<el-button @click="chargeRefundOpen = false">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
:title="'积分更正 — ' + (editScoreForm.deptName || '')"
|
||||||
|
:visible.sync="editScoreOpen"
|
||||||
|
width="520px"
|
||||||
|
append-to-body
|
||||||
|
@close="resetEditScore"
|
||||||
|
>
|
||||||
|
<el-form ref="editScoreFormRef" :model="editScoreForm" :rules="editScoreRules" label-width="88px">
|
||||||
|
<el-form-item label="积分" prop="score">
|
||||||
|
<el-input
|
||||||
|
:value="editScoreScoreDisplay"
|
||||||
|
class="edit-score-input"
|
||||||
|
clearable
|
||||||
|
placeholder="正数增加,负数扣减,不能为0,如 9,999,999 或 -1,000,000"
|
||||||
|
@input="onEditScoreInput"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注" prop="remark">
|
||||||
|
<el-input
|
||||||
|
v-model="editScoreForm.remark"
|
||||||
|
type="textarea"
|
||||||
|
:rows="2"
|
||||||
|
maxlength="50"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="必填,最多50字"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitEditScore">确 定</el-button>
|
||||||
|
<el-button @click="editScoreOpen = false">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.model-parm-block {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.model-parm-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.model-parm-input-label {
|
||||||
|
width: 38%;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.model-parm-input-value {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.model-parm-hint {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.form-tip {
|
||||||
|
margin-right: 16px;
|
||||||
|
color: #e6a23c;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.edit-score-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/system/dept"
|
import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild, getDeptArk, updateDeptArk, chargeRefundDept, editScore } from "@/api/system/dept"
|
||||||
|
import {
|
||||||
|
WESTERN_MONEY_MAX,
|
||||||
|
sanitizeMoneyDigits,
|
||||||
|
formatMoneyWesternDisplay,
|
||||||
|
moneyStringToNumber,
|
||||||
|
formatMoneyWesternFinal,
|
||||||
|
sanitizeIntDigits,
|
||||||
|
formatIntWesternDisplay,
|
||||||
|
intStringToNumber,
|
||||||
|
sanitizeSignedIntDigits,
|
||||||
|
formatSignedIntDisplay,
|
||||||
|
signedIntStringToNumber
|
||||||
|
} from "@/utils/westernNumberFormat"
|
||||||
import Treeselect from "@riophae/vue-treeselect"
|
import Treeselect from "@riophae/vue-treeselect"
|
||||||
import "@riophae/vue-treeselect/dist/vue-treeselect.css"
|
import "@riophae/vue-treeselect/dist/vue-treeselect.css"
|
||||||
|
import secondLevelDeptFilter from "@/mixins/secondLevelDeptFilter"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Dept",
|
name: "Dept",
|
||||||
dicts: ['sys_normal_disable'],
|
dicts: ['sys_normal_disable'],
|
||||||
components: { Treeselect },
|
components: { Treeselect },
|
||||||
|
mixins: [secondLevelDeptFilter],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// 遮罩层
|
// 遮罩层
|
||||||
|
|
@ -180,24 +428,103 @@ export default {
|
||||||
title: "",
|
title: "",
|
||||||
// 是否显示弹出层
|
// 是否显示弹出层
|
||||||
open: false,
|
open: false,
|
||||||
|
arkOpen: false,
|
||||||
|
arkDeptId: null,
|
||||||
|
arkForm: {
|
||||||
|
byteApiKey: undefined,
|
||||||
|
project: undefined,
|
||||||
|
modelParm: undefined
|
||||||
|
},
|
||||||
|
arkModelParamRows: [{ label: '', value: '' }],
|
||||||
|
chargeRefundOpen: false,
|
||||||
|
chargeRefundMoneyDisplay: "",
|
||||||
|
chargeRefundAmountDisplay: "",
|
||||||
|
chargeRefundForm: {
|
||||||
|
deptId: undefined,
|
||||||
|
deptName: "",
|
||||||
|
orderType: 0,
|
||||||
|
money: undefined,
|
||||||
|
amount: undefined,
|
||||||
|
remark: ""
|
||||||
|
},
|
||||||
|
chargeRefundRules: {
|
||||||
|
orderType: [
|
||||||
|
{ required: true, message: "类型不能为空", trigger: "change" }
|
||||||
|
],
|
||||||
|
money: [
|
||||||
|
{ required: true, message: "金额不能为空", trigger: "blur" },
|
||||||
|
{ type: "number", min: 0, max: 10000000, message: "金额须在 0~10000000 之间", trigger: "blur" }
|
||||||
|
],
|
||||||
|
amount: [
|
||||||
|
{ required: true, message: "积分不能为空", trigger: "blur" },
|
||||||
|
{ type: "number", min: 1, max: 100000000, message: "积分须在 1~100000000 之间", trigger: "blur" }
|
||||||
|
],
|
||||||
|
remark: [
|
||||||
|
{ max: 50, message: "备注不能超过50个字符", trigger: "blur" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
editScoreOpen: false,
|
||||||
|
editScoreScoreDisplay: "",
|
||||||
|
editScoreForm: {
|
||||||
|
deptId: undefined,
|
||||||
|
deptName: "",
|
||||||
|
score: undefined,
|
||||||
|
remark: ""
|
||||||
|
},
|
||||||
|
editScoreRules: {
|
||||||
|
score: [
|
||||||
|
{ required: true, message: "积分不能为空", trigger: "blur" },
|
||||||
|
{
|
||||||
|
validator(rule, value, callback) {
|
||||||
|
if (value === undefined || value === null || value === "") {
|
||||||
|
callback(new Error("积分不能为空"))
|
||||||
|
} else if (!Number.isInteger(Number(value))) {
|
||||||
|
callback(new Error("积分须为整数"))
|
||||||
|
} else if (Number(value) === 0) {
|
||||||
|
callback(new Error("积分不能为0"))
|
||||||
|
} else if (Number(value) < -100000000 || Number(value) > 100000000) {
|
||||||
|
callback(new Error("积分须在 -100000000~100000000 之间(不含0)"))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
remark: [
|
||||||
|
{ required: true, message: "备注不能为空", trigger: "blur" },
|
||||||
|
{
|
||||||
|
validator(rule, value, callback) {
|
||||||
|
if (!value || String(value).trim() === "") {
|
||||||
|
callback(new Error("备注不能为空"))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: "blur"
|
||||||
|
},
|
||||||
|
{ max: 50, message: "备注不能超过50个字符", trigger: "blur" }
|
||||||
|
]
|
||||||
|
},
|
||||||
// 是否展开,默认全部展开
|
// 是否展开,默认全部展开
|
||||||
isExpandAll: true,
|
isExpandAll: true,
|
||||||
// 重新渲染表格状态
|
// 重新渲染表格状态
|
||||||
refreshTable: true,
|
refreshTable: true,
|
||||||
// 查询参数
|
// 查询参数
|
||||||
queryParams: {
|
queryParams: {
|
||||||
deptName: undefined,
|
deptId: null,
|
||||||
status: undefined
|
status: undefined
|
||||||
},
|
},
|
||||||
// 表单参数
|
// 表单参数
|
||||||
form: {},
|
form: {},
|
||||||
|
originalForm: {},
|
||||||
// 表单校验
|
// 表单校验
|
||||||
rules: {
|
rules: {
|
||||||
parentId: [
|
parentId: [
|
||||||
{ required: true, message: "上级部门不能为空", trigger: "blur" }
|
{ required: true, message: "上级团队不能为空", trigger: "blur" }
|
||||||
],
|
],
|
||||||
deptName: [
|
deptName: [
|
||||||
{ required: true, message: "部门名称不能为空", trigger: "blur" }
|
{ required: true, message: "团队名称不能为空", trigger: "blur" }
|
||||||
],
|
],
|
||||||
orderNum: [
|
orderNum: [
|
||||||
{ required: true, message: "显示排序不能为空", trigger: "blur" }
|
{ required: true, message: "显示排序不能为空", trigger: "blur" }
|
||||||
|
|
@ -215,19 +542,304 @@ export default {
|
||||||
message: "请输入正确的手机号码",
|
message: "请输入正确的手机号码",
|
||||||
trigger: "blur"
|
trigger: "blur"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
maxUserCount: [
|
||||||
|
{ required: true, message: "账号上限不能为空", trigger: "change" },
|
||||||
|
{
|
||||||
|
type: "number",
|
||||||
|
min: 0,
|
||||||
|
max: 999999,
|
||||||
|
message: "账号上限须为 0–999999 的整数,0 表示不限制",
|
||||||
|
trigger: "change"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
this.loadSecondLevelDeptOptions()
|
||||||
this.getList()
|
this.getList()
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
isFirstLevelEditForm() {
|
||||||
|
return this.form.deptId !== undefined && this.isFirstLevelRow(this.form)
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** 列表「剩余积分」:西式千分位,与业务积分展示一致;空为 — */
|
||||||
|
formatDeptBalance(value) {
|
||||||
|
if (value == null || value === "") {
|
||||||
|
return "—"
|
||||||
|
}
|
||||||
|
const n = Number(value)
|
||||||
|
if (Number.isNaN(n)) {
|
||||||
|
return "—"
|
||||||
|
}
|
||||||
|
return n.toLocaleString("en-US", { maximumFractionDigits: 2, minimumFractionDigits: 0 })
|
||||||
|
},
|
||||||
|
onEditScoreInput(val) {
|
||||||
|
const s = sanitizeSignedIntDigits(val)
|
||||||
|
this.editScoreScoreDisplay = s === "" ? "" : s === "-" ? "-" : formatSignedIntDisplay(s)
|
||||||
|
this.editScoreForm.score = signedIntStringToNumber(s)
|
||||||
|
},
|
||||||
|
isFirstLevelRow(row) {
|
||||||
|
if (!row) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return Number(row.parentId) === 0 && String(row.ancestors || "") === "0"
|
||||||
|
},
|
||||||
|
isSecondLevelRow(row) {
|
||||||
|
if (!row) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const ancestors = String(row.ancestors || "").split(",").filter(Boolean)
|
||||||
|
return ancestors.length === 2
|
||||||
|
},
|
||||||
|
getFirstLevelDeptOptions(nodes) {
|
||||||
|
if (!Array.isArray(nodes)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return nodes.filter(item => this.isFirstLevelRow(item)).map(item => {
|
||||||
|
const current = { ...item }
|
||||||
|
delete current.children
|
||||||
|
return current
|
||||||
|
})
|
||||||
|
},
|
||||||
|
syncArkModelRowsFromForm() {
|
||||||
|
this.arkModelParamRows = [{ label: '', value: '' }]
|
||||||
|
const raw = this.arkForm.modelParm
|
||||||
|
if (!raw || String(raw).trim() === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const arr = JSON.parse(raw)
|
||||||
|
if (Array.isArray(arr) && arr.length) {
|
||||||
|
this.arkModelParamRows = arr.map(x => ({
|
||||||
|
label: (x && x.label) ? String(x.label) : '',
|
||||||
|
value: (x && x.value) ? String(x.value) : ''
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.arkModelParamRows = [{ label: '', value: '' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buildArkModelParmPayload() {
|
||||||
|
const rows = (this.arkModelParamRows || []).filter(r => r.label && r.value)
|
||||||
|
this.arkForm.modelParm = rows.length ? JSON.stringify(rows) : ''
|
||||||
|
},
|
||||||
|
addArkModelParamRow() {
|
||||||
|
this.arkModelParamRows.push({ label: '', value: '' })
|
||||||
|
},
|
||||||
|
removeArkModelParamRow(idx) {
|
||||||
|
if (this.arkModelParamRows.length <= 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.arkModelParamRows.splice(idx, 1)
|
||||||
|
},
|
||||||
|
resetArkForm() {
|
||||||
|
this.arkDeptId = null
|
||||||
|
this.arkForm = {
|
||||||
|
byteApiKey: undefined,
|
||||||
|
project: undefined,
|
||||||
|
modelParm: undefined
|
||||||
|
}
|
||||||
|
this.arkModelParamRows = [{ label: '', value: '' }]
|
||||||
|
if (this.$refs.arkFormRef) {
|
||||||
|
this.$refs.arkFormRef.resetFields()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleArkConfig(row) {
|
||||||
|
if (!this.isFirstLevelRow(row) && !this.isSecondLevelRow(row)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.arkDeptId = row.deptId
|
||||||
|
getDeptArk(row.deptId).then(response => {
|
||||||
|
const d = response.data || {}
|
||||||
|
this.arkForm = {
|
||||||
|
byteApiKey: d.byteApiKey,
|
||||||
|
project: d.project,
|
||||||
|
modelParm: d.modelParm
|
||||||
|
}
|
||||||
|
this.syncArkModelRowsFromForm()
|
||||||
|
this.arkOpen = true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
resetChargeRefund() {
|
||||||
|
this.chargeRefundMoneyDisplay = ""
|
||||||
|
this.chargeRefundAmountDisplay = ""
|
||||||
|
this.chargeRefundForm = {
|
||||||
|
deptId: undefined,
|
||||||
|
deptName: "",
|
||||||
|
orderType: 0,
|
||||||
|
money: undefined,
|
||||||
|
amount: undefined,
|
||||||
|
remark: ""
|
||||||
|
}
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.chargeRefundFormRef) {
|
||||||
|
this.$refs.chargeRefundFormRef.clearValidate()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleChargeRefund(row) {
|
||||||
|
if (!this.isFirstLevelRow(row) && !this.isSecondLevelRow(row)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.chargeRefundMoneyDisplay = ""
|
||||||
|
this.chargeRefundAmountDisplay = ""
|
||||||
|
this.chargeRefundForm = {
|
||||||
|
deptId: row.deptId,
|
||||||
|
deptName: row.deptName,
|
||||||
|
orderType: 0,
|
||||||
|
money: undefined,
|
||||||
|
amount: undefined,
|
||||||
|
remark: ""
|
||||||
|
}
|
||||||
|
this.chargeRefundOpen = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.chargeRefundFormRef) {
|
||||||
|
this.$refs.chargeRefundFormRef.clearValidate()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onChargeRefundMoneyInput(val) {
|
||||||
|
const sanitized = sanitizeMoneyDigits(val)
|
||||||
|
if (!sanitized) {
|
||||||
|
this.chargeRefundMoneyDisplay = ""
|
||||||
|
this.chargeRefundForm.money = undefined
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const raw = parseFloat(sanitized)
|
||||||
|
if (!isNaN(raw) && raw > WESTERN_MONEY_MAX) {
|
||||||
|
this.chargeRefundForm.money = WESTERN_MONEY_MAX
|
||||||
|
this.chargeRefundMoneyDisplay = formatMoneyWesternFinal(WESTERN_MONEY_MAX)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.chargeRefundMoneyDisplay = formatMoneyWesternDisplay(sanitized)
|
||||||
|
this.chargeRefundForm.money = moneyStringToNumber(sanitized)
|
||||||
|
},
|
||||||
|
onChargeRefundMoneyBlur() {
|
||||||
|
if (this.chargeRefundForm.money !== undefined && this.chargeRefundForm.money !== null) {
|
||||||
|
this.chargeRefundMoneyDisplay = formatMoneyWesternFinal(this.chargeRefundForm.money)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onChargeRefundAmountInput(val) {
|
||||||
|
const digits = sanitizeIntDigits(val)
|
||||||
|
this.chargeRefundAmountDisplay = digits === "" ? "" : formatIntWesternDisplay(digits)
|
||||||
|
this.chargeRefundForm.amount = intStringToNumber(digits)
|
||||||
|
},
|
||||||
|
submitChargeRefund() {
|
||||||
|
this.onChargeRefundMoneyBlur()
|
||||||
|
this.$refs["chargeRefundFormRef"].validate(valid => {
|
||||||
|
if (!valid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const data = {
|
||||||
|
deptId: this.chargeRefundForm.deptId,
|
||||||
|
orderType: this.chargeRefundForm.orderType,
|
||||||
|
money: this.chargeRefundForm.money,
|
||||||
|
amount: this.chargeRefundForm.amount,
|
||||||
|
remark: this.chargeRefundForm.remark ? this.chargeRefundForm.remark.trim() : undefined
|
||||||
|
}
|
||||||
|
chargeRefundDept(data).then(() => {
|
||||||
|
this.$modal.msgSuccess("操作成功")
|
||||||
|
this.chargeRefundOpen = false
|
||||||
|
this.getList()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
resetEditScore() {
|
||||||
|
this.editScoreScoreDisplay = ""
|
||||||
|
this.editScoreForm = {
|
||||||
|
deptId: undefined,
|
||||||
|
deptName: "",
|
||||||
|
score: undefined,
|
||||||
|
remark: ""
|
||||||
|
}
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.editScoreFormRef) {
|
||||||
|
this.$refs.editScoreFormRef.clearValidate()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleEditScore(row) {
|
||||||
|
if (!this.isFirstLevelRow(row) && !this.isSecondLevelRow(row)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.editScoreScoreDisplay = ""
|
||||||
|
this.editScoreForm = {
|
||||||
|
deptId: row.deptId,
|
||||||
|
deptName: row.deptName,
|
||||||
|
score: undefined,
|
||||||
|
remark: ""
|
||||||
|
}
|
||||||
|
this.editScoreOpen = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.editScoreFormRef) {
|
||||||
|
this.$refs.editScoreFormRef.clearValidate()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
submitEditScore() {
|
||||||
|
this.$refs["editScoreFormRef"].validate(valid => {
|
||||||
|
if (!valid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const remark = (this.editScoreForm.remark || "").trim()
|
||||||
|
const data = {
|
||||||
|
deptId: this.editScoreForm.deptId,
|
||||||
|
score: this.editScoreForm.score,
|
||||||
|
remark: remark
|
||||||
|
}
|
||||||
|
editScore(data).then(() => {
|
||||||
|
this.$modal.msgSuccess("操作成功")
|
||||||
|
this.editScoreOpen = false
|
||||||
|
this.getList()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
submitArkForm() {
|
||||||
|
this.buildArkModelParmPayload()
|
||||||
|
const payload = {
|
||||||
|
deptId: this.arkDeptId,
|
||||||
|
byteApiKey: this.arkForm.byteApiKey !== undefined && this.arkForm.byteApiKey !== null ? this.arkForm.byteApiKey : '',
|
||||||
|
project: this.arkForm.project !== undefined && this.arkForm.project !== null ? this.arkForm.project : '',
|
||||||
|
modelParm: this.arkForm.modelParm !== undefined && this.arkForm.modelParm !== null ? this.arkForm.modelParm : ''
|
||||||
|
}
|
||||||
|
updateDeptArk(payload).then(() => {
|
||||||
|
this.$modal.msgSuccess('保存成功')
|
||||||
|
this.arkOpen = false
|
||||||
|
this.resetArkForm()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/** 二级团队下拉:走系统部门接口(与页面列表权限一致) */
|
||||||
|
secondLevelDeptListRequest(params) {
|
||||||
|
return listDept(params)
|
||||||
|
},
|
||||||
|
deptRowInSubtree(d, rootId) {
|
||||||
|
if (d.deptId != null && Number(d.deptId) === rootId) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (!d.ancestors) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return String(d.ancestors)
|
||||||
|
.split(",")
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(s => Number(s))
|
||||||
|
.includes(rootId)
|
||||||
|
},
|
||||||
/** 查询部门列表 */
|
/** 查询部门列表 */
|
||||||
getList() {
|
getList() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
listDept(this.queryParams).then(response => {
|
const { deptId, ...deptListQuery } = this.queryParams
|
||||||
this.deptList = this.handleTree(response.data, "deptId")
|
listDept(deptListQuery).then(response => {
|
||||||
|
let raw = response.data || []
|
||||||
|
if (deptId != null && deptId !== "") {
|
||||||
|
const rootId = Number(deptId)
|
||||||
|
raw = raw.filter(d => this.deptRowInSubtree(d, rootId))
|
||||||
|
}
|
||||||
|
this.deptList = this.handleTree(raw, "deptId")
|
||||||
this.loading = false
|
this.loading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
@ -252,13 +864,16 @@ export default {
|
||||||
this.form = {
|
this.form = {
|
||||||
deptId: undefined,
|
deptId: undefined,
|
||||||
parentId: undefined,
|
parentId: undefined,
|
||||||
|
ancestors: undefined,
|
||||||
deptName: undefined,
|
deptName: undefined,
|
||||||
orderNum: undefined,
|
orderNum: undefined,
|
||||||
leader: undefined,
|
leader: undefined,
|
||||||
phone: undefined,
|
phone: undefined,
|
||||||
email: undefined,
|
email: undefined,
|
||||||
|
maxUserCount: 0,
|
||||||
status: "0"
|
status: "0"
|
||||||
}
|
}
|
||||||
|
this.originalForm = {}
|
||||||
this.resetForm("form")
|
this.resetForm("form")
|
||||||
},
|
},
|
||||||
/** 搜索按钮操作 */
|
/** 搜索按钮操作 */
|
||||||
|
|
@ -273,13 +888,18 @@ export default {
|
||||||
/** 新增按钮操作 */
|
/** 新增按钮操作 */
|
||||||
handleAdd(row) {
|
handleAdd(row) {
|
||||||
this.reset()
|
this.reset()
|
||||||
if (row != undefined) {
|
if (row !== undefined) {
|
||||||
|
if (!this.isFirstLevelRow(row)) {
|
||||||
|
this.$modal.msgError("仅允许在一级团队下新增二级团队")
|
||||||
|
return
|
||||||
|
}
|
||||||
this.form.parentId = row.deptId
|
this.form.parentId = row.deptId
|
||||||
}
|
}
|
||||||
|
this.form.maxUserCount = 0
|
||||||
this.open = true
|
this.open = true
|
||||||
this.title = "添加部门"
|
this.title = "添加二级团队"
|
||||||
listDept().then(response => {
|
listDept().then(response => {
|
||||||
this.deptOptions = this.handleTree(response.data, "deptId")
|
this.deptOptions = this.getFirstLevelDeptOptions(this.handleTree(response.data, "deptId"))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
/** 展开/折叠操作 */
|
/** 展开/折叠操作 */
|
||||||
|
|
@ -294,11 +914,16 @@ export default {
|
||||||
handleUpdate(row) {
|
handleUpdate(row) {
|
||||||
this.reset()
|
this.reset()
|
||||||
getDept(row.deptId).then(response => {
|
getDept(row.deptId).then(response => {
|
||||||
this.form = response.data
|
this.form = { ...response.data }
|
||||||
|
this.originalForm = { ...response.data }
|
||||||
|
if (this.form.maxUserCount == null) {
|
||||||
|
this.form.maxUserCount = 0
|
||||||
|
}
|
||||||
this.open = true
|
this.open = true
|
||||||
this.title = "修改部门"
|
this.title = "修改团队"
|
||||||
listDeptExcludeChild(row.deptId).then(response => {
|
listDeptExcludeChild(row.deptId).then(response => {
|
||||||
this.deptOptions = this.handleTree(response.data, "deptId")
|
const allOptions = this.handleTree(response.data, "deptId")
|
||||||
|
this.deptOptions = this.getFirstLevelDeptOptions(allOptions)
|
||||||
if (this.deptOptions.length == 0) {
|
if (this.deptOptions.length == 0) {
|
||||||
const noResultsOptions = { deptId: this.form.parentId, deptName: this.form.parentName, children: [] }
|
const noResultsOptions = { deptId: this.form.parentId, deptName: this.form.parentName, children: [] }
|
||||||
this.deptOptions.push(noResultsOptions)
|
this.deptOptions.push(noResultsOptions)
|
||||||
|
|
@ -310,7 +935,21 @@ export default {
|
||||||
submitForm: function() {
|
submitForm: function() {
|
||||||
this.$refs["form"].validate(valid => {
|
this.$refs["form"].validate(valid => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
|
if (this.form.deptId == undefined && !this.deptOptions.some(item => Number(item.deptId) === Number(this.form.parentId))) {
|
||||||
|
this.$modal.msgError("仅允许在一级团队下创建二级团队")
|
||||||
|
return
|
||||||
|
}
|
||||||
if (this.form.deptId != undefined) {
|
if (this.form.deptId != undefined) {
|
||||||
|
if (this.isFirstLevelEditForm) {
|
||||||
|
const oldDept = this.originalForm || {}
|
||||||
|
this.form.parentId = oldDept.parentId
|
||||||
|
this.form.orderNum = oldDept.orderNum
|
||||||
|
this.form.leader = oldDept.leader
|
||||||
|
this.form.phone = oldDept.phone
|
||||||
|
this.form.email = oldDept.email
|
||||||
|
this.form.status = oldDept.status
|
||||||
|
this.form.maxUserCount = oldDept.maxUserCount
|
||||||
|
}
|
||||||
updateDept(this.form).then(response => {
|
updateDept(this.form).then(response => {
|
||||||
this.$modal.msgSuccess("修改成功")
|
this.$modal.msgSuccess("修改成功")
|
||||||
this.open = false
|
this.open = false
|
||||||
|
|
|
||||||
|
|
@ -298,11 +298,11 @@ export default {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "3",
|
value: "3",
|
||||||
label: "本部门数据权限"
|
label: "本团队数据权限"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "4",
|
value: "4",
|
||||||
label: "本部门及以下数据权限"
|
label: "本团队及以下数据权限"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "5",
|
value: "5",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<pane size="16">
|
<pane size="16">
|
||||||
<el-col>
|
<el-col>
|
||||||
<div class="head-container">
|
<div class="head-container">
|
||||||
<el-input v-model="deptName" placeholder="请输入部门名称" clearable size="small" prefix-icon="el-icon-search" style="margin-bottom: 20px" />
|
<el-input v-model="deptName" placeholder="请输入团队名称" clearable size="small" prefix-icon="el-icon-search" style="margin-bottom: 20px" />
|
||||||
</div>
|
</div>
|
||||||
<div class="head-container">
|
<div class="head-container">
|
||||||
<el-tree :data="deptOptions" :props="defaultProps" :expand-on-click-node="false" :filter-node-method="filterNode" ref="tree" node-key="id" default-expand-all highlight-current @node-click="handleNodeClick" />
|
<el-tree :data="deptOptions" :props="defaultProps" :expand-on-click-node="false" :filter-node-method="filterNode" ref="tree" node-key="id" default-expand-all highlight-current @node-click="handleNodeClick" />
|
||||||
|
|
@ -61,7 +61,7 @@
|
||||||
<el-table-column label="用户编号" align="center" key="userId" prop="userId" v-if="columns[0].visible" />
|
<el-table-column label="用户编号" align="center" key="userId" prop="userId" v-if="columns[0].visible" />
|
||||||
<el-table-column label="用户名称" align="center" key="userName" prop="userName" v-if="columns[1].visible" :show-overflow-tooltip="true" />
|
<el-table-column label="用户名称" align="center" key="userName" prop="userName" v-if="columns[1].visible" :show-overflow-tooltip="true" />
|
||||||
<el-table-column label="用户昵称" align="center" key="nickName" prop="nickName" v-if="columns[2].visible" :show-overflow-tooltip="true" />
|
<el-table-column label="用户昵称" align="center" key="nickName" prop="nickName" v-if="columns[2].visible" :show-overflow-tooltip="true" />
|
||||||
<el-table-column label="部门" align="center" key="deptName" prop="dept.deptName" v-if="columns[3].visible" :show-overflow-tooltip="true" />
|
<el-table-column label="团队" align="center" key="deptName" prop="dept.deptName" v-if="columns[3].visible" :show-overflow-tooltip="true" />
|
||||||
<el-table-column label="手机号码" align="center" key="phonenumber" prop="phonenumber" v-if="columns[4].visible" width="120" />
|
<el-table-column label="手机号码" align="center" key="phonenumber" prop="phonenumber" v-if="columns[4].visible" width="120" />
|
||||||
<el-table-column label="状态" align="center" key="status" v-if="columns[5].visible">
|
<el-table-column label="状态" align="center" key="status" v-if="columns[5].visible">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
|
|
@ -236,7 +236,7 @@ export default {
|
||||||
enabledDeptOptions: undefined,
|
enabledDeptOptions: undefined,
|
||||||
// 是否显示弹出层
|
// 是否显示弹出层
|
||||||
open: false,
|
open: false,
|
||||||
// 部门名称
|
// 团队名称
|
||||||
deptName: undefined,
|
deptName: undefined,
|
||||||
// 默认密码
|
// 默认密码
|
||||||
initPassword: undefined,
|
initPassword: undefined,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
<div class="pull-right">{{ user.email }}</div>
|
<div class="pull-right">{{ user.email }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
<svg-icon icon-class="tree" />所属部门
|
<svg-icon icon-class="tree" />所属团队
|
||||||
<div class="pull-right" v-if="user.dept">{{ user.dept.deptName }} / {{ postGroup }}</div>
|
<div class="pull-right" v-if="user.dept">{{ user.dept.deptName }} / {{ postGroup }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,4 @@ VITE_AMAP_SAFE_KEY = 23d3fdbc6b4eaf65d4a82a16510938c9
|
||||||
VITE_REGISTER_SOURCE = ALL
|
VITE_REGISTER_SOURCE = ALL
|
||||||
#VITE_REGISTER_SOURCE = XM001
|
#VITE_REGISTER_SOURCE = XM001
|
||||||
VITE_SHOW_PAY = SUCCESS
|
VITE_SHOW_PAY = SUCCESS
|
||||||
|
VITE_API_URL = http://47.86.170.114:8011/api
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# 端口
|
# 端口
|
||||||
VITE_PORT = 8887
|
VITE_PORT = 8887
|
||||||
VITE_API_URL = https://api.undressing.name/api
|
VITE_API_URL = http://47.86.170.114:8011/api
|
||||||
|
|
@ -10,7 +10,7 @@ yarn-error.log*
|
||||||
/test/e2e/reports/
|
/test/e2e/reports/
|
||||||
selenium-debug.log
|
selenium-debug.log
|
||||||
nodejs/public/uploads
|
nodejs/public/uploads
|
||||||
|
*.zip
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -7,8 +7,9 @@
|
||||||
content="width=device-width,initial-scale=1.0, maximum-scale=1.0,minimum-scale=1.0" />
|
content="width=device-width,initial-scale=1.0, maximum-scale=1.0,minimum-scale=1.0" />
|
||||||
<link
|
<link
|
||||||
rel="icon"
|
rel="icon"
|
||||||
href="/images/logo.png" />
|
type="image/svg+xml"
|
||||||
<title>asio</title>
|
href="/favicon.svg" />
|
||||||
|
<title>智绘影音</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
|
|
@ -23,25 +23,30 @@ body {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body[arco-theme='dark'] {
|
||||||
|
color: #e5e7eb;
|
||||||
|
background-color: #121826;
|
||||||
|
}
|
||||||
|
|
||||||
// firefox 滚动条样式
|
// firefox 滚动条样式
|
||||||
* {
|
* {
|
||||||
scrollbar-color: rgba(144, 147, 153, 0.5) #0f0f12;
|
scrollbar-color: rgba(77, 137, 255, 0.35) #121826;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*滚动条整体样式*/
|
/*滚动条整体样式*/
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
background-color: #0f0f12;
|
background-color: #121826;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*滚动条里面小方块*/
|
/*滚动条里面小方块*/
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background-color: red;
|
background-color: rgba(77, 137, 255, 0.35);
|
||||||
border: 3px solid #0f0f12;
|
border: 3px solid #121826;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*滚动条里面轨道*/
|
/*滚动条里面轨道*/
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: #0f0f12;
|
background: #121826;
|
||||||
}
|
}
|
||||||
|
|
||||||
:focus {
|
:focus {
|
||||||
|
|
@ -67,11 +72,22 @@ body {
|
||||||
|
|
||||||
// 正常页面切换动画
|
// 正常页面切换动画
|
||||||
.page-enter-active {
|
.page-enter-active {
|
||||||
animation: page-in 0.3s;
|
animation: page-in 0.35s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-leave-active {
|
.page-leave-active {
|
||||||
animation: page-in 0.3s reverse;
|
animation: page-in 0.28s ease-in reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 路由视图淡入淡出(与 appMain 中 portal-page 过渡配合) */
|
||||||
|
.portal-page-enter-active,
|
||||||
|
.portal-page-leave-active {
|
||||||
|
transition: opacity 0.32s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-page-enter-from,
|
||||||
|
.portal-page-leave-to {
|
||||||
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes page-in {
|
@keyframes page-in {
|
||||||
|
|
@ -195,11 +211,11 @@ fieldset {
|
||||||
padding: 20px 10px;
|
padding: 20px 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #fd8000;
|
color: #f59e0b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-error {
|
.page-error {
|
||||||
color: red;
|
color: #ef4444;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
}
|
}
|
||||||
|
|
@ -240,33 +256,51 @@ input:-webkit-autofill {
|
||||||
width: 100vw !important;
|
width: 100vw !important;
|
||||||
height: 100vh !important;
|
height: 100vh !important;
|
||||||
z-index: 100 !important;
|
z-index: 100 !important;
|
||||||
background: #fff;
|
background: #121826;
|
||||||
padding: 12px !important;
|
padding: 12px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-modal-simple {
|
.arco-modal-simple {
|
||||||
background: #0f0f12;
|
background: #1a1f2e;
|
||||||
border-radius: 10px;
|
border-radius: 12px;
|
||||||
border: 2px solid #e6217a;
|
border: none;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(77, 137, 255, 0.12),
|
||||||
|
0 4px 20px rgba(0, 0, 0, 0.35);
|
||||||
.arco-modal-title {
|
.arco-modal-title {
|
||||||
color: #fff;
|
color: #e5e7eb;
|
||||||
}
|
}
|
||||||
.arco-modal-body {
|
.arco-modal-body {
|
||||||
color: #fff;
|
color: #e5e7eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-modal-footer {
|
.arco-modal-footer {
|
||||||
.arco-btn-secondary {
|
.arco-btn-secondary {
|
||||||
color: #ffffff;
|
color: #e5e7eb;
|
||||||
background-color: #1a1a1a;
|
background-color: #252b3a;
|
||||||
border: 1px solid #1a1a1a;
|
border: 1px solid rgba(77, 137, 255, 0.15);
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #262626;
|
background-color: rgba(77, 137, 255, 0.14);
|
||||||
|
box-shadow: 0 0 12px rgba(77, 137, 255, 0.25);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 主按钮 hover:轻微提亮 + 外发光 */
|
||||||
|
.arco-btn-primary:not(.arco-btn-disabled):hover {
|
||||||
|
filter: brightness(1.06);
|
||||||
|
box-shadow: 0 0 12px rgba(77, 137, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入类 focus 同色系发光边框 */
|
||||||
|
.arco-input-wrapper:focus-within:not(.arco-input-disabled),
|
||||||
|
.arco-textarea-wrapper:focus-within:not(.arco-textarea-disabled),
|
||||||
|
.arco-select-view:focus-within:not(.arco-select-view-disabled) {
|
||||||
|
border-color: rgb(var(--primary-6)) !important;
|
||||||
|
box-shadow: 0 0 0 2px rgba(77, 137, 255, 0.22) !important;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
@media (max-width: 576px) {
|
||||||
.arco-modal-simple {
|
.arco-modal-simple {
|
||||||
width: calc(100% - 20px);
|
width: calc(100% - 20px);
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -615,4 +615,4 @@
|
||||||
right: 15px
|
right: 15px
|
||||||
}
|
}
|
||||||
|
|
||||||
/*# sourceMappingURL=quill.core.css.map*/
|
/* Quill core styles - source map removed to fix build error */
|
||||||
|
|
@ -1286,4 +1286,4 @@
|
||||||
border: 1px solid #ccc
|
border: 1px solid #ccc
|
||||||
}
|
}
|
||||||
|
|
||||||
/*# sourceMappingURL=quill.snow.css.map*/
|
/* Quill snow styles - source map removed to fix build error */
|
||||||
|
|
@ -1,10 +1,40 @@
|
||||||
|
/* portal-ui 暗色科技主题 — 设计令牌(与 Arco 暗色模式协同) */
|
||||||
|
|
||||||
body {
|
body {
|
||||||
// 背景色 - 浅
|
--portal-bg-page: #121826;
|
||||||
|
--portal-bg-card: #1a1f2e;
|
||||||
|
--portal-bg-sub: #252b3a;
|
||||||
|
--portal-text: #e5e7eb;
|
||||||
|
--portal-text-2: #9ca3af;
|
||||||
|
--portal-text-disabled: #6b7280;
|
||||||
|
--portal-primary: #4d89ff;
|
||||||
|
--portal-accent: #9d6bff;
|
||||||
|
--portal-success: #10b981;
|
||||||
|
--portal-warning: #f59e0b;
|
||||||
|
--portal-danger: #ef4444;
|
||||||
|
--portal-glow-border: rgba(77, 137, 255, 0.1);
|
||||||
|
--portal-shadow-soft: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
--portal-shadow-glow: 0 0 12px rgba(77, 137, 255, 0.4);
|
||||||
--mf-color-bg-3: #f8f8fa;
|
--mf-color-bg-3: #f8f8fa;
|
||||||
--his-border-color: rgb(166, 124, 82, 0.3);
|
--his-border-color: rgb(166, 124, 82, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[arco-theme='dark'] {
|
body[arco-theme='dark'] {
|
||||||
// 背景色 - 浅
|
--mf-color-bg-3: #252b3a;
|
||||||
--mf-color-bg-3: #17171a;
|
/* Arco 语义色贴近本主题层级 */
|
||||||
|
--color-bg-1: #121826;
|
||||||
|
--color-bg-2: #1a1f2e;
|
||||||
|
--color-bg-3: #252b3a;
|
||||||
|
--color-bg-4: #2e3548;
|
||||||
|
--color-bg-5: #363d52;
|
||||||
|
--color-fill-1: rgba(255, 255, 255, 0.04);
|
||||||
|
--color-fill-2: rgba(255, 255, 255, 0.06);
|
||||||
|
--color-fill-3: rgba(255, 255, 255, 0.08);
|
||||||
|
--color-border-1: rgba(77, 137, 255, 0.12);
|
||||||
|
--color-border-2: rgba(77, 137, 255, 0.18);
|
||||||
|
--color-border-3: rgba(77, 137, 255, 0.25);
|
||||||
|
--color-text-1: #e5e7eb;
|
||||||
|
--color-text-2: #9ca3af;
|
||||||
|
--color-text-3: #9ca3af;
|
||||||
|
--color-text-4: #6b7280;
|
||||||
}
|
}
|
||||||
|
|
@ -8,10 +8,6 @@
|
||||||
fullscreen
|
fullscreen
|
||||||
:class="`${prefixCls}-wrapper`"
|
:class="`${prefixCls}-wrapper`"
|
||||||
:modal-class="`${prefixCls}-dialog`">
|
:modal-class="`${prefixCls}-dialog`">
|
||||||
<mf-video
|
|
||||||
autoplay
|
|
||||||
:controls="false"
|
|
||||||
modelValue="https://images.iqyjsnwv.com/tmp/hello-BCSGJ8fP.mp4" />
|
|
||||||
<div :class="`${prefixCls}-shadow`">
|
<div :class="`${prefixCls}-shadow`">
|
||||||
<div :class="`${prefixCls}-title`">
|
<div :class="`${prefixCls}-title`">
|
||||||
{{ $t('common.fbTitle') }}
|
{{ $t('common.fbTitle') }}
|
||||||
|
|
@ -38,6 +34,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import i18n from '@/lang/i18n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'mf-forbidden',
|
name: 'mf-forbidden',
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -53,7 +51,13 @@ export default {
|
||||||
this.$router.replace('/403')
|
this.$router.replace('/403')
|
||||||
},
|
},
|
||||||
ok() {
|
ok() {
|
||||||
this.$store.dispatch('main/setForbidden', false)
|
// 满18+:关闭弹窗并进入首页
|
||||||
|
Promise.resolve(this.$store.dispatch('main/setForbidden', false)).finally(() => {
|
||||||
|
// 默认语言:繁体中文(zh_HK)
|
||||||
|
this.$store.dispatch('main/setLanguage', 'zh_HK')
|
||||||
|
i18n.global.locale = 'zh_HK'
|
||||||
|
this.$router.push({ name: 'index' })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -77,9 +81,9 @@ export default {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
// width: 428px;
|
// width: 428px;
|
||||||
// height: 258px;
|
// height: 258px;
|
||||||
// background: linear-gradient(0deg, #271433 0%, #e6217a 49%);
|
// background: linear-gradient(160deg, #121826 0%, #1a1f2e 100%);
|
||||||
// border-radius: 20px;
|
// border-radius: 20px;
|
||||||
// border: 2px solid #e6217a;
|
// border: none; box-shadow: inset 0 0 0 1px rgba(77,137,255,0.1);
|
||||||
// top: 45% !important;
|
// top: 45% !important;
|
||||||
// transform: translateY(-45%) !important;
|
// transform: translateY(-45%) !important;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,9 +41,12 @@ export default {
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
.preview-dialog {
|
.preview-dialog {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #0f0f12;
|
background: #1a1f2e;
|
||||||
border-radius: 20px;
|
border-radius: 12px;
|
||||||
border: 2px solid #e6217a;
|
border: none;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(77, 137, 255, 0.1),
|
||||||
|
0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
height: 520px;
|
height: 520px;
|
||||||
width: 864px;
|
width: 864px;
|
||||||
top: 45% !important;
|
top: 45% !important;
|
||||||
|
|
|
||||||
|
|
@ -520,15 +520,18 @@ export default {
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
.test-pay-dialog {
|
.test-pay-dialog {
|
||||||
border-radius: 20px;
|
border-radius: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
0deg,
|
160deg,
|
||||||
rgba(39, 20, 51, 0.7) 0%,
|
rgba(18, 24, 38, 0.98) 0%,
|
||||||
rgba(230, 33, 122, 0.7) 49%
|
rgba(26, 31, 46, 0.98) 50%,
|
||||||
|
rgba(37, 43, 58, 0.96) 100%
|
||||||
);
|
);
|
||||||
border-radius: 20px;
|
border: none;
|
||||||
border: 2px solid #e6217a;
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(77, 137, 255, 0.1),
|
||||||
|
0 4px 20px rgba(0, 0, 0, 0.35);
|
||||||
width: 500px;
|
width: 500px;
|
||||||
// height: 320px;
|
// height: 320px;
|
||||||
top: 45% !important;
|
top: 45% !important;
|
||||||
|
|
@ -621,15 +624,18 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.vm-card-dialog {
|
.vm-card-dialog {
|
||||||
border-radius: 20px;
|
border-radius: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
0deg,
|
160deg,
|
||||||
rgba(39, 20, 51, 0.7) 0%,
|
rgba(18, 24, 38, 0.98) 0%,
|
||||||
rgba(230, 33, 122, 0.7) 49%
|
rgba(26, 31, 46, 0.98) 50%,
|
||||||
|
rgba(37, 43, 58, 0.96) 100%
|
||||||
);
|
);
|
||||||
border-radius: 20px;
|
border: none;
|
||||||
border: 2px solid #e6217a;
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(77, 137, 255, 0.1),
|
||||||
|
0 4px 20px rgba(0, 0, 0, 0.35);
|
||||||
width: 600px;
|
width: 600px;
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
@ -700,19 +706,20 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.arco-select-dropdown) {
|
:deep(.arco-select-dropdown) {
|
||||||
background-color: rgba(39, 20, 51, 0.95);
|
background-color: #1a1f2e;
|
||||||
border: 1px solid #e6217a;
|
border: 1px solid rgba(77, 137, 255, 0.25);
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.arco-select-option) {
|
:deep(.arco-select-option) {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(230, 33, 122, 0.3);
|
background-color: rgba(77, 137, 255, 0.14);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.arco-select-option-selected {
|
&.arco-select-option-selected {
|
||||||
background-color: rgba(230, 33, 122, 0.5);
|
background-color: rgba(77, 137, 255, 0.22);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -122,11 +122,13 @@ export default {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: wrap;
|
flex-flow: wrap;
|
||||||
&-dialog {
|
&-dialog {
|
||||||
border-radius: 20px;
|
border-radius: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #0f0f12;
|
background: #1a1f2e;
|
||||||
border-radius: 20px;
|
border: none;
|
||||||
border: 2px solid #e6217a;
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(77, 137, 255, 0.1),
|
||||||
|
0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
width: 500px;
|
width: 500px;
|
||||||
top: 45% !important;
|
top: 45% !important;
|
||||||
transform: translateY(-45%) !important;
|
transform: translateY(-45%) !important;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,460 @@
|
||||||
|
<template>
|
||||||
|
<div class="rich-editor-root">
|
||||||
|
<!-- 工具栏 -->
|
||||||
|
<div class="toolbar">
|
||||||
|
<button class="tool-btn" type="button" @click="execCommand('bold')"><b>B</b></button>
|
||||||
|
<button class="tool-btn" type="button" @click="execCommand('italic')"><i>I</i></button>
|
||||||
|
<button class="tool-btn" type="button" @click="openImagePicker">{{ $t('common.insertImage') || '插入图片' }}</button>
|
||||||
|
<button class="tool-btn" type="button" @click="clear">清空</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 隐藏的文件输入 -->
|
||||||
|
<input
|
||||||
|
ref="fileInputRef"
|
||||||
|
class="hidden-input"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
multiple
|
||||||
|
@change="handleSelectFiles"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 编辑器区域 -->
|
||||||
|
<div
|
||||||
|
ref="editorRef"
|
||||||
|
class="user-input rich-editor"
|
||||||
|
contenteditable="true"
|
||||||
|
:data-placeholder="placeholder || '请输入文本...'"
|
||||||
|
@input="handleInput"
|
||||||
|
@keyup="handleKeyup"
|
||||||
|
@click="handleEditorClick"
|
||||||
|
@keydown="handleKeydown"
|
||||||
|
@paste="handlePaste"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- @ 图片选择面板 -->
|
||||||
|
<div v-if="mentionVisible" class="mention-panel">
|
||||||
|
<div
|
||||||
|
v-for="(item, idx) in mentionImageList"
|
||||||
|
:key="item.url"
|
||||||
|
class="mention-item"
|
||||||
|
@mousedown.prevent="insertMentionImage(item)"
|
||||||
|
>
|
||||||
|
<img :src="item.url" class="mention-thumb" />
|
||||||
|
<span class="mention-label">@图{{ idx + 1 }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="mentionImageList.length === 0" class="mention-empty">
|
||||||
|
{{ $t('common.noImageToMention') || '暂无可引用图片' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { nextTick, onMounted, ref, watch } from 'vue'
|
||||||
|
import { uploadFile, extractUploadUrlFromResponse, PORTAL_TENCENT_COS_UPLOAD_URL } from '@/utils/file'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RichTextEditor',
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '请输入文本生成视频...'
|
||||||
|
},
|
||||||
|
uploadedImages: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue', 'text-change', 'image-upload'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const editorRef = ref(null)
|
||||||
|
const fileInputRef = ref(null)
|
||||||
|
const savedSelectionRange = ref(null)
|
||||||
|
const mentionVisible = ref(false)
|
||||||
|
const mentionImageList = ref([])
|
||||||
|
const MAX_IMAGE_COUNT = 4
|
||||||
|
|
||||||
|
// 执行编辑器命令
|
||||||
|
const execCommand = (command) => {
|
||||||
|
if (!editorRef.value) return
|
||||||
|
editorRef.value.focus()
|
||||||
|
document.execCommand(command, false, null)
|
||||||
|
handleInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开图片选择器
|
||||||
|
const openImagePicker = () => {
|
||||||
|
if (fileInputRef.value) {
|
||||||
|
fileInputRef.value.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理文件选择 - 上传到服务器
|
||||||
|
const handleSelectFiles = async (event) => {
|
||||||
|
const files = event.target.files
|
||||||
|
if (!files.length) return
|
||||||
|
|
||||||
|
for (let file of files) {
|
||||||
|
if (file.type.startsWith('image/')) {
|
||||||
|
try {
|
||||||
|
// 显示上传中提示
|
||||||
|
const loading = window.$message ? window.$message.loading('上传中...') : null
|
||||||
|
|
||||||
|
// 上传到后端
|
||||||
|
const res = await uploadFile({
|
||||||
|
url: PORTAL_TENCENT_COS_UPLOAD_URL,
|
||||||
|
file: file,
|
||||||
|
name: 'file'
|
||||||
|
})
|
||||||
|
|
||||||
|
const imageUrl = extractUploadUrlFromResponse(res)
|
||||||
|
if (res && (Number(res.code) === 200 || res.code === 200) && imageUrl) {
|
||||||
|
insertImage(imageUrl, file.name)
|
||||||
|
|
||||||
|
// 通知父组件有新图片上传成功(用于@功能)
|
||||||
|
emit('image-upload', { url: imageUrl, name: file.name })
|
||||||
|
} else {
|
||||||
|
console.error('上传失败:', res)
|
||||||
|
alert('图片上传失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传图片出错:', error)
|
||||||
|
alert('图片上传失败: ' + (error.message || '未知错误'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空input
|
||||||
|
event.target.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插入图片
|
||||||
|
const insertImage = (url, name = 'image') => {
|
||||||
|
if (!editorRef.value) return
|
||||||
|
|
||||||
|
editorRef.value.focus()
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (!selection) return
|
||||||
|
|
||||||
|
const img = document.createElement('img')
|
||||||
|
img.src = url
|
||||||
|
img.alt = name
|
||||||
|
img.style.maxWidth = '100%'
|
||||||
|
img.style.height = 'auto'
|
||||||
|
img.style.margin = '4px 0'
|
||||||
|
img.setAttribute('data-image-name', name)
|
||||||
|
|
||||||
|
const range = selection.getRangeAt(0)
|
||||||
|
range.deleteContents()
|
||||||
|
range.insertNode(img)
|
||||||
|
|
||||||
|
// 添加空格
|
||||||
|
const space = document.createTextNode(' ')
|
||||||
|
range.setStartAfter(img)
|
||||||
|
range.setEndAfter(img)
|
||||||
|
range.insertNode(space)
|
||||||
|
|
||||||
|
handleInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理输入
|
||||||
|
const handleInput = () => {
|
||||||
|
if (!editorRef.value) return
|
||||||
|
const content = editorRef.value.innerHTML
|
||||||
|
emit('update:modelValue', content)
|
||||||
|
emit('text-change', content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理按键
|
||||||
|
const handleKeyup = (e) => {
|
||||||
|
if (e.key === '@') {
|
||||||
|
showMentionPanel()
|
||||||
|
} else if (e.key === 'Escape' && mentionVisible.value) {
|
||||||
|
mentionVisible.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeydown = (e) => {
|
||||||
|
if (e.key === '@') {
|
||||||
|
// 保存当前选择位置
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (selection && selection.rangeCount > 0) {
|
||||||
|
savedSelectionRange.value = selection.getRangeAt(0).cloneRange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertPlainTextAtCursor = (text) => {
|
||||||
|
if (!editorRef.value || text == null) return
|
||||||
|
const normalized = String(text).replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
||||||
|
editorRef.value.focus()
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (!selection || selection.rangeCount === 0) return
|
||||||
|
let range = selection.getRangeAt(0)
|
||||||
|
if (!editorRef.value.contains(range.commonAncestorContainer)) {
|
||||||
|
const r = document.createRange()
|
||||||
|
r.selectNodeContents(editorRef.value)
|
||||||
|
r.collapse(false)
|
||||||
|
selection.removeAllRanges()
|
||||||
|
selection.addRange(r)
|
||||||
|
range = selection.getRangeAt(0)
|
||||||
|
}
|
||||||
|
range.deleteContents()
|
||||||
|
const lines = normalized.split('\n')
|
||||||
|
const fragment = document.createDocumentFragment()
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
if (line) fragment.appendChild(document.createTextNode(line))
|
||||||
|
if (index < lines.length - 1) fragment.appendChild(document.createElement('br'))
|
||||||
|
})
|
||||||
|
range.insertNode(fragment)
|
||||||
|
range.collapse(false)
|
||||||
|
selection.removeAllRanges()
|
||||||
|
selection.addRange(range)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePaste = (e) => {
|
||||||
|
if (!editorRef.value) return
|
||||||
|
const cd = e.clipboardData
|
||||||
|
if (!cd) return
|
||||||
|
const items = cd.items
|
||||||
|
if (items) {
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
if (items[i].type.indexOf('image') !== -1) {
|
||||||
|
const blob = items[i].getAsFile()
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (event) => {
|
||||||
|
insertImage(event.target.result, 'pasted-image')
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(blob)
|
||||||
|
e.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let text = cd.getData('text/plain') || ''
|
||||||
|
if (!text && cd.getData('text/html')) {
|
||||||
|
const tmp = document.createElement('div')
|
||||||
|
tmp.innerHTML = cd.getData('text/html')
|
||||||
|
text = tmp.innerText || ''
|
||||||
|
}
|
||||||
|
e.preventDefault()
|
||||||
|
insertPlainTextAtCursor(text)
|
||||||
|
handleInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示 @ 面板
|
||||||
|
const showMentionPanel = () => {
|
||||||
|
if (props.uploadedImages && props.uploadedImages.length > 0) {
|
||||||
|
mentionImageList.value = props.uploadedImages.slice(0, MAX_IMAGE_COUNT)
|
||||||
|
mentionVisible.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插入 @ 图片
|
||||||
|
const insertMentionImage = (imageItem) => {
|
||||||
|
if (!editorRef.value || !savedSelectionRange.value) {
|
||||||
|
mentionVisible.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
editorRef.value.focus()
|
||||||
|
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (selection) {
|
||||||
|
selection.removeAllRanges()
|
||||||
|
selection.addRange(savedSelectionRange.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const img = document.createElement('img')
|
||||||
|
img.src = imageItem.url
|
||||||
|
img.alt = '@图片'
|
||||||
|
img.style.maxWidth = '60px'
|
||||||
|
img.style.height = 'auto'
|
||||||
|
img.style.verticalAlign = 'middle'
|
||||||
|
img.setAttribute('data-mention', 'true')
|
||||||
|
|
||||||
|
const range = savedSelectionRange.value
|
||||||
|
range.deleteContents()
|
||||||
|
range.insertNode(img)
|
||||||
|
|
||||||
|
// 添加空格
|
||||||
|
const space = document.createTextNode(' ')
|
||||||
|
range.setStartAfter(img)
|
||||||
|
range.setEndAfter(img)
|
||||||
|
range.insertNode(space)
|
||||||
|
|
||||||
|
mentionVisible.value = false
|
||||||
|
handleInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空编辑器
|
||||||
|
const clear = () => {
|
||||||
|
if (!editorRef.value) return
|
||||||
|
editorRef.value.innerHTML = ''
|
||||||
|
handleInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理编辑器点击
|
||||||
|
const handleEditorClick = () => {
|
||||||
|
if (mentionVisible.value) {
|
||||||
|
mentionVisible.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 props 变化
|
||||||
|
watch(() => props.modelValue, (newValue) => {
|
||||||
|
if (editorRef.value && editorRef.value.innerHTML !== newValue) {
|
||||||
|
editorRef.value.innerHTML = newValue || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听上传的图片列表更新
|
||||||
|
watch(() => props.uploadedImages, (newImages) => {
|
||||||
|
mentionImageList.value = newImages || []
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (editorRef.value) {
|
||||||
|
editorRef.value.innerHTML = props.modelValue || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
editorRef,
|
||||||
|
fileInputRef,
|
||||||
|
mentionVisible,
|
||||||
|
mentionImageList,
|
||||||
|
execCommand,
|
||||||
|
openImagePicker,
|
||||||
|
handleSelectFiles,
|
||||||
|
handleInput,
|
||||||
|
handleKeyup,
|
||||||
|
handleKeydown,
|
||||||
|
handlePaste,
|
||||||
|
handleEditorClick,
|
||||||
|
insertMentionImage,
|
||||||
|
clear
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.rich-editor-root {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #1a1b20;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
|
||||||
|
.tool-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-editor {
|
||||||
|
min-height: 120px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #1a1b20;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
/* 覆盖粘贴残留的内联颜色,保证深底上始终可读 */
|
||||||
|
:deep(*) {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:empty:before {
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
color: #666;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
margin: 4px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
background: #1f2128;
|
||||||
|
border: 1px solid #3a3d47;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 1000;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
width: 280px;
|
||||||
|
|
||||||
|
.mention-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #2a2d38;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-thumb {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-label {
|
||||||
|
color: #a1a4b3;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-empty {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,600 @@
|
||||||
|
<template>
|
||||||
|
<div class="video-editor-root">
|
||||||
|
<div v-if="showToolbar" class="toolbar">
|
||||||
|
<button class="tool-btn" type="button" @click="openImagePicker">插入参考素材</button>
|
||||||
|
<button class="tool-btn" type="button" @click="clear">清空所有参考素材和文字</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref="fileInputRef"
|
||||||
|
class="hidden-input"
|
||||||
|
type="file"
|
||||||
|
accept="image/*,video/*,audio/*"
|
||||||
|
multiple
|
||||||
|
@change="handleSelectFiles" />
|
||||||
|
<div class="editor-wrapper">
|
||||||
|
<div
|
||||||
|
ref="editorRef"
|
||||||
|
class="user-input video-rich-editor"
|
||||||
|
contenteditable="true"
|
||||||
|
:data-placeholder="placeholder || '请输入文本生成视频...'"
|
||||||
|
@input="handleInput"
|
||||||
|
@paste="handlePaste"
|
||||||
|
@keyup="handleKeyup"
|
||||||
|
@click="handleEditorClick"></div>
|
||||||
|
<div v-if="mentionVisible" class="mention-panel">
|
||||||
|
<div
|
||||||
|
v-for="item in mentionImageList"
|
||||||
|
:key="item.url"
|
||||||
|
class="mention-item"
|
||||||
|
@mousedown.prevent="insertMentionImage(item)">
|
||||||
|
<img v-if="item.mediaType === 'image'" :src="item.url" class="mention-thumb" alt="" />
|
||||||
|
<span v-else class="mention-type-badge">{{ item.mediaType === 'video' ? '视频' : '音频' }}</span>
|
||||||
|
<span class="mention-label">参考{{ item.refNo }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="mentionImageList.length === 0" class="mention-empty">暂无可引用素材</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { nextTick, onMounted, ref } from 'vue'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
|
import { uploadFile, extractUploadUrlFromResponse, PORTAL_TENCENT_COS_UPLOAD_URL } from '@/utils/file'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
/** 文生视频等场景可隐藏工具栏,仅保留纯文本编辑 */
|
||||||
|
showToolbar: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['text-change'])
|
||||||
|
|
||||||
|
const editorRef = ref(null)
|
||||||
|
const fileInputRef = ref(null)
|
||||||
|
const savedSelectionRange = ref(null)
|
||||||
|
const MAX_IMAGE_COUNT = 9
|
||||||
|
const mentionVisible = ref(false)
|
||||||
|
const mentionImageList = ref([])
|
||||||
|
|
||||||
|
const detectMediaType = (file) => {
|
||||||
|
const mime = (file?.type || '').toLowerCase()
|
||||||
|
if (mime.startsWith('image/')) return 'image'
|
||||||
|
if (mime.startsWith('video/')) return 'video'
|
||||||
|
if (mime.startsWith('audio/')) return 'audio'
|
||||||
|
const lowerName = (file?.name || '').toLowerCase()
|
||||||
|
if (/\.(png|jpe?g|gif|webp|bmp|svg)$/.test(lowerName)) return 'image'
|
||||||
|
if (/\.(mp4|mov|webm|m4v|avi|mkv)$/.test(lowerName)) return 'video'
|
||||||
|
if (/\.(mp3|wav|aac|m4a|ogg|flac)$/.test(lowerName)) return 'audio'
|
||||||
|
return 'image'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPlainText = () => (editorRef.value?.innerText || '').trim()
|
||||||
|
|
||||||
|
const focusEditorToEnd = () => {
|
||||||
|
if (!editorRef.value) return
|
||||||
|
editorRef.value.focus()
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (!selection) return
|
||||||
|
const range = document.createRange()
|
||||||
|
range.selectNodeContents(editorRef.value)
|
||||||
|
range.collapse(false)
|
||||||
|
selection.removeAllRanges()
|
||||||
|
selection.addRange(range)
|
||||||
|
savedSelectionRange.value = range.cloneRange()
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveSelection = () => {
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (!selection || selection.rangeCount === 0) return
|
||||||
|
const range = selection.getRangeAt(0)
|
||||||
|
if (editorRef.value?.contains(range.commonAncestorContainer)) {
|
||||||
|
savedSelectionRange.value = range.cloneRange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreSelection = () => {
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (!selection || !editorRef.value) return
|
||||||
|
selection.removeAllRanges()
|
||||||
|
if (savedSelectionRange.value) {
|
||||||
|
selection.addRange(savedSelectionRange.value)
|
||||||
|
} else {
|
||||||
|
const range = document.createRange()
|
||||||
|
range.selectNodeContents(editorRef.value)
|
||||||
|
range.collapse(false)
|
||||||
|
selection.addRange(range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInput = () => {
|
||||||
|
emit('text-change', getPlainText())
|
||||||
|
saveSelection()
|
||||||
|
syncMentionPanelVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertPlainTextAtCursor = (text) => {
|
||||||
|
if (!editorRef.value) return
|
||||||
|
const normalizedText = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
||||||
|
editorRef.value.focus()
|
||||||
|
restoreSelection()
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (!selection || selection.rangeCount === 0) return
|
||||||
|
let range = selection.getRangeAt(0)
|
||||||
|
if (!editorRef.value.contains(range.commonAncestorContainer)) {
|
||||||
|
focusEditorToEnd()
|
||||||
|
const currentSelection = window.getSelection()
|
||||||
|
if (!currentSelection || currentSelection.rangeCount === 0) return
|
||||||
|
range = currentSelection.getRangeAt(0)
|
||||||
|
}
|
||||||
|
range.deleteContents()
|
||||||
|
const lines = normalizedText.split('\n')
|
||||||
|
const fragment = document.createDocumentFragment()
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
if (line) fragment.appendChild(document.createTextNode(line))
|
||||||
|
if (index < lines.length - 1) fragment.appendChild(document.createElement('br'))
|
||||||
|
})
|
||||||
|
range.insertNode(fragment)
|
||||||
|
range.collapse(false)
|
||||||
|
selection.removeAllRanges()
|
||||||
|
selection.addRange(range)
|
||||||
|
saveSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePaste = (event) => {
|
||||||
|
if (!editorRef.value) return
|
||||||
|
event.preventDefault()
|
||||||
|
const clipboardText = event.clipboardData?.getData('text/plain') || ''
|
||||||
|
if (!clipboardText) return
|
||||||
|
insertPlainTextAtCursor(clipboardText)
|
||||||
|
emit('text-change', getPlainText())
|
||||||
|
syncMentionPanelVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateMentionImageList = () => {
|
||||||
|
mentionImageList.value = mentionImageList.value.slice(0, MAX_IMAGE_COUNT)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideMentionPanel = () => {
|
||||||
|
mentionVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasActiveMentionTriggerBeforeCursor = () => {
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (!selection || selection.rangeCount === 0) return false
|
||||||
|
const range = selection.getRangeAt(0)
|
||||||
|
if (!range.collapsed) return false
|
||||||
|
if (!editorRef.value?.contains(range.commonAncestorContainer)) return false
|
||||||
|
const container = range.startContainer
|
||||||
|
if (container.nodeType !== Node.TEXT_NODE) return false
|
||||||
|
const before = container.data.slice(0, range.startOffset)
|
||||||
|
const atIndex = before.lastIndexOf('@')
|
||||||
|
if (atIndex < 0) return false
|
||||||
|
const afterAt = before.slice(atIndex + 1)
|
||||||
|
return !/\s/.test(afterAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncMentionPanelVisibility = () => {
|
||||||
|
const shouldShow = hasActiveMentionTriggerBeforeCursor()
|
||||||
|
if (!shouldShow) {
|
||||||
|
hideMentionPanel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateMentionImageList()
|
||||||
|
mentionVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeMentionKeywordBeforeCursor = () => {
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (!selection || selection.rangeCount === 0) return
|
||||||
|
const range = selection.getRangeAt(0)
|
||||||
|
if (!range.collapsed) return
|
||||||
|
const container = range.startContainer
|
||||||
|
if (container.nodeType !== Node.TEXT_NODE) return
|
||||||
|
const before = container.data.slice(0, range.startOffset)
|
||||||
|
const atIndex = before.lastIndexOf('@')
|
||||||
|
if (atIndex < 0) return
|
||||||
|
const afterAt = before.slice(atIndex + 1)
|
||||||
|
if (/\s/.test(afterAt)) return
|
||||||
|
const removeRange = document.createRange()
|
||||||
|
removeRange.setStart(container, atIndex)
|
||||||
|
removeRange.setEnd(container, range.startOffset)
|
||||||
|
removeRange.deleteContents()
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertReference = (item) => {
|
||||||
|
if (!item?.url || !editorRef.value) return
|
||||||
|
editorRef.value.focus()
|
||||||
|
restoreSelection()
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (!selection || selection.rangeCount === 0) return
|
||||||
|
const range = selection.getRangeAt(0)
|
||||||
|
if (!editorRef.value.contains(range.commonAncestorContainer)) return
|
||||||
|
|
||||||
|
const refNoText = `[图${item.refNo}]`
|
||||||
|
let node
|
||||||
|
if (item.mediaType === 'image') {
|
||||||
|
const img = document.createElement('img')
|
||||||
|
img.src = item.url
|
||||||
|
img.alt = `reference-${item.refNo}`
|
||||||
|
img.className = 'inline-rich-image'
|
||||||
|
img.setAttribute('data-image-url', item.url)
|
||||||
|
img.setAttribute('data-mention-reference', '1')
|
||||||
|
img.setAttribute('data-reference-no', String(item.refNo))
|
||||||
|
node = img
|
||||||
|
} else {
|
||||||
|
const tag = document.createElement('span')
|
||||||
|
tag.className = 'inline-rich-reference'
|
||||||
|
tag.textContent = refNoText
|
||||||
|
tag.setAttribute('data-reference-url', item.url)
|
||||||
|
tag.setAttribute('data-mention-reference', '1')
|
||||||
|
tag.setAttribute('data-reference-no', String(item.refNo))
|
||||||
|
tag.setAttribute('data-reference-type', item.mediaType)
|
||||||
|
node = tag
|
||||||
|
}
|
||||||
|
|
||||||
|
range.insertNode(node)
|
||||||
|
const space = document.createTextNode(' ')
|
||||||
|
range.setStartAfter(node)
|
||||||
|
range.insertNode(space)
|
||||||
|
range.setStartAfter(space)
|
||||||
|
range.collapse(true)
|
||||||
|
selection.removeAllRanges()
|
||||||
|
selection.addRange(range)
|
||||||
|
saveSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertMentionImage = (item) => {
|
||||||
|
if (!item?.url || !editorRef.value) return
|
||||||
|
editorRef.value.focus()
|
||||||
|
restoreSelection()
|
||||||
|
removeMentionKeywordBeforeCursor()
|
||||||
|
// 删除 @关键字 后必须重新记录选区,否则 insertReference 里 restoreSelection 仍用删除前的 Range,会插错位置甚至覆盖后文
|
||||||
|
saveSelection()
|
||||||
|
insertReference(item)
|
||||||
|
hideMentionPanel()
|
||||||
|
emit('text-change', getPlainText())
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyup = (event) => {
|
||||||
|
saveSelection()
|
||||||
|
if (event.key === '@') {
|
||||||
|
syncMentionPanelVisibility()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
hideMentionPanel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
syncMentionPanelVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEditorClick = () => {
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (!selection || selection.rangeCount === 0) {
|
||||||
|
focusEditorToEnd()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const range = selection.getRangeAt(0)
|
||||||
|
if (!editorRef.value?.contains(range.commonAncestorContainer)) {
|
||||||
|
focusEditorToEnd()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
saveSelection()
|
||||||
|
syncMentionPanelVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInsertedUniqueImageCount = () => mentionImageList.value.length
|
||||||
|
|
||||||
|
const openImagePicker = () => {
|
||||||
|
const current = getInsertedUniqueImageCount()
|
||||||
|
if (current >= MAX_IMAGE_COUNT) {
|
||||||
|
Message.warning(`最多插入 ${MAX_IMAGE_COUNT} 个参考素材`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fileInputRef.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadToCos = async (file) => {
|
||||||
|
const res = await uploadFile({
|
||||||
|
url: PORTAL_TENCENT_COS_UPLOAD_URL,
|
||||||
|
file,
|
||||||
|
name: 'file'
|
||||||
|
})
|
||||||
|
const codeOk = res && (Number(res.code) === 200 || res.code === 200)
|
||||||
|
const url = extractUploadUrlFromResponse(res)
|
||||||
|
if (codeOk && url) return url
|
||||||
|
throw new Error(res?.msg || '上传失败:未返回文件地址')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectFiles = async (event) => {
|
||||||
|
const input = event.target
|
||||||
|
const files = Array.from(input.files || [])
|
||||||
|
if (!files.length) return
|
||||||
|
const remain = MAX_IMAGE_COUNT - getInsertedUniqueImageCount()
|
||||||
|
if (remain <= 0) {
|
||||||
|
Message.warning(`最多插入 ${MAX_IMAGE_COUNT} 个参考素材`)
|
||||||
|
input.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const selected = files.slice(0, remain)
|
||||||
|
if (files.length > remain) {
|
||||||
|
Message.warning(`最多插入 ${MAX_IMAGE_COUNT} 个参考素材,本次仅插入 ${remain} 个`)
|
||||||
|
}
|
||||||
|
for (const file of selected) {
|
||||||
|
const mediaType = detectMediaType(file)
|
||||||
|
try {
|
||||||
|
const url = await uploadToCos(file)
|
||||||
|
if (url && !mentionImageList.value.some((item) => item.url === url)) {
|
||||||
|
const refNo = mentionImageList.value.length + 1
|
||||||
|
mentionImageList.value.push({ url, refNo, mediaType })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Message.error('参考素材上传失败')
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input.value = ''
|
||||||
|
emit('text-change', getPlainText())
|
||||||
|
}
|
||||||
|
|
||||||
|
const getContentItems = () => {
|
||||||
|
const editor = editorRef.value
|
||||||
|
const items = []
|
||||||
|
if (!editor) return items
|
||||||
|
let textBuffer = ''
|
||||||
|
const flushText = () => {
|
||||||
|
const normalized = textBuffer.replace(/\u00a0/g, ' ').replace(/\s+\n/g, '\n')
|
||||||
|
if (normalized.trim()) items.push({ type: 'text', text: normalized.trim() })
|
||||||
|
textBuffer = ''
|
||||||
|
}
|
||||||
|
const walk = (node) => {
|
||||||
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
|
textBuffer += node.textContent || ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (node.nodeType !== Node.ELEMENT_NODE) return
|
||||||
|
const el = node
|
||||||
|
const referenceUrl = (
|
||||||
|
el.dataset?.referenceUrl ||
|
||||||
|
el.dataset?.imageUrl ||
|
||||||
|
el.getAttribute('src') ||
|
||||||
|
''
|
||||||
|
).trim()
|
||||||
|
if (el.dataset?.mentionReference === '1' && referenceUrl) {
|
||||||
|
flushText()
|
||||||
|
items.push({
|
||||||
|
type: 'image_url',
|
||||||
|
image_url: { url: referenceUrl },
|
||||||
|
role: 'reference_image'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (el.tagName === 'IMG' && (el.dataset?.imageUrl || el.getAttribute('src'))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (el.tagName === 'BR') {
|
||||||
|
textBuffer += '\n'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (['DIV', 'P', 'LI'].includes(el.tagName) && textBuffer && !textBuffer.endsWith('\n')) textBuffer += '\n'
|
||||||
|
Array.from(el.childNodes).forEach(walk)
|
||||||
|
if (['DIV', 'P', 'LI'].includes(el.tagName) && textBuffer && !textBuffer.endsWith('\n')) textBuffer += '\n'
|
||||||
|
}
|
||||||
|
Array.from(editor.childNodes).forEach(walk)
|
||||||
|
flushText()
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
if (!editorRef.value) return
|
||||||
|
editorRef.value.innerHTML = ''
|
||||||
|
emit('text-change', '')
|
||||||
|
savedSelectionRange.value = null
|
||||||
|
mentionImageList.value = []
|
||||||
|
hideMentionPanel()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 供父组件持久化:参考图模式的 HTML + 素材列表 */
|
||||||
|
const getDraftState = () => ({
|
||||||
|
html: editorRef.value ? editorRef.value.innerHTML : '',
|
||||||
|
mentionList: mentionImageList.value.map(({ url, refNo, mediaType }) => ({
|
||||||
|
url,
|
||||||
|
refNo,
|
||||||
|
mediaType
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
const applyDraftState = (draft) => {
|
||||||
|
hideMentionPanel()
|
||||||
|
savedSelectionRange.value = null
|
||||||
|
if (!draft || typeof draft !== 'object') {
|
||||||
|
clear()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mentionImageList.value = Array.isArray(draft.mentionList)
|
||||||
|
? draft.mentionList.map((x) => ({
|
||||||
|
url: x.url,
|
||||||
|
refNo: Number(x.refNo) || 0,
|
||||||
|
mediaType: x.mediaType || 'image'
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
if (editorRef.value) {
|
||||||
|
editorRef.value.innerHTML = typeof draft.html === 'string' ? draft.html : ''
|
||||||
|
}
|
||||||
|
nextTick(() => {
|
||||||
|
emit('text-change', getPlainText())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
getContentItems,
|
||||||
|
getPlainText,
|
||||||
|
clear,
|
||||||
|
getDraftState,
|
||||||
|
applyDraftState
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
focusEditorToEnd()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.video-editor-root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.hidden-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.editor-wrapper {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
.tool-btn {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
color: rgba(255, 255, 255, 0.88);
|
||||||
|
padding: 6px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: border-color 0.2s, background 0.2s;
|
||||||
|
}
|
||||||
|
.tool-btn:hover {
|
||||||
|
border-color: rgba(0, 202, 224, 0.4);
|
||||||
|
background: rgba(0, 202, 224, 0.08);
|
||||||
|
}
|
||||||
|
.video-rich-editor {
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 220px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.9;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-y: auto;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
caret-color: #4d89ff;
|
||||||
|
|
||||||
|
:deep(*) {
|
||||||
|
color: rgba(255, 255, 255, 0.9) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.inline-rich-reference) {
|
||||||
|
color: #5eebf5 !important;
|
||||||
|
background: rgba(0, 202, 224, 0.15);
|
||||||
|
}
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
.video-rich-editor:focus {
|
||||||
|
border-color: rgba(0, 202, 224, 0.45);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 202, 224, 0.15);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.video-rich-editor:empty::before {
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
color: rgba(255, 255, 255, 0.35);
|
||||||
|
pointer-events: none;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.video-rich-editor :deep(.inline-rich-image) {
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
max-width: min(260px, 100%);
|
||||||
|
max-height: 148px;
|
||||||
|
object-fit: contain;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin: 6px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
.video-rich-editor :deep(.inline-rich-reference) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 6px;
|
||||||
|
margin: 0 3px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(0, 202, 224, 0.15);
|
||||||
|
color: #5eebf5;
|
||||||
|
font-size: 0.85em;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.mention-panel {
|
||||||
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
|
right: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
max-height: 180px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(22, 24, 30, 0.98);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
.mention-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.mention-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
.mention-thumb {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
.mention-type-badge {
|
||||||
|
min-width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
.mention-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
.mention-empty {
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -8,5 +8,7 @@ export default {
|
||||||
fastVideo: 'إنشاء فيديو',
|
fastVideo: 'إنشاء فيديو',
|
||||||
recharge: 'شحن سريع',
|
recharge: 'شحن سريع',
|
||||||
help: 'مركز المساعدة',
|
help: 'مركز المساعدة',
|
||||||
moneyInvite: 'دعوة مكافآت'
|
moneyInvite: 'دعوة مكافآت',
|
||||||
|
assetGroupManage: 'إدارة مجموعات الأصول',
|
||||||
|
assetManage: 'إدارة الأصول'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,14 @@ export default {
|
||||||
cardNoRequired: 'Please enter card number',
|
cardNoRequired: 'Please enter card number',
|
||||||
cardNameRequired: 'Please enter cardholder name',
|
cardNameRequired: 'Please enter cardholder name',
|
||||||
rechargeFailed: 'Recharge failed',
|
rechargeFailed: 'Recharge failed',
|
||||||
|
// Model selection
|
||||||
|
selectModel: 'Select Model',
|
||||||
|
seedance20: 'Seedance 2.0',
|
||||||
|
seedance20Fast: 'Seedance 2.0 Fast',
|
||||||
|
// Rich text editor
|
||||||
|
insertImage: 'Insert Image',
|
||||||
|
mentionImage: 'Mention Image',
|
||||||
|
noImageToMention: 'No images to mention',
|
||||||
// VM支付相关
|
// VM支付相关
|
||||||
vmCardInfo: 'Credit Card Information',
|
vmCardInfo: 'Credit Card Information',
|
||||||
cardNumber: 'Card Number',
|
cardNumber: 'Card Number',
|
||||||
|
|
@ -175,5 +183,9 @@ export default {
|
||||||
country: 'Country',
|
country: 'Country',
|
||||||
countryPlaceholder: 'Select country',
|
countryPlaceholder: 'Select country',
|
||||||
countryRequired: 'Please select country',
|
countryRequired: 'Please select country',
|
||||||
submit: 'Submit'
|
videoGen: 'Video Generation',
|
||||||
|
uploadFirstImage: 'Upload Reference Image',
|
||||||
|
insertImage: 'Insert Image',
|
||||||
|
submit: 'Submit',
|
||||||
|
cancel: 'Cancel'
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
export default {
|
export default {
|
||||||
index: 'Home',
|
index: 'Home',
|
||||||
imageToImage: 'One-click undressing',
|
imageToImage: 'Image-to-Image 1',
|
||||||
imageToImage2: 'Image-to-Image2',
|
imageToImage2: 'Image-to-Image2',
|
||||||
changeFace: 'Swap Face',
|
changeFace: 'Swap Face',
|
||||||
changeFaceVideo: 'Swap Video Face',
|
changeFaceVideo: 'Swap Video Face',
|
||||||
fastImage: 'Gen Image',
|
fastImage: 'Gen Image',
|
||||||
fastVideo: 'Gen Video',
|
fastVideo: 'Gen Video',
|
||||||
|
videoGen: 'Video Generation',
|
||||||
recharge: 'Quick Recharge',
|
recharge: 'Quick Recharge',
|
||||||
help: 'Help Center',
|
help: 'Help Center',
|
||||||
moneyInvite: 'Reward Invitation'
|
moneyInvite: 'Reward Invitation',
|
||||||
|
assetGroupManage: 'Asset Group Manage',
|
||||||
|
assetManage: 'Asset Manage'
|
||||||
}
|
}
|
||||||
|
|
@ -8,5 +8,7 @@ export default {
|
||||||
fastVideo: 'Generar video',
|
fastVideo: 'Generar video',
|
||||||
recharge: 'Recarga rápida',
|
recharge: 'Recarga rápida',
|
||||||
help: 'Centro de ayuda',
|
help: 'Centro de ayuda',
|
||||||
moneyInvite: 'Invitación con recompensa'
|
moneyInvite: 'Invitación con recompensa',
|
||||||
|
assetGroupManage: 'Gestión de grupos de activos',
|
||||||
|
assetManage: 'Gestión de activos'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,7 @@ export default {
|
||||||
fastVideo: 'Générer une vidéo',
|
fastVideo: 'Générer une vidéo',
|
||||||
recharge: 'Recharge rapide',
|
recharge: 'Recharge rapide',
|
||||||
help: 'Centre d\'aide',
|
help: 'Centre d\'aide',
|
||||||
moneyInvite: 'Invitation avec récompense'
|
moneyInvite: 'Invitation avec récompense',
|
||||||
|
assetGroupManage: 'Gestion des groupes d\'actifs',
|
||||||
|
assetManage: 'Gestion des actifs'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,7 @@ export default {
|
||||||
fastVideo: 'वीडियो जनरेट करें',
|
fastVideo: 'वीडियो जनरेट करें',
|
||||||
recharge: 'त्वरित रिचार्ज',
|
recharge: 'त्वरित रिचार्ज',
|
||||||
help: 'सहायता केंद्र',
|
help: 'सहायता केंद्र',
|
||||||
moneyInvite: 'इनाम निमंत्रण'
|
moneyInvite: 'इनाम निमंत्रण',
|
||||||
|
assetGroupManage: 'एसेट समूह प्रबंधन',
|
||||||
|
assetManage: 'एसेट प्रबंधन'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,28 @@
|
||||||
import { createI18n } from 'vue-i18n'
|
import { createI18n } from 'vue-i18n'
|
||||||
import Cookies from 'js-cookie'
|
import Cookies from 'js-cookie'
|
||||||
import zh_HK from '@/lang/zh_HK/index.js'
|
import zh_HK from '@/lang/zh_HK/index.js'
|
||||||
|
/**
|
||||||
import en_US from '@/lang/en_US/index.js'
|
import en_US from '@/lang/en_US/index.js'
|
||||||
import es_ES from '@/lang/es_ES/index.js'
|
mport es_ES from '@/lang/es_ES/index.js'
|
||||||
import pt_BR from '@/lang/pt_BR/index.js'
|
import pt_BR from '@/lang/pt_BR/index.js'
|
||||||
import hi_IN from '@/lang/hi_IN/index.js'
|
import hi_IN from '@/lang/hi_IN/index.js'
|
||||||
import ru_RU from '@/lang/ru_RU/index.js'
|
import ru_RU from '@/lang/ru_RU/index.js'
|
||||||
import ar_SA from '@/lang/ar_SA/index.js'
|
import ar_SA from '@/lang/ar_SA/index.js'
|
||||||
import fr_FR from '@/lang/fr_FR/index.js'
|
import fr_FR from '@/lang/fr_FR/index.js'**/
|
||||||
|
|
||||||
let locale = Cookies.get('language') || 'en_US'
|
// 多语言切换已禁用:全站固定繁体中文
|
||||||
|
let locale = 'zh_HK'
|
||||||
|
|
||||||
/** 各语言在界面上的显示名称 */
|
/** 各语言在界面上的显示名称 */
|
||||||
export const LOCALE_NAMES = {
|
export const LOCALE_NAMES = {
|
||||||
zh_HK: '繁体中文',
|
zh_HK: '繁体中文',
|
||||||
en_US: 'English',
|
/** en_US: 'English',
|
||||||
es_ES: 'Español',
|
es_ES: 'Español',
|
||||||
pt_BR: 'Português',
|
pt_BR: 'Português',
|
||||||
hi_IN: 'हिन्दी',
|
hi_IN: 'हिन्दी',
|
||||||
ru_RU: 'Русский',
|
ru_RU: 'Русский',
|
||||||
ar_SA: 'العربية',
|
ar_SA: 'العربية',
|
||||||
fr_FR: 'Français'
|
fr_FR: 'Français'**/
|
||||||
}
|
}
|
||||||
|
|
||||||
const i18n = createI18n({
|
const i18n = createI18n({
|
||||||
|
|
@ -28,13 +30,13 @@ const i18n = createI18n({
|
||||||
locale,
|
locale,
|
||||||
messages: {
|
messages: {
|
||||||
zh_HK,
|
zh_HK,
|
||||||
en_US,
|
/** en_US,
|
||||||
es_ES,
|
es_ES,
|
||||||
pt_BR,
|
pt_BR,
|
||||||
hi_IN,
|
hi_IN,
|
||||||
ru_RU,
|
ru_RU,
|
||||||
ar_SA,
|
ar_SA,
|
||||||
fr_FR
|
fr_FR**/
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import en_USLocale from './en/index'
|
//import en_USLocale from './en/index'
|
||||||
import zh_HKLocale from './zh_HK/index'
|
import zh_HKLocale from './zh_HK/index'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
en_US: en_USLocale,
|
//en_US: en_USLocale,
|
||||||
zh_HK: zh_HKLocale
|
zh_HK: zh_HKLocale
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,7 @@ export default {
|
||||||
fastVideo: 'Gerar vídeo',
|
fastVideo: 'Gerar vídeo',
|
||||||
recharge: 'Recarga rápida',
|
recharge: 'Recarga rápida',
|
||||||
help: 'Central de ajuda',
|
help: 'Central de ajuda',
|
||||||
moneyInvite: 'Convite com recompensa'
|
moneyInvite: 'Convite com recompensa',
|
||||||
|
assetGroupManage: 'Gerenciamento de grupos de ativos',
|
||||||
|
assetManage: 'Gerenciamento de ativos'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,7 @@ export default {
|
||||||
fastVideo: 'Создать видео',
|
fastVideo: 'Создать видео',
|
||||||
recharge: 'Быстрая пополнение',
|
recharge: 'Быстрая пополнение',
|
||||||
help: 'Центр помощи',
|
help: 'Центр помощи',
|
||||||
moneyInvite: 'Приглашение с наградой'
|
moneyInvite: 'Приглашение с наградой',
|
||||||
|
assetGroupManage: 'Управление группами ресурсов',
|
||||||
|
assetManage: 'Управление ресурсами'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,14 @@ export default {
|
||||||
cardNoRequired: '請輸入銀行卡號',
|
cardNoRequired: '請輸入銀行卡號',
|
||||||
cardNameRequired: '請輸入銀行卡姓名',
|
cardNameRequired: '請輸入銀行卡姓名',
|
||||||
rechargeFailed: '充值失敗',
|
rechargeFailed: '充值失敗',
|
||||||
|
// 模型选择
|
||||||
|
selectModel: '選擇模型',
|
||||||
|
seedance20: 'Seedance 2.0',
|
||||||
|
seedance20Fast: 'Seedance 2.0 Fast',
|
||||||
|
// 富文本编辑器
|
||||||
|
insertImage: '插入圖片',
|
||||||
|
mentionImage: '引用圖片',
|
||||||
|
noImageToMention: '暫無可引用的圖片',
|
||||||
// VM支付相關
|
// VM支付相關
|
||||||
vmCardInfo: '信用卡信息',
|
vmCardInfo: '信用卡信息',
|
||||||
cardNumber: '信用卡卡號',
|
cardNumber: '信用卡卡號',
|
||||||
|
|
@ -178,6 +186,9 @@ export default {
|
||||||
country: '國家',
|
country: '國家',
|
||||||
countryPlaceholder: '請選擇國家',
|
countryPlaceholder: '請選擇國家',
|
||||||
countryRequired: '請選擇國家',
|
countryRequired: '請選擇國家',
|
||||||
|
videoGen: '視頻生成',
|
||||||
|
uploadFirstImage: '上傳首圖',
|
||||||
|
insertImage: '插入圖片',
|
||||||
submit: '提交',
|
submit: '提交',
|
||||||
cancel: '取消'
|
cancel: '取消'
|
||||||
}
|
}
|
||||||
|
|
@ -6,7 +6,11 @@ export default {
|
||||||
changeFaceVideo: '視頻換臉',
|
changeFaceVideo: '視頻換臉',
|
||||||
fastImage: '快捷生圖',
|
fastImage: '快捷生圖',
|
||||||
fastVideo: '快捷生視頻',
|
fastVideo: '快捷生視頻',
|
||||||
|
videoGen: '視頻生成',
|
||||||
recharge: '快速充值',
|
recharge: '快速充值',
|
||||||
help: '幫助中心',
|
help: '幫助中心',
|
||||||
moneyInvite: '有獎邀請'
|
moneyInvite: '有獎邀請',
|
||||||
|
assetGroupManage: '資源組管理',
|
||||||
|
assetManage: '素材管理',
|
||||||
|
generatedAssets: '作品库'
|
||||||
}
|
}
|
||||||
|
|
@ -188,16 +188,19 @@ export default {
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
.change-password-dialog {
|
.change-password-dialog {
|
||||||
border-radius: 20px;
|
border-radius: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
0deg,
|
160deg,
|
||||||
rgba(39, 20, 51, 0.7) 0%,
|
rgba(18, 24, 38, 0.98) 0%,
|
||||||
rgba(230, 33, 122, 0.7) 49%
|
rgba(26, 31, 46, 0.98) 50%,
|
||||||
|
rgba(37, 43, 58, 0.96) 100%
|
||||||
);
|
);
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
border-radius: 20px;
|
border: none;
|
||||||
border: 2px solid #e6217a;
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(77, 137, 255, 0.1),
|
||||||
|
0 4px 20px rgba(0, 0, 0, 0.35);
|
||||||
width: 500px;
|
width: 500px;
|
||||||
top: 45% !important;
|
top: 45% !important;
|
||||||
transform: translateY(-45%) !important;
|
transform: translateY(-45%) !important;
|
||||||
|
|
|
||||||
|
|
@ -235,16 +235,19 @@ export default {
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
.forgot-dialog {
|
.forgot-dialog {
|
||||||
border-radius: 20px;
|
border-radius: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
0deg,
|
160deg,
|
||||||
rgba(39, 20, 51, 0.7) 0%,
|
rgba(18, 24, 38, 0.98) 0%,
|
||||||
rgba(230, 33, 122, 0.7) 49%
|
rgba(26, 31, 46, 0.98) 50%,
|
||||||
|
rgba(37, 43, 58, 0.96) 100%
|
||||||
);
|
);
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
border-radius: 20px;
|
border: none;
|
||||||
border: 2px solid #e6217a;
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(77, 137, 255, 0.1),
|
||||||
|
0 4px 20px rgba(0, 0, 0, 0.35);
|
||||||
width: 500px;
|
width: 500px;
|
||||||
top: 45% !important;
|
top: 45% !important;
|
||||||
transform: translateY(-45%) !important;
|
transform: translateY(-45%) !important;
|
||||||
|
|
|
||||||
|
|
@ -44,20 +44,7 @@
|
||||||
inputType="password"
|
inputType="password"
|
||||||
:placeholder="`${$t('common.passwordPlaceholder')}`" />
|
:placeholder="`${$t('common.passwordPlaceholder')}`" />
|
||||||
|
|
||||||
<div class="login-link">
|
|
||||||
<mf-button
|
|
||||||
class="grey"
|
|
||||||
type="text"
|
|
||||||
@click="showForgot">
|
|
||||||
{{ $t('common.forgotPassword') }}?
|
|
||||||
</mf-button>
|
|
||||||
</div>
|
|
||||||
<div class="login-submit">
|
<div class="login-submit">
|
||||||
<mf-button
|
|
||||||
size="large"
|
|
||||||
@click="showRegister">
|
|
||||||
{{ $t('common.register') }}
|
|
||||||
</mf-button>
|
|
||||||
<mf-button
|
<mf-button
|
||||||
size="large"
|
size="large"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|
@ -67,25 +54,10 @@
|
||||||
</mf-button>
|
</mf-button>
|
||||||
</div>
|
</div>
|
||||||
</mf-dialog>
|
</mf-dialog>
|
||||||
<Forgot
|
|
||||||
v-if="forgotVisible"
|
|
||||||
:visible="forgotVisible"
|
|
||||||
@open="forgotVisible = true"
|
|
||||||
@back="back"
|
|
||||||
@cancel="forgotVisible = false" />
|
|
||||||
|
|
||||||
<Register
|
|
||||||
v-if="registerVisible"
|
|
||||||
:visible="registerVisible"
|
|
||||||
@cancel="registerVisible = false"
|
|
||||||
@back="backFormRegister"
|
|
||||||
@open="registerVisible = true" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Forgot from './Forgot.vue'
|
|
||||||
import Register from './Register.vue'
|
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import i18n from '@/lang/i18n'
|
import i18n from '@/lang/i18n'
|
||||||
|
|
||||||
|
|
@ -93,22 +65,16 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
email: '',
|
email: '',
|
||||||
forgotVisible: false,
|
|
||||||
emailVisible: false,
|
emailVisible: false,
|
||||||
registerVisible: false,
|
|
||||||
loading: false,
|
loading: false,
|
||||||
username: '',
|
username: '',
|
||||||
password: ''
|
password: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
visible: Boolean,
|
visible: Boolean
|
||||||
register: Boolean
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
Forgot,
|
|
||||||
Register
|
|
||||||
},
|
},
|
||||||
|
components: {},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['lang'])
|
...mapGetters(['lang'])
|
||||||
},
|
},
|
||||||
|
|
@ -121,10 +87,6 @@ export default {
|
||||||
// }
|
// }
|
||||||
this.username = ""
|
this.username = ""
|
||||||
this.password = ""
|
this.password = ""
|
||||||
let { inviteCode } = this.$route.query || {}
|
|
||||||
if (inviteCode) {
|
|
||||||
this.showRegister()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
cancel() {
|
cancel() {
|
||||||
|
|
@ -132,24 +94,12 @@ export default {
|
||||||
this.password = ''
|
this.password = ''
|
||||||
this.$emit('cancel')
|
this.$emit('cancel')
|
||||||
},
|
},
|
||||||
showRegister() {
|
|
||||||
this.registerVisible = true
|
|
||||||
this.$emit('cancel')
|
|
||||||
},
|
|
||||||
changeLang(value) {
|
changeLang(value) {
|
||||||
if (value != this.lang) {
|
if (value != this.lang) {
|
||||||
this.$store.dispatch('main/setLanguage', value)
|
this.$store.dispatch('main/setLanguage', value)
|
||||||
i18n.global.locale = value
|
i18n.global.locale = value
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
back() {
|
|
||||||
this.forgotVisible = false
|
|
||||||
this.$emit('open')
|
|
||||||
},
|
|
||||||
backFormRegister() {
|
|
||||||
this.registerVisible = false
|
|
||||||
this.$emit('open')
|
|
||||||
},
|
|
||||||
login() {
|
login() {
|
||||||
if (!this.username) {
|
if (!this.username) {
|
||||||
this.$message.error(this.$t('common.userEmailPlaceholder'))
|
this.$message.error(this.$t('common.userEmailPlaceholder'))
|
||||||
|
|
@ -181,6 +131,7 @@ export default {
|
||||||
})
|
})
|
||||||
this.username = ""
|
this.username = ""
|
||||||
this.password = ""
|
this.password = ""
|
||||||
|
this.$router.replace({ name: 'video-gen' })
|
||||||
this.$emit('cancel')
|
this.$emit('cancel')
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -190,10 +141,6 @@ export default {
|
||||||
.catch((_) => {
|
.catch((_) => {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
})
|
})
|
||||||
},
|
|
||||||
showForgot() {
|
|
||||||
this.forgotVisible = true
|
|
||||||
this.$emit('cancel')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -201,15 +148,18 @@ export default {
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
.login-dialog {
|
.login-dialog {
|
||||||
border-radius: 20px;
|
border-radius: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
0deg,
|
160deg,
|
||||||
rgba(39, 20, 51, 0.7) 0%,
|
rgba(18, 24, 38, 0.98) 0%,
|
||||||
rgba(230, 33, 122, 0.7) 49%
|
rgba(26, 31, 46, 0.98) 50%,
|
||||||
|
rgba(37, 43, 58, 0.96) 100%
|
||||||
);
|
);
|
||||||
border-radius: 20px;
|
border: none;
|
||||||
border: 2px solid #e6217a;
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(77, 137, 255, 0.1),
|
||||||
|
0 4px 20px rgba(0, 0, 0, 0.35);
|
||||||
width: 500px;
|
width: 500px;
|
||||||
height: 320px;
|
height: 320px;
|
||||||
top: 45% !important;
|
top: 45% !important;
|
||||||
|
|
@ -323,36 +273,8 @@ export default {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
.mf-button {
|
.mf-button {
|
||||||
width: 160px;
|
width: 220px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin: 0 14px;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
color: #ffffff;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
&:hover {
|
|
||||||
background-color: #262626;
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
background-color: #0d0d0d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-link {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
color: #999;
|
|
||||||
margin-top: 10px;
|
|
||||||
.mf-button {
|
|
||||||
padding: 0;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 12px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,15 +22,11 @@
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<a-menu-item :key="item.key">
|
<a-menu-item :key="item.key">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<a-image
|
<span
|
||||||
:width="24"
|
class="menu-svg-icon"
|
||||||
:preview="false"
|
:class="{ active: selectedKeys.indexOf(item.key) > -1 }"
|
||||||
:height="24"
|
v-html="getMenuSvg(item.key)"
|
||||||
:src="`/images/nav/${
|
></span>
|
||||||
selectedKeys.indexOf(item.key) > -1
|
|
||||||
? item.icon + '-a'
|
|
||||||
: item.icon
|
|
||||||
}.png`" />
|
|
||||||
</template>
|
</template>
|
||||||
<div class="menu-item">
|
<div class="menu-item">
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
|
|
@ -50,6 +46,9 @@ import cloneDeep from 'lodash-es/cloneDeep'
|
||||||
import { constantRoutes } from '@/router/index'
|
import { constantRoutes } from '@/router/index'
|
||||||
import { generateTitle, generateLang } from '@/utils/i18n'
|
import { generateTitle, generateLang } from '@/utils/i18n'
|
||||||
|
|
||||||
|
/** 左侧导航仅显示这些路由(name 与 router/index.js 一致) */
|
||||||
|
const SIDEBAR_ONLY_ROUTE_NAMES = ['video-gen', 'asset-group-manage', 'asset-manage', 'generatedAssets']
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
collapsed: Boolean
|
collapsed: Boolean
|
||||||
})
|
})
|
||||||
|
|
@ -62,9 +61,14 @@ const $base = inject('$base')
|
||||||
const $message = inject('$message')
|
const $message = inject('$message')
|
||||||
|
|
||||||
const menuItems = computed(() => {
|
const menuItems = computed(() => {
|
||||||
return constantRoutes
|
const root = constantRoutes.find((d) => d.path === '/')
|
||||||
.find((d) => d.path == '/')
|
const children = root?.children ?? []
|
||||||
.children.map((route) => ({
|
return children
|
||||||
|
.filter(
|
||||||
|
(r) =>
|
||||||
|
r.meta?.menuItem !== false && SIDEBAR_ONLY_ROUTE_NAMES.includes(r.name)
|
||||||
|
)
|
||||||
|
.map((route) => ({
|
||||||
key: route.name,
|
key: route.name,
|
||||||
label: generateTitle(route.meta?.title),
|
label: generateTitle(route.meta?.title),
|
||||||
meta: route.meta,
|
meta: route.meta,
|
||||||
|
|
@ -140,6 +144,19 @@ const count = (system) => {
|
||||||
? servicePolicyCount.value
|
? servicePolicyCount.value
|
||||||
: agencyPolicyCount.value
|
: agencyPolicyCount.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getMenuSvg = (key) => {
|
||||||
|
if (key === 'video-gen') {
|
||||||
|
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3a9 9 0 1 0 0 18a9 9 0 0 0 0-18Zm-1.5 5.2a.9.9 0 0 1 1.35-.78l4.7 2.7a.9.9 0 0 1 0 1.56l-4.7 2.7a.9.9 0 0 1-1.35-.78V8.2Z"/></svg>'
|
||||||
|
}
|
||||||
|
if (key === 'asset-group-manage') {
|
||||||
|
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 6.5A2.5 2.5 0 0 1 5.5 4H9l1.7 2H18.5A2.5 2.5 0 0 1 21 8.5v8A2.5 2.5 0 0 1 18.5 19h-13A2.5 2.5 0 0 1 3 16.5v-10Z"/></svg>'
|
||||||
|
}
|
||||||
|
if (key === 'asset-manage') {
|
||||||
|
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4h14a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Zm1.8 11.5h10.4L14 11.4l-2.6 3.1-1.7-2.1-2.9 3.1Zm2.7-6.1a1.6 1.6 0 1 0 0-3.2a1.6 1.6 0 0 0 0 3.2Z"/></svg>'
|
||||||
|
}
|
||||||
|
return '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="8"/></svg>'
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
.arco-tooltip-content {
|
.arco-tooltip-content {
|
||||||
|
|
@ -166,4 +183,25 @@ const count = (system) => {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-svg-icon {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: rgba(255, 255, 255, 0.74);
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-svg-icon:deep(svg) {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
display: block;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-svg-icon.active {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ export default {
|
||||||
&-save {
|
&-save {
|
||||||
width: 410px;
|
width: 410px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
background: #e6217a;
|
background: #4d89ff;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -263,16 +263,19 @@ export default {
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
.register-dialog {
|
.register-dialog {
|
||||||
border-radius: 20px;
|
border-radius: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
0deg,
|
160deg,
|
||||||
rgba(39, 20, 51, 0.7) 0%,
|
rgba(18, 24, 38, 0.98) 0%,
|
||||||
rgba(230, 33, 122, 0.7) 49%
|
rgba(26, 31, 46, 0.98) 50%,
|
||||||
|
rgba(37, 43, 58, 0.96) 100%
|
||||||
);
|
);
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
border-radius: 20px;
|
border: none;
|
||||||
border: 2px solid #e6217a;
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(77, 137, 255, 0.1),
|
||||||
|
0 4px 20px rgba(0, 0, 0, 0.35);
|
||||||
width: 500px;
|
width: 500px;
|
||||||
top: 45% !important;
|
top: 45% !important;
|
||||||
transform: translateY(-45%) !important;
|
transform: translateY(-45%) !important;
|
||||||
|
|
|
||||||
|
|
@ -24,60 +24,36 @@
|
||||||
:title="$t('common.myAccount')">
|
:title="$t('common.myAccount')">
|
||||||
<UserAccount />
|
<UserAccount />
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane
|
<!-- 隐藏有奖邀请 -->
|
||||||
key="money"
|
<!-- <a-tab-pane key="money" :title="$t('common.moneyInvite')"><Money /></a-tab-pane> -->
|
||||||
:title="$t('common.moneyInvite')">
|
<!-- 隐藏快速充值入口 -->
|
||||||
<Money />
|
<!-- <a-tab-pane
|
||||||
</a-tab-pane>
|
|
||||||
<a-tab-pane
|
|
||||||
key="recharge"
|
key="recharge"
|
||||||
:title="$t('common.rechargeRecord')">
|
:title="$t('common.rechargeRecord')">
|
||||||
<RechargeRecord />
|
<RechargeRecord />
|
||||||
</a-tab-pane>
|
</a-tab-pane> -->
|
||||||
<a-tab-pane
|
<a-tab-pane
|
||||||
key="resume"
|
key="resume"
|
||||||
:title="$t('common.myProduct')">
|
:title="$t('common.myProduct')">
|
||||||
<ResumeRecord />
|
<ResumeRecord />
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane
|
<!-- 隐藏邀请消费记录 -->
|
||||||
key="invite"
|
<!-- <a-tab-pane key="invite" :title="$t('common.inviteRecord')"><RewardRecord /></a-tab-pane> -->
|
||||||
:title="$t('common.inviteRecord')">
|
|
||||||
<RewardRecord />
|
|
||||||
</a-tab-pane>
|
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
</div>
|
</div>
|
||||||
</mf-dialog>
|
</mf-dialog>
|
||||||
<Forgot
|
|
||||||
:visible="forgotVisible"
|
|
||||||
@open="forgotVisible = true"
|
|
||||||
@back="back"
|
|
||||||
@cancel="forgotVisible = false" />
|
|
||||||
<Register
|
|
||||||
:visible="registerVisible"
|
|
||||||
@cancel="registerVisible = false"
|
|
||||||
@back="backFormRegister"
|
|
||||||
@open="registerVisible = true" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Forgot from './Forgot.vue'
|
|
||||||
import Register from './Register.vue'
|
|
||||||
import { mapGetters } from 'vuex'
|
|
||||||
import i18n from '@/lang/i18n'
|
|
||||||
import Money from './Money.vue'
|
|
||||||
import UserAccount from './UserAccount.vue'
|
import UserAccount from './UserAccount.vue'
|
||||||
import RechargeRecord from './RechargeRecord.vue'
|
|
||||||
import ResumeRecord from './ResumeRecord.vue'
|
import ResumeRecord from './ResumeRecord.vue'
|
||||||
import RewardRecord from './RewardRecord.vue'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
email: '',
|
email: '',
|
||||||
forgotVisible: false,
|
|
||||||
emailVisible: false,
|
emailVisible: false,
|
||||||
registerVisible: false,
|
|
||||||
loading: false,
|
loading: false,
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
|
|
@ -85,28 +61,13 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
visible: Boolean,
|
visible: Boolean
|
||||||
register: Boolean
|
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Forgot,
|
|
||||||
Register,
|
|
||||||
Money,
|
|
||||||
UserAccount,
|
UserAccount,
|
||||||
RechargeRecord,
|
ResumeRecord
|
||||||
ResumeRecord,
|
|
||||||
RewardRecord
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters(['lang'])
|
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.register) {
|
|
||||||
this.showRegister()
|
|
||||||
setTimeout((_) => {
|
|
||||||
this.$emit('cancel')
|
|
||||||
}, 300)
|
|
||||||
}
|
|
||||||
window.addEventListener('message', (event) => {
|
window.addEventListener('message', (event) => {
|
||||||
if (event.data.type === 'google-login') {
|
if (event.data.type === 'google-login') {
|
||||||
const { token } = event.data
|
const { token } = event.data
|
||||||
|
|
@ -120,24 +81,6 @@ export default {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showRegister() {
|
|
||||||
this.registerVisible = true
|
|
||||||
this.$emit('cancel')
|
|
||||||
},
|
|
||||||
changeLang(value) {
|
|
||||||
if (value != this.lang) {
|
|
||||||
this.$store.dispatch('main/setLanguage', value)
|
|
||||||
i18n.global.locale = value
|
|
||||||
}
|
|
||||||
},
|
|
||||||
back() {
|
|
||||||
this.forgotVisible = false
|
|
||||||
this.$emit('open')
|
|
||||||
},
|
|
||||||
backFormRegister() {
|
|
||||||
this.registerVisible = false
|
|
||||||
this.$emit('open')
|
|
||||||
},
|
|
||||||
backEmail() {
|
backEmail() {
|
||||||
this.emailVisible = false
|
this.emailVisible = false
|
||||||
this.$emit('open')
|
this.$emit('open')
|
||||||
|
|
@ -170,10 +113,6 @@ export default {
|
||||||
.catch((_) => {
|
.catch((_) => {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
})
|
})
|
||||||
},
|
|
||||||
showForgot() {
|
|
||||||
this.forgotVisible = true
|
|
||||||
this.$emit('cancel')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -182,10 +121,12 @@ export default {
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
.user-dialog {
|
.user-dialog {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #0f0f12;
|
background: #1a1f2e;
|
||||||
border-radius: 10px;
|
border-radius: 12px;
|
||||||
border-radius: 20px;
|
border: none;
|
||||||
border: 2px solid #e6217a;
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(77, 137, 255, 0.1),
|
||||||
|
0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
width: 500px;
|
width: 500px;
|
||||||
min-height: 520px;
|
min-height: 520px;
|
||||||
top: 45% !important;
|
top: 45% !important;
|
||||||
|
|
@ -227,11 +168,12 @@ export default {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
&-active {
|
&-active {
|
||||||
background: #e6217a;
|
background: #4d89ff;
|
||||||
|
box-shadow: 0 0 12px rgba(77, 137, 255, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover:not(.arco-tabs-tab-active) {
|
||||||
background: #e6217a;
|
background: rgba(77, 137, 255, 0.16);
|
||||||
|
|
||||||
.arco-tabs-tab-title {
|
.arco-tabs-tab-title {
|
||||||
&:before {
|
&:before {
|
||||||
|
|
|
||||||
|
|
@ -281,10 +281,12 @@ export default {
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
.edit-user-dialog {
|
.edit-user-dialog {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #0f0f12;
|
background: #1a1f2e;
|
||||||
border-radius: 10px;
|
border-radius: 12px;
|
||||||
border-radius: 20px;
|
border: none;
|
||||||
border: 2px solid #e6217a;
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(77, 137, 255, 0.1),
|
||||||
|
0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
width: 500px;
|
width: 500px;
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
top: 45% !important;
|
top: 45% !important;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@
|
||||||
class="app-main-wrap"
|
class="app-main-wrap"
|
||||||
:style="style">
|
:style="style">
|
||||||
<section class="app-main">
|
<section class="app-main">
|
||||||
|
<transition
|
||||||
|
name="portal-page"
|
||||||
|
mode="out-in">
|
||||||
<keep-alive
|
<keep-alive
|
||||||
:include="cachedViews"
|
:include="cachedViews"
|
||||||
:max="100">
|
:max="100">
|
||||||
|
|
@ -11,6 +14,7 @@
|
||||||
:is="Component"
|
:is="Component"
|
||||||
:key="key" />
|
:key="key" />
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
|
</transition>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<a-back-top
|
<a-back-top
|
||||||
|
|
@ -24,6 +28,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import Breadcrumb from './Breadcrumb.vue'
|
import Breadcrumb from './Breadcrumb.vue'
|
||||||
|
const LAYOUT_MOBILE_MAX = 768
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'app-main',
|
name: 'app-main',
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -33,6 +39,21 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: { Breadcrumb },
|
components: { Breadcrumb },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
layoutWidth:
|
||||||
|
typeof window !== 'undefined' ? window.innerWidth : LAYOUT_MOBILE_MAX + 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this._layoutOnResize = () => {
|
||||||
|
this.layoutWidth = window.innerWidth
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', this._layoutOnResize)
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
window.removeEventListener('resize', this._layoutOnResize)
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['cachedViews', 'sidebar']),
|
...mapGetters(['cachedViews', 'sidebar']),
|
||||||
// 相同路由,不同参数缓存问题
|
// 相同路由,不同参数缓存问题
|
||||||
|
|
@ -43,7 +64,10 @@ export default {
|
||||||
return !this.sidebar.opened
|
return !this.sidebar.opened
|
||||||
},
|
},
|
||||||
style() {
|
style() {
|
||||||
let sidebarWidth = this.isCollapsed ? '50px' : this.sidebar.width
|
if (this.layoutWidth <= LAYOUT_MOBILE_MAX) {
|
||||||
|
return { width: '100%' }
|
||||||
|
}
|
||||||
|
const sidebarWidth = this.isCollapsed ? '56px' : this.sidebar.width
|
||||||
return {
|
return {
|
||||||
width: `calc(100% - ${sidebarWidth})`
|
width: `calc(100% - ${sidebarWidth})`
|
||||||
}
|
}
|
||||||
|
|
@ -64,13 +88,15 @@ export default {
|
||||||
.app-main {
|
.app-main {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
&-wrap {
|
&-wrap {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
@media (max-width: 768px) {
|
||||||
.app-main {
|
.app-main {
|
||||||
&-wrap {
|
&-wrap {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="navbar">
|
<div class="navbar">
|
||||||
<div class="left-menu">
|
<div class="left-menu">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="sidebar-trigger"
|
||||||
|
:aria-expanded="sidebar.opened"
|
||||||
|
aria-label="展开或收起侧栏"
|
||||||
|
@click="$store.dispatch('main/toggleSideBar')">
|
||||||
<mf-icon
|
<mf-icon
|
||||||
:class="['left-collapse', { isCollapse: isCollapse }]"
|
class="sidebar-trigger__icon sidebar-trigger__icon--desktop"
|
||||||
|
:class="{ isCollapse: isCollapse }"
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
@click="$store.dispatch('main/toggleSideBar')"
|
|
||||||
value="icon-shrink" />
|
value="icon-shrink" />
|
||||||
|
<span class="sidebar-trigger__icon sidebar-trigger__icon--mobile" aria-hidden="true">
|
||||||
|
<span class="hamburger-line" />
|
||||||
|
<span class="hamburger-line" />
|
||||||
|
<span class="hamburger-line" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<div
|
<div
|
||||||
class="logo-wrap"
|
class="logo-wrap"
|
||||||
|
|
@ -31,20 +43,9 @@
|
||||||
</mf-button>
|
</mf-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="right-menu-item language">
|
<div class="right-menu-item language">
|
||||||
<a-dropdown @select="handleSelect">
|
<mf-button type="text" disabled class="language-display">
|
||||||
<mf-button type="text">
|
|
||||||
{{ localeName }}
|
{{ localeName }}
|
||||||
<icon-down />
|
|
||||||
</mf-button>
|
</mf-button>
|
||||||
<template #content>
|
|
||||||
<a-doption
|
|
||||||
v-for="(name, code) in localeNames"
|
|
||||||
:key="code"
|
|
||||||
:value="code">
|
|
||||||
{{ name }}
|
|
||||||
</a-doption>
|
|
||||||
</template>
|
|
||||||
</a-dropdown>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="right-menu-item user"
|
class="right-menu-item user"
|
||||||
|
|
@ -56,7 +57,7 @@
|
||||||
:preview="false"
|
:preview="false"
|
||||||
:width="16"
|
:width="16"
|
||||||
fit="cover"
|
fit="cover"
|
||||||
src="/images/nav/img_money@2x.png" />
|
src="https://img.icons8.com/fluency/48/wallet.png" />
|
||||||
{{ userInfo.balance || '0.00' }}
|
{{ userInfo.balance || '0.00' }}
|
||||||
</span>
|
</span>
|
||||||
<mf-avatar
|
<mf-avatar
|
||||||
|
|
@ -77,7 +78,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Login
|
<Login
|
||||||
:register="!$datas.isEmpty(inviteCode)"
|
|
||||||
:visible="showLogin"
|
:visible="showLogin"
|
||||||
@open="openLogin"
|
@open="openLogin"
|
||||||
@cancel="cancelLogin" />
|
@cancel="cancelLogin" />
|
||||||
|
|
@ -97,8 +97,9 @@ import { mapGetters, mapState } from 'vuex'
|
||||||
import cloneDeep from 'lodash-es/cloneDeep'
|
import cloneDeep from 'lodash-es/cloneDeep'
|
||||||
import { constantRoutes } from '@/router/index.js'
|
import { constantRoutes } from '@/router/index.js'
|
||||||
import Login from './Login.vue'
|
import Login from './Login.vue'
|
||||||
import i18n, { LOCALE_NAMES } from '@/lang/i18n'
|
import { LOCALE_NAMES } from '@/lang/i18n'
|
||||||
import User from './User.vue'
|
import User from './User.vue'
|
||||||
|
import { getToken } from '@/utils/auth'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'nav-bar',
|
name: 'nav-bar',
|
||||||
|
|
@ -116,7 +117,6 @@ export default {
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
publishVisible: false,
|
publishVisible: false,
|
||||||
inviteCode: this.$route.query?.inviteCode,
|
|
||||||
userVisible: false,
|
userVisible: false,
|
||||||
logoUrl: null
|
logoUrl: null
|
||||||
}
|
}
|
||||||
|
|
@ -126,9 +126,6 @@ export default {
|
||||||
demoEnv() {
|
demoEnv() {
|
||||||
return import.meta.env.MODE === 'demo'
|
return import.meta.env.MODE === 'demo'
|
||||||
},
|
},
|
||||||
localeNames() {
|
|
||||||
return LOCALE_NAMES
|
|
||||||
},
|
|
||||||
localeName() {
|
localeName() {
|
||||||
return LOCALE_NAMES[this.lang] || 'English'
|
return LOCALE_NAMES[this.lang] || 'English'
|
||||||
},
|
},
|
||||||
|
|
@ -180,10 +177,10 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.inviteCode) {
|
|
||||||
this.openLogin()
|
|
||||||
}
|
|
||||||
this.getLogo()
|
this.getLogo()
|
||||||
|
if (getToken()) {
|
||||||
|
this.$store.dispatch('user/getInfo').catch(() => {})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
openLogin() {
|
openLogin() {
|
||||||
|
|
@ -219,12 +216,6 @@ export default {
|
||||||
this.openLogin()
|
this.openLogin()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleSelect(value) {
|
|
||||||
if (value != this.lang) {
|
|
||||||
this.$store.dispatch('main/setLanguage', value)
|
|
||||||
i18n.global.locale = value
|
|
||||||
}
|
|
||||||
},
|
|
||||||
logout() {
|
logout() {
|
||||||
this.$store.dispatch('user/logout2').then((_) => {
|
this.$store.dispatch('user/logout2').then((_) => {
|
||||||
this.$router.replace('/')
|
this.$router.replace('/')
|
||||||
|
|
@ -246,28 +237,63 @@ export default {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
padding-left: 30px;
|
padding-left: 20px;
|
||||||
padding-right: 30px;
|
padding-right: 30px;
|
||||||
|
background: #1a1f2e;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
|
||||||
|
|
||||||
.left {
|
.left-menu {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
padding-left: 4px;
|
||||||
|
height: 60px;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
&-collapse {
|
.sidebar-trigger {
|
||||||
display: none;
|
display: inline-flex;
|
||||||
transition: 0.25s;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: rgba(255, 255, 255, 0.92);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon--desktop {
|
||||||
|
display: flex;
|
||||||
|
transition: transform 0.25s;
|
||||||
|
|
||||||
&.isCollapse {
|
&.isCollapse {
|
||||||
transform: rotate(-180deg);
|
transform: rotate(-180deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-menu {
|
&__icon--mobile {
|
||||||
padding-left: 8px;
|
display: none;
|
||||||
display: flex;
|
flex-direction: column;
|
||||||
align-items: center;
|
justify-content: center;
|
||||||
height: 60px;
|
gap: 5px;
|
||||||
|
width: 22px;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger-line {
|
||||||
|
display: block;
|
||||||
|
height: 2px;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 1px;
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -281,7 +307,6 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.right-menu {
|
.right-menu {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
|
|
@ -303,12 +328,18 @@ export default {
|
||||||
&.language {
|
&.language {
|
||||||
.mf-button {
|
.mf-button {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #999999;
|
color: #9ca3af;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.language-display[disabled] {
|
||||||
|
color: #9ca3af !important;
|
||||||
|
opacity: 0.85;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.logout {
|
&.logout {
|
||||||
|
|
@ -330,10 +361,9 @@ export default {
|
||||||
&.user {
|
&.user {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
color: rgb(var(--primary-6));
|
color: rgb(var(--primary-6));
|
||||||
border: 2px solid transparent;
|
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border: 1px solid #5c5d68;
|
border: 1px solid rgba(77, 137, 255, 0.2);
|
||||||
background: #26272e;
|
background: #252b3a;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -410,24 +440,24 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
@media (max-width: 768px) {
|
||||||
.navbar {
|
.navbar {
|
||||||
padding: 0 10px;
|
padding: 0 12px;
|
||||||
.left {
|
|
||||||
&-collapse {
|
.left-menu {
|
||||||
display: block;
|
.sidebar-trigger {
|
||||||
font-size: 18px;
|
&__icon--desktop {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon--mobile {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-menu {
|
|
||||||
display: flex;
|
|
||||||
.logo {
|
.logo {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.mf-icon {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-menu-item {
|
.right-menu-item {
|
||||||
|
|
|
||||||
|
|
@ -112,19 +112,19 @@ export default {
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: width 0.3s;
|
transition: width 0.3s;
|
||||||
z-index: 12;
|
z-index: 12;
|
||||||
background-color: #000;
|
background-color: #121826;
|
||||||
border-right: 1px solid; /* 宽度随你调整 */
|
border-right: 1px solid transparent;
|
||||||
border-image: linear-gradient(
|
border-image: linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
#0f0f12 0%,
|
#121826 0%,
|
||||||
rgba(255,255,255, 0.7) 40%,
|
rgba(77, 137, 255, 0.35) 45%,
|
||||||
rgba(255,255,255, 0.7) 60%,
|
rgba(157, 107, 255, 0.25) 55%,
|
||||||
#0f0f12 100%
|
#121826 100%
|
||||||
)
|
)
|
||||||
1 100%;
|
1 100%;
|
||||||
|
|
||||||
&.collapsed {
|
&.collapsed {
|
||||||
width: 50px !important;
|
width: 56px !important;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
:deep(.arco-menu) {
|
:deep(.arco-menu) {
|
||||||
|
|
@ -134,19 +134,36 @@ export default {
|
||||||
|
|
||||||
:deep(.arco-menu) {
|
:deep(.arco-menu) {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
padding-left: 16px;
|
padding: 8px 12px 12px;
|
||||||
|
|
||||||
&-item {
|
&-item {
|
||||||
width: 180px;
|
min-height: 48px;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-size: 15px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: rgba(255, 255, 255, 0.7);
|
color: rgba(255, 255, 255, 0.7);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 10px;
|
||||||
|
|
||||||
&.arco-menu-selected,
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgb(var(--primary-6));
|
background-color: rgba(77, 137, 255, 0.12);
|
||||||
|
color: #e5e7eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.arco-menu-selected {
|
||||||
|
background-color: rgba(77, 137, 255, 0.2);
|
||||||
|
color: #e5e7eb;
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(77, 137, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.arco-menu-selected:hover {
|
||||||
|
background-color: rgba(77, 137, 255, 0.26);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.arco-menu-collapsed) .arco-menu-item {
|
||||||
|
width: calc(100% - 8px);
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -172,18 +189,22 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
@media (max-width: 768px) {
|
||||||
.sidebar-container {
|
.sidebar-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
top: 60px;
|
top: 60px;
|
||||||
z-index: 10;
|
z-index: 100;
|
||||||
height: calc(100% - 60px);
|
height: calc(100% - 60px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
box-shadow: 8px 0 32px rgba(0, 0, 0, 0.35);
|
||||||
|
|
||||||
&.collapsed {
|
&.collapsed {
|
||||||
width: 0px !important;
|
width: 0px !important;
|
||||||
|
min-width: 0 !important;
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="app-wrapper">
|
<div class="app-wrapper">
|
||||||
|
<div
|
||||||
|
v-show="showSidebar && mobileDrawerOpen"
|
||||||
|
class="sidebar-backdrop"
|
||||||
|
aria-hidden="true"
|
||||||
|
@click="closeMobileDrawer" />
|
||||||
<div class="fixed-header">
|
<div class="fixed-header">
|
||||||
<nav-bar class="nav-bar" />
|
<nav-bar class="nav-bar" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -18,6 +23,8 @@
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import { sideBar, appMain, navBar, appFooter } from './components'
|
import { sideBar, appMain, navBar, appFooter } from './components'
|
||||||
|
|
||||||
|
const LAYOUT_MOBILE_MAX = 768
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'layout',
|
name: 'layout',
|
||||||
components: {
|
components: {
|
||||||
|
|
@ -26,16 +33,44 @@ export default {
|
||||||
navBar,
|
navBar,
|
||||||
appFooter
|
appFooter
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
layoutWidth:
|
||||||
|
typeof window !== 'undefined' ? window.innerWidth : LAYOUT_MOBILE_MAX + 1
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['sidebar']),
|
...mapGetters(['sidebar']),
|
||||||
isCollapsed() {
|
isCollapsed() {
|
||||||
return !this.sidebar.opened
|
return !this.sidebar.opened
|
||||||
},
|
},
|
||||||
|
isMobileLayout() {
|
||||||
|
return this.layoutWidth <= LAYOUT_MOBILE_MAX
|
||||||
|
},
|
||||||
|
mobileDrawerOpen() {
|
||||||
|
return this.isMobileLayout && this.sidebar.opened
|
||||||
|
},
|
||||||
// 是否显示菜单
|
// 是否显示菜单
|
||||||
showSidebar() {
|
showSidebar() {
|
||||||
let { menu } = this.$route.query || {}
|
let { menu } = this.$route.query || {}
|
||||||
return menu != 0
|
return menu != 0
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this._layoutOnResize = () => {
|
||||||
|
this.layoutWidth = window.innerWidth
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', this._layoutOnResize)
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
window.removeEventListener('resize', this._layoutOnResize)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeMobileDrawer() {
|
||||||
|
if (this.isMobileLayout && this.sidebar.opened) {
|
||||||
|
this.$store.dispatch('main/closeSideBar')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -47,7 +82,11 @@ export default {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #0f0f12;
|
background: #121826;
|
||||||
|
|
||||||
|
.sidebar-backdrop {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.fixed-header {
|
.fixed-header {
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
|
@ -59,6 +98,18 @@ export default {
|
||||||
.main-wrapper {
|
.main-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: calc(100% - 60px);
|
height: calc(100% - 60px);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.app-wrapper .sidebar-backdrop {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
inset: 60px 0 0 0;
|
||||||
|
z-index: 99;
|
||||||
|
background: rgba(0, 0, 0, 0.55);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,11 @@ import router from './router'
|
||||||
|
|
||||||
import '@/assets/styles/base.less'
|
import '@/assets/styles/base.less'
|
||||||
|
|
||||||
import '@/assets/styles/quill.bubble.css'
|
// Quill 样式 - 临时注释避免 source map 文件缺失导致的构建错误
|
||||||
import '@/assets/styles/quill.core.css'
|
// 如果后续需要使用 Quill 富文本编辑器,可取消注释
|
||||||
import '@/assets/styles/quill.snow.css'
|
// import '@/assets/styles/quill.bubble.css'
|
||||||
|
// import '@/assets/styles/quill.core.css'
|
||||||
|
// import '@/assets/styles/quill.snow.css'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ export const constantRoutes = [{
|
||||||
path: '/',
|
path: '/',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
redirect: {
|
redirect: {
|
||||||
name: 'index'
|
name: 'video-gen'
|
||||||
},
|
},
|
||||||
children: [{
|
children: [{
|
||||||
path: 'index',
|
path: 'index',
|
||||||
|
|
@ -69,49 +69,47 @@ export const constantRoutes = [{
|
||||||
permission: "pass",
|
permission: "pass",
|
||||||
icon: 'btn_tst'
|
icon: 'btn_tst'
|
||||||
}
|
}
|
||||||
},
|
}, {
|
||||||
// {
|
path: 'image-to-image?type=2',
|
||||||
// path: 'image-to-image?type=2',
|
name: 'image-to-image2',
|
||||||
// name: 'image-to-image2',
|
component: () => import('@/views/Image.vue'),
|
||||||
// component: () => import('@/views/Image.vue'),
|
meta: {
|
||||||
// meta: {
|
title: 'imageToImage2',
|
||||||
// title: 'imageToImage2',
|
menuItem: true,
|
||||||
// menuItem: true,
|
permission: "pass",
|
||||||
// permission: "pass",
|
icon: 'btn_tst'
|
||||||
// icon: 'btn_tst'
|
}
|
||||||
// }
|
}, {
|
||||||
// }, {
|
path: 'change-face',
|
||||||
// path: 'change-face',
|
name: 'change-face',
|
||||||
// name: 'change-face',
|
component: () => import('@/views/ChangeFace.vue'),
|
||||||
// component: () => import('@/views/ChangeFace.vue'),
|
meta: {
|
||||||
// meta: {
|
title: 'changeFace',
|
||||||
// title: 'changeFace',
|
menuItem: true,
|
||||||
// menuItem: true,
|
permission: "pass",
|
||||||
// permission: "pass",
|
icon: 'btn_yjhl'
|
||||||
// icon: 'btn_yjhl'
|
}
|
||||||
// }
|
}, {
|
||||||
// }, {
|
path: 'change-face-video',
|
||||||
// path: 'change-face-video',
|
name: 'change-face-video',
|
||||||
// name: 'change-face-video',
|
component: () => import('@/views/ChangeFace.vue'),
|
||||||
// component: () => import('@/views/ChangeFace.vue'),
|
meta: {
|
||||||
// meta: {
|
title: 'changeFaceVideo',
|
||||||
// title: 'changeFaceVideo',
|
menuItem: true,
|
||||||
// menuItem: true,
|
permission: "pass",
|
||||||
// permission: "pass",
|
icon: 'btn_yjhl'
|
||||||
// icon: 'btn_yjhl'
|
}
|
||||||
// }
|
}, {
|
||||||
// }, {
|
path: 'fast-image',
|
||||||
// path: 'fast-image',
|
name: 'fast-image',
|
||||||
// name: 'fast-image',
|
component: () => import('@/views/FastImage.vue'),
|
||||||
// component: () => import('@/views/FastImage.vue'),
|
meta: {
|
||||||
// meta: {
|
title: 'fastImage',
|
||||||
// title: 'fastImage',
|
menuItem: true,
|
||||||
// menuItem: true,
|
permission: "pass",
|
||||||
// permission: "pass",
|
icon: 'btn_kjst'
|
||||||
// icon: 'btn_kjst'
|
}
|
||||||
// }
|
}, {
|
||||||
// },
|
|
||||||
{
|
|
||||||
path: 'fast-video',
|
path: 'fast-video',
|
||||||
name: 'fast-video',
|
name: 'fast-video',
|
||||||
component: () => import('@/views/FastVideo.vue'),
|
component: () => import('@/views/FastVideo.vue'),
|
||||||
|
|
@ -122,14 +120,44 @@ export const constantRoutes = [{
|
||||||
icon: 'btn_kjsp'
|
icon: 'btn_kjsp'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'recharge',
|
path: 'video-gen',
|
||||||
name: 'recharge',
|
name: 'video-gen',
|
||||||
component: () => import('@/views/Recharge.vue'),
|
component: () => import('@/views/VideoGen.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'recharge',
|
title: 'videoGen',
|
||||||
menuItem: true,
|
menuItem: true,
|
||||||
permission: "pass",
|
permission: "pass",
|
||||||
icon: 'btn_kscz'
|
icon: 'btn_video'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
path: 'asset-group-manage',
|
||||||
|
name: 'asset-group-manage',
|
||||||
|
component: () => import('@/views/AssetGroupManage.vue'),
|
||||||
|
meta: {
|
||||||
|
title: 'assetGroupManage',
|
||||||
|
menuItem: true,
|
||||||
|
permission: "pass",
|
||||||
|
icon: 'btn_video'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
path: 'asset-manage',
|
||||||
|
name: 'asset-manage',
|
||||||
|
component: () => import('@/views/AssetManage.vue'),
|
||||||
|
meta: {
|
||||||
|
title: 'assetManage',
|
||||||
|
menuItem: true,
|
||||||
|
permission: "pass",
|
||||||
|
icon: 'btn_video'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
path: 'generated-assets',
|
||||||
|
name: 'generatedAssets',
|
||||||
|
component: () => import('@/views/GeneratedAssets.vue'),
|
||||||
|
meta: {
|
||||||
|
title: 'generatedAssets',
|
||||||
|
menuItem: true,
|
||||||
|
permission: "pass",
|
||||||
|
icon: 'btn_video'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'help',
|
path: 'help',
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,13 @@ const state = {
|
||||||
sidebar: {
|
sidebar: {
|
||||||
opened: isMobile() ? false : true,
|
opened: isMobile() ? false : true,
|
||||||
withoutAnimation: false,
|
withoutAnimation: false,
|
||||||
width: Cookies.get('sidebarWidth') || '230px'
|
// 左侧导航默认适中宽度(用户若需更窄/宽可本地调小范围会写 cookie)
|
||||||
|
width: Cookies.get('sidebarWidth') || '268px'
|
||||||
},
|
},
|
||||||
topMenu: Cookies.get('topMenu'),
|
topMenu: Cookies.get('topMenu'),
|
||||||
// 当前语言
|
// 当前语言
|
||||||
language: Cookies.get('language') || 'en_US',
|
// 多语言切换已禁用:全站固定繁体中文(zh_HK)
|
||||||
|
language: 'zh_HK',
|
||||||
// 主题 '':亮色 / dark:暗黑
|
// 主题 '':亮色 / dark:暗黑
|
||||||
theme: $storage.get('theme') || '',
|
theme: $storage.get('theme') || '',
|
||||||
// 系统端
|
// 系统端
|
||||||
|
|
@ -39,8 +41,8 @@ const state = {
|
||||||
showMessage: false,
|
showMessage: false,
|
||||||
messageData: {},
|
messageData: {},
|
||||||
messageCount: 0,
|
messageCount: 0,
|
||||||
// 是否显示18禁弹窗
|
// 是否显示18禁弹窗:默认不拦截,直接进入首页等主页面
|
||||||
showForbidden: true,
|
showForbidden: false,
|
||||||
// 阻止页面切换
|
// 阻止页面切换
|
||||||
showPrevent: false,
|
showPrevent: false,
|
||||||
showLogin: false
|
showLogin: false
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,12 @@ export const isFirefox = (_) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isMobile = _=> {
|
export const isMobile = (_) => {
|
||||||
const userAgent = navigator.userAgent;
|
const userAgent = navigator.userAgent
|
||||||
const isMobile = /iPad|Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
|
const uaMobile =
|
||||||
const isSmallScreen = window.innerWidth <= 576;
|
/iPad|Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||||
return isMobile || isSmallScreen;
|
userAgent
|
||||||
|
)
|
||||||
|
const isSmallScreen = typeof window !== 'undefined' && window.innerWidth <= 768
|
||||||
|
return uaMobile || isSmallScreen
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
/**
|
||||||
|
* 火山素材相关接口 data 层字段兼容:
|
||||||
|
* Spring/Jackson 序列化为 camelCase(items、totalCount),
|
||||||
|
* 旧前端曾按 PascalCase(Items、TotalCount)取值,此处统一解析。
|
||||||
|
*/
|
||||||
|
export function byteApiItems(data) {
|
||||||
|
if (!data) return []
|
||||||
|
if (Array.isArray(data.items)) return data.items
|
||||||
|
if (Array.isArray(data.Items)) return data.Items
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
export function byteApiTotalCount(data) {
|
||||||
|
if (!data) return 0
|
||||||
|
const raw = data.totalCount ?? data.TotalCount
|
||||||
|
const n = Number(raw)
|
||||||
|
return Number.isFinite(n) ? n : 0
|
||||||
|
}
|
||||||
|
|
@ -130,6 +130,23 @@ export const convertBase64ToUrl = (base64) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从若依 AjaxResult / 兼容仅返回顶层 url 的上传响应中解析访问地址
|
||||||
|
*/
|
||||||
|
export const extractUploadUrlFromResponse = (res) => {
|
||||||
|
if (!res) return ''
|
||||||
|
if (typeof res.data === 'string' && res.data.trim()) return res.data.trim()
|
||||||
|
if (res.data && typeof res.data === 'object') {
|
||||||
|
const u = res.data.url || res.data.path
|
||||||
|
if (typeof u === 'string' && u.trim()) return u.trim()
|
||||||
|
}
|
||||||
|
if (typeof res.url === 'string' && res.url.trim()) return res.url.trim()
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 门户统一腾讯云 COS:后端 FileController `/api/file/upload`,与 mf-image-upload 一致 */
|
||||||
|
export const PORTAL_TENCENT_COS_UPLOAD_URL = '/api/file/upload'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件上传
|
* 文件上传
|
||||||
* @param {Object} {url, file,name} 文件上传地址 file 文件 name 文件名参数
|
* @param {Object} {url, file,name} 文件上传地址 file 文件 name 文件名参数
|
||||||
|
|
@ -148,12 +165,10 @@ export const uploadFile = ({
|
||||||
formData.append(key, data[key])
|
formData.append(key, data[key])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// 不要手动设置 Content-Type,否则缺少 boundary,服务端无法解析 multipart,文件参数字段为空
|
||||||
return request({
|
return request({
|
||||||
url: url,
|
url: url,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: formData,
|
data: formData
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -21,7 +21,7 @@ const messages = i18n.global.messages[lang];
|
||||||
* @desc 创建axios实例
|
* @desc 创建axios实例
|
||||||
*/
|
*/
|
||||||
const service = axios.create({
|
const service = axios.create({
|
||||||
// baseURL: process.env.VUE_APP_BASE_API,
|
baseURL: process.env.VUE_APP_BASE_API,
|
||||||
withCredentials: import.meta.env.MODE === 'development',
|
withCredentials: import.meta.env.MODE === 'development',
|
||||||
timeout: 600 * 1000
|
timeout: 600 * 1000
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,465 @@
|
||||||
|
<template>
|
||||||
|
<div class="asset-group-page">
|
||||||
|
<section class="ag-panel">
|
||||||
|
<div class="ag-head">
|
||||||
|
<h3 class="ag-title">资源组管理</h3>
|
||||||
|
<a-button type="primary" @click="openCreateDialog">新增资源组</a-button>
|
||||||
|
</div>
|
||||||
|
<div class="ag-filter">
|
||||||
|
<div class="ag-field">
|
||||||
|
<label>名称</label>
|
||||||
|
<a-input v-model="filters.name" placeholder="按名称过滤" />
|
||||||
|
</div>
|
||||||
|
<div class="ag-field">
|
||||||
|
<label>资源组编号</label>
|
||||||
|
<a-input v-model="filters.groupIdsText" placeholder="多个编号用英文逗号分隔" />
|
||||||
|
</div>
|
||||||
|
<div class="ag-field">
|
||||||
|
<label>资源组类型</label>
|
||||||
|
<a-select v-model="filters.groupType">
|
||||||
|
<a-option value="AIGC">AIGC(生成类)</a-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
<div class="ag-field">
|
||||||
|
<label>排序字段</label>
|
||||||
|
<a-select v-model="filters.sortBy">
|
||||||
|
<a-option value="CreateTime">创建时间</a-option>
|
||||||
|
<a-option value="UpdateTime">更新时间</a-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
<div class="ag-field">
|
||||||
|
<label>排序方向</label>
|
||||||
|
<a-select v-model="filters.sortOrder">
|
||||||
|
<a-option value="Desc">从新到旧</a-option>
|
||||||
|
<a-option value="Asc">从旧到新</a-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
<div class="ag-actions">
|
||||||
|
<a-button type="primary" :loading="listLoading" @click="search(1)">查询</a-button>
|
||||||
|
<a-button @click="resetFilters">重置</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-spin :loading="listLoading">
|
||||||
|
<div class="ag-total">总数:{{ totalCount }}</div>
|
||||||
|
<div class="ag-table-wrap">
|
||||||
|
<table class="ag-table">
|
||||||
|
<colgroup>
|
||||||
|
<col class="ag-col-id" />
|
||||||
|
<col class="ag-col-name" />
|
||||||
|
<col class="ag-col-desc" />
|
||||||
|
<col class="ag-col-type" />
|
||||||
|
<col class="ag-col-project" />
|
||||||
|
<col class="ag-col-time" />
|
||||||
|
<col class="ag-col-time" />
|
||||||
|
<col class="ag-col-action" />
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>编号</th>
|
||||||
|
<th>名称</th>
|
||||||
|
<th>描述</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>项目名称</th>
|
||||||
|
<th>创建时间</th>
|
||||||
|
<th>更新时间</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="item in items" :key="item.Id || item.id">
|
||||||
|
<td>{{ item.Id || item.id }}</td>
|
||||||
|
<td>{{ item.Name || item.name }}</td>
|
||||||
|
<td>{{ item.Description || item.description || '-' }}</td>
|
||||||
|
<td>{{ formatGroupTypeLabel(item) }}</td>
|
||||||
|
<td>{{ item.ProjectName || item.projectName || '-' }}</td>
|
||||||
|
<td>{{ item.CreateTime || item.createTime || '-' }}</td>
|
||||||
|
<td>{{ item.UpdateTime || item.updateTime || '-' }}</td>
|
||||||
|
<td>
|
||||||
|
<a-button
|
||||||
|
size="mini"
|
||||||
|
type="outline"
|
||||||
|
:loading="detailLoadingId === (item.Id || item.id)"
|
||||||
|
@click="getDetail(item)">
|
||||||
|
详情
|
||||||
|
</a-button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!items.length">
|
||||||
|
<td colspan="8" class="ag-empty">暂无数据</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="ag-pagination">
|
||||||
|
<a-pagination
|
||||||
|
:total="totalCount"
|
||||||
|
:current="filters.pageNumber"
|
||||||
|
:page-size="filters.pageSize"
|
||||||
|
show-total
|
||||||
|
show-jumper
|
||||||
|
show-page-size
|
||||||
|
:page-size-options="listPageSizeOptions"
|
||||||
|
@change="onGroupPageChange"
|
||||||
|
@page-size-change="onPageSizeChange" />
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<a-modal v-model:visible="detailVisible" title="资源组详情" :footer="false" width="680px">
|
||||||
|
<pre class="ag-detail">{{ prettyDetail }}</pre>
|
||||||
|
</a-modal>
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="createVisible"
|
||||||
|
title="新增资源组"
|
||||||
|
width="520px"
|
||||||
|
:confirm-loading="createLoading"
|
||||||
|
@ok="createGroup">
|
||||||
|
<!-- 弹层 teleported 到 body,使用 FormItem 保证标签与 dark 样式生效 -->
|
||||||
|
<a-form :model="createForm" layout="horizontal" auto-label-width class="ag-create-modal-form">
|
||||||
|
<a-form-item label="名称" required>
|
||||||
|
<a-input
|
||||||
|
v-model="createForm.name"
|
||||||
|
placeholder="请输入资源组名称(≤64 字符)"
|
||||||
|
:max-length="64"
|
||||||
|
allow-clear />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="描述">
|
||||||
|
<a-textarea
|
||||||
|
v-model="createForm.description"
|
||||||
|
:max-length="300"
|
||||||
|
show-word-limit
|
||||||
|
:auto-size="{ minRows: 3, maxRows: 8 }"
|
||||||
|
placeholder="请输入描述(≤300 字符)" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { byteApiItems, byteApiTotalCount } from '@/utils/byteAssetApi'
|
||||||
|
|
||||||
|
const LIST_PAGE_NUMBER_MAX = 100
|
||||||
|
const LIST_PAGE_SIZE_OPTIONS = [10, 20, 50, 100]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AssetGroupManage',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
createLoading: false,
|
||||||
|
createVisible: false,
|
||||||
|
listLoading: false,
|
||||||
|
detailLoadingId: '',
|
||||||
|
detailVisible: false,
|
||||||
|
detailData: null,
|
||||||
|
createForm: {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
groupType: 'AIGC'
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
name: '',
|
||||||
|
groupIdsText: '',
|
||||||
|
groupType: 'AIGC',
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
sortBy: 'CreateTime',
|
||||||
|
sortOrder: 'Desc'
|
||||||
|
},
|
||||||
|
totalCount: 0,
|
||||||
|
items: [],
|
||||||
|
listPageSizeOptions: LIST_PAGE_SIZE_OPTIONS
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
prettyDetail() {
|
||||||
|
return this.detailData ? JSON.stringify(this.detailData, null, 2) : '{}'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.search(1)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatGroupTypeLabel(item) {
|
||||||
|
const t = String(item?.GroupType || item?.groupType || '').trim()
|
||||||
|
if (!t) return '-'
|
||||||
|
if (t === 'AIGC') return 'AIGC(生成类)'
|
||||||
|
return t
|
||||||
|
},
|
||||||
|
clampGroupPage(n) {
|
||||||
|
const page = Number(n) || 1
|
||||||
|
const size = Number(this.filters.pageSize) || 10
|
||||||
|
let maxPage = LIST_PAGE_NUMBER_MAX
|
||||||
|
if (this.totalCount > 0) {
|
||||||
|
const totalPages = Math.ceil(this.totalCount / size)
|
||||||
|
maxPage = Math.min(LIST_PAGE_NUMBER_MAX, Math.max(1, totalPages))
|
||||||
|
}
|
||||||
|
return Math.min(Math.max(1, page), maxPage)
|
||||||
|
},
|
||||||
|
onGroupPageChange(page) {
|
||||||
|
this.search(this.clampGroupPage(page))
|
||||||
|
},
|
||||||
|
openCreateDialog() {
|
||||||
|
this.createVisible = true
|
||||||
|
},
|
||||||
|
buildGroupIds() {
|
||||||
|
return String(this.filters.groupIdsText || '')
|
||||||
|
.split(',')
|
||||||
|
.map((x) => x.trim())
|
||||||
|
.filter((x) => !!x)
|
||||||
|
},
|
||||||
|
async createGroup() {
|
||||||
|
const name = String(this.createForm.name || '').trim()
|
||||||
|
if (!name) {
|
||||||
|
this.$message.error('请填写名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.createLoading = true
|
||||||
|
try {
|
||||||
|
const res = await this.$axios({
|
||||||
|
url: 'api/byteAssetGroup/createAssetGroup',
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
description: String(this.createForm.description || '').trim()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.$message.success('新增成功')
|
||||||
|
this.createForm.name = ''
|
||||||
|
this.createForm.description = ''
|
||||||
|
this.createVisible = false
|
||||||
|
this.search(1)
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '新增失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(e?.message || '新增失败')
|
||||||
|
} finally {
|
||||||
|
this.createLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async search(page = this.filters.pageNumber) {
|
||||||
|
this.filters.pageNumber = this.clampGroupPage(page)
|
||||||
|
this.listLoading = true
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
filter: {
|
||||||
|
groupType: this.filters.groupType || 'AIGC'
|
||||||
|
},
|
||||||
|
pageNumber: this.filters.pageNumber,
|
||||||
|
pageSize: this.filters.pageSize,
|
||||||
|
sortBy: this.filters.sortBy,
|
||||||
|
sortOrder: this.filters.sortOrder
|
||||||
|
}
|
||||||
|
const name = String(this.filters.name || '').trim()
|
||||||
|
if (name) payload.filter.name = name
|
||||||
|
const ids = this.buildGroupIds()
|
||||||
|
if (ids.length) payload.filter.groupIds = ids
|
||||||
|
|
||||||
|
const res = await this.$axios({
|
||||||
|
url: 'api/byteAssetGroup/listAssetGroups',
|
||||||
|
method: 'POST',
|
||||||
|
data: payload
|
||||||
|
})
|
||||||
|
const data = res.data || {}
|
||||||
|
this.totalCount = byteApiTotalCount(data)
|
||||||
|
this.items = byteApiItems(data)
|
||||||
|
if (res.code !== 200) {
|
||||||
|
this.$message.error(res.msg || '查询失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(e?.message || '查询失败')
|
||||||
|
} finally {
|
||||||
|
this.listLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPageSizeChange(size) {
|
||||||
|
const s = Number(size) || 10
|
||||||
|
this.filters.pageSize = LIST_PAGE_SIZE_OPTIONS.includes(s) ? s : 10
|
||||||
|
this.search(1)
|
||||||
|
},
|
||||||
|
resetFilters() {
|
||||||
|
this.filters = {
|
||||||
|
name: '',
|
||||||
|
groupIdsText: '',
|
||||||
|
groupType: 'AIGC',
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
sortBy: 'CreateTime',
|
||||||
|
sortOrder: 'Desc'
|
||||||
|
}
|
||||||
|
this.search(1)
|
||||||
|
},
|
||||||
|
async getDetail(item) {
|
||||||
|
const id = item?.Id || item?.id
|
||||||
|
if (!id) return
|
||||||
|
this.detailLoadingId = id
|
||||||
|
try {
|
||||||
|
const res = await this.$axios({
|
||||||
|
url: 'api/byteAssetGroup/getAssetGroup',
|
||||||
|
method: 'POST',
|
||||||
|
data: { id: id }
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.detailData = res.data || {}
|
||||||
|
this.detailVisible = true
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '查询详情失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(e?.message || '查询详情失败')
|
||||||
|
} finally {
|
||||||
|
this.detailLoadingId = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.asset-group-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 18px;
|
||||||
|
background: #121826;
|
||||||
|
color: rgba(255, 255, 255, 0.88);
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-panel {
|
||||||
|
background: rgba(22, 24, 30, 0.92);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.ag-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-form,
|
||||||
|
.ag-filter {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(180px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.ag-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-field label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-total {
|
||||||
|
margin: 6px 0 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-table-wrap {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-x: auto;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-table {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 列宽按百分比分配,随容器(100% 宽)随分辨率伸缩 */
|
||||||
|
.ag-col-id {
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
.ag-col-name {
|
||||||
|
width: 12%;
|
||||||
|
}
|
||||||
|
.ag-col-desc {
|
||||||
|
width: 24%;
|
||||||
|
}
|
||||||
|
.ag-col-type {
|
||||||
|
width: 8%;
|
||||||
|
}
|
||||||
|
.ag-col-project {
|
||||||
|
width: 12%;
|
||||||
|
}
|
||||||
|
.ag-col-time {
|
||||||
|
width: 11%;
|
||||||
|
}
|
||||||
|
.ag-col-action {
|
||||||
|
width: 12%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-table th,
|
||||||
|
.ag-table td {
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-table th {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-weight: 600;
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-empty {
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-pagination {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-detail {
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
color: #d8f4f7;
|
||||||
|
max-height: 420px;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.ag-form,
|
||||||
|
.ag-filter {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,932 @@
|
||||||
|
<template>
|
||||||
|
<div class="asset-manage-page">
|
||||||
|
<section class="asset-left">
|
||||||
|
<div class="asset-left-head">
|
||||||
|
<div class="panel-title">素材组树</div>
|
||||||
|
</div>
|
||||||
|
<div class="group-tree">
|
||||||
|
<div
|
||||||
|
v-for="g in groups"
|
||||||
|
:key="g.Id || g.id"
|
||||||
|
:class="['group-node', { active: selectedGroupId === (g.Id || g.id) }]"
|
||||||
|
@click="selectGroup(g)">
|
||||||
|
<span class="folder-icon">📁</span>
|
||||||
|
<div class="group-meta">
|
||||||
|
<div class="group-name">{{ g.Name || g.name || (g.Id || g.id) }}</div>
|
||||||
|
<div class="group-id">{{ g.Id || g.id }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!groups.length" class="empty-tip">暂无素材组,请先在“资源组管理”创建</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="asset-right">
|
||||||
|
<div class="asset-left-head">
|
||||||
|
<div class="panel-title">素材管理</div>
|
||||||
|
<div class="field actions">
|
||||||
|
<a-button type="primary" size="mini" @click="openCreateAssetDialog">新增素材</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-title mtop">查询素材</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="field">
|
||||||
|
<label>素材组</label>
|
||||||
|
<a-select
|
||||||
|
v-model="filters.groupId"
|
||||||
|
placeholder="请选择素材组"
|
||||||
|
allow-clear
|
||||||
|
:loading="groupLoading"
|
||||||
|
@change="onFilterGroupChange">
|
||||||
|
<a-option v-for="g in groups" :key="g.Id || g.id" :value="String(g.Id || g.id)">
|
||||||
|
{{ (g.Name || g.name || g.Id || g.id) + ' · ' + (g.Id || g.id) }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>素材名称</label>
|
||||||
|
<a-input v-model="filters.name" placeholder="按名称筛选" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>状态</label>
|
||||||
|
<a-select v-model="filters.status" placeholder="全部状态">
|
||||||
|
<a-option value="">全部</a-option>
|
||||||
|
<a-option value="Active">可用</a-option>
|
||||||
|
<a-option value="Processing">处理中</a-option>
|
||||||
|
<a-option value="Failed">失败</a-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>排序字段</label>
|
||||||
|
<a-select v-model="filters.sortBy">
|
||||||
|
<a-option value="CreateTime">创建时间</a-option>
|
||||||
|
<a-option value="UpdateTime">更新时间</a-option>
|
||||||
|
<a-option value="GroupId">素材组</a-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>排序方向</label>
|
||||||
|
<a-select v-model="filters.sortOrder">
|
||||||
|
<a-option value="Desc">从新到旧</a-option>
|
||||||
|
<a-option value="Asc">从旧到新</a-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
<div class="field actions">
|
||||||
|
<a-button type="primary" :loading="listLoading" @click="searchAssets(1)">查询</a-button>
|
||||||
|
<a-button @click="resetFilters">重置</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-spin :loading="listLoading">
|
||||||
|
<div class="total-line">总数:{{ totalCount }}</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table class="asset-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>素材编号</th>
|
||||||
|
<th>名称</th>
|
||||||
|
<th>访问地址</th>
|
||||||
|
<th>所属素材组</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>创建时间</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="it in items" :key="it.Id || it.id">
|
||||||
|
<td class="asset-td-id">{{ it.Id || it.id }}</td>
|
||||||
|
<td>{{ it.Name || it.name || '-' }}</td>
|
||||||
|
<td class="asset-url-cell">
|
||||||
|
<template v-if="assetUrlPreviewKind(it) === 'image' && !thumbError[it.Id || it.id]">
|
||||||
|
<a
|
||||||
|
:href="assetUrl(it)"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
class="asset-thumb-link">
|
||||||
|
<img
|
||||||
|
class="asset-url-thumb"
|
||||||
|
:src="assetUrl(it)"
|
||||||
|
alt=""
|
||||||
|
loading="lazy"
|
||||||
|
@error="onAssetThumbError(it)" />
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="assetUrlPreviewKind(it) === 'video'">
|
||||||
|
<video
|
||||||
|
class="asset-url-video"
|
||||||
|
:src="assetUrl(it)"
|
||||||
|
muted
|
||||||
|
playsinline
|
||||||
|
preload="metadata"
|
||||||
|
controls />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="assetUrl(it)">
|
||||||
|
<a :href="assetUrl(it)" target="_blank" rel="noreferrer" class="asset-url-plain">{{
|
||||||
|
assetUrl(it)
|
||||||
|
}}</a>
|
||||||
|
</template>
|
||||||
|
<span v-else class="asset-url-empty">-</span>
|
||||||
|
<div
|
||||||
|
v-if="assetUrlCaptionVisible(it)"
|
||||||
|
class="asset-url-caption"
|
||||||
|
:title="assetUrl(it)">
|
||||||
|
{{ assetUrl(it) }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{{ it.GroupId || it.groupId || '-' }}</td>
|
||||||
|
<td>{{ formatAssetTypeLabel(it) }}</td>
|
||||||
|
<td>{{ formatAssetStatusLabel(it) }}</td>
|
||||||
|
<td>{{ it.CreateTime || it.createTime || '-' }}</td>
|
||||||
|
<td>
|
||||||
|
<a-button size="mini" type="outline" @click="getAsset(it)">详情</a-button>
|
||||||
|
<a-button size="mini" status="danger" @click="deleteAsset(it)">删除</a-button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!items.length">
|
||||||
|
<td colspan="8" class="empty-tip">暂无素材</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="pager">
|
||||||
|
<a-pagination
|
||||||
|
:total="totalCount"
|
||||||
|
:current="filters.pageNumber"
|
||||||
|
:page-size="filters.pageSize"
|
||||||
|
show-total
|
||||||
|
show-jumper
|
||||||
|
show-page-size
|
||||||
|
:page-size-options="listPageSizeOptions"
|
||||||
|
@change="onAssetPageChange"
|
||||||
|
@page-size-change="onPageSizeChange" />
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<a-modal v-model:visible="detailVisible" title="素材详情" :footer="false" width="700px">
|
||||||
|
<pre class="detail-box">{{ detailText }}</pre>
|
||||||
|
</a-modal>
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="createAssetVisible"
|
||||||
|
title="新增素材"
|
||||||
|
width="520px"
|
||||||
|
:confirm-loading="createLoading"
|
||||||
|
@ok="createAsset">
|
||||||
|
<a-form :model="createForm" layout="horizontal" auto-label-width class="asset-create-modal-form">
|
||||||
|
<a-form-item label="素材组" required>
|
||||||
|
<a-select v-model="createForm.groupId" placeholder="请选择素材组" allow-clear>
|
||||||
|
<a-option v-for="g in groups" :key="g.Id || g.id" :value="String(g.Id || g.id)">
|
||||||
|
{{ g.Name || g.name || (g.Id || g.id) }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="上传文件" required>
|
||||||
|
<div
|
||||||
|
class="asset-file-dropzone"
|
||||||
|
:class="{ 'asset-file-dropzone--dragover': createUploadDragOver }"
|
||||||
|
@click="triggerCreateFilePicker"
|
||||||
|
@dragenter.prevent="onCreateUploadDragEnter"
|
||||||
|
@dragover.prevent="onCreateUploadDragOver"
|
||||||
|
@dragleave.prevent="onCreateUploadDragLeave"
|
||||||
|
@drop.prevent="onCreateUploadDrop">
|
||||||
|
<input
|
||||||
|
ref="createAssetFileInput"
|
||||||
|
class="asset-file-input"
|
||||||
|
type="file"
|
||||||
|
style="display: none"
|
||||||
|
@change="onFileChange" />
|
||||||
|
<div class="asset-file-dropzone-content">
|
||||||
|
<div class="asset-file-dropzone-icon">+</div>
|
||||||
|
<div class="asset-file-dropzone-text">点击选择或拖拽上传</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="createForm.previewUrl && createForm.assetType === 'Image'" class="asset-file-preview">
|
||||||
|
<img :src="createForm.previewUrl" alt="" />
|
||||||
|
</div>
|
||||||
|
<div class="asset-file-hint">{{ createForm.fileName || '未选择文件' }}</div>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="素材名称">
|
||||||
|
<a-input v-model="createForm.name" placeholder="素材名称(可选)" allow-clear />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="类型">
|
||||||
|
<a-select v-model="createForm.assetType">
|
||||||
|
<a-option value="Image">图片</a-option>
|
||||||
|
<a-option value="Video">视频</a-option>
|
||||||
|
<a-option value="Audio">音频</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { byteApiItems, byteApiTotalCount } from '@/utils/byteAssetApi'
|
||||||
|
import { extractUploadUrlFromResponse, PORTAL_TENCENT_COS_UPLOAD_URL, uploadFile } from '@/utils/file'
|
||||||
|
|
||||||
|
/** 素材树与筛选区素材组列表统一请求体 */
|
||||||
|
const GROUP_LIST_REQUEST_BODY = {
|
||||||
|
filter: { groupType: 'AIGC' },
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 100,
|
||||||
|
sortBy: 'CreateTime',
|
||||||
|
sortOrder: 'Desc'
|
||||||
|
}
|
||||||
|
|
||||||
|
const GROUP_LIST_API = 'api/byteAssetGroup/listAssetGroups'
|
||||||
|
const ASSET_CREATE_API = 'api/byteAsset/createAsset'
|
||||||
|
const ASSET_LIST_API = 'api/byteAsset/listAssets'
|
||||||
|
const ASSET_GET_API = 'api/byteAsset/getAsset'
|
||||||
|
const ASSET_DELETE_API = 'api/byteAsset/deleteAsset'
|
||||||
|
|
||||||
|
/** 列表页码上限(与接口分页一致);每页条数可选项 */
|
||||||
|
const LIST_PAGE_NUMBER_MAX = 100
|
||||||
|
const LIST_PAGE_SIZE_OPTIONS = [10, 20, 50, 100]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AssetManage',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
groupLoading: false,
|
||||||
|
createLoading: false,
|
||||||
|
listLoading: false,
|
||||||
|
createAssetVisible: false,
|
||||||
|
groups: [],
|
||||||
|
selectedGroupId: '',
|
||||||
|
createForm: {
|
||||||
|
groupId: '',
|
||||||
|
file: null,
|
||||||
|
fileName: '',
|
||||||
|
previewUrl: '',
|
||||||
|
localPreviewUrl: '',
|
||||||
|
name: '',
|
||||||
|
assetType: 'Image'
|
||||||
|
},
|
||||||
|
createUploadDragOver: false,
|
||||||
|
filters: {
|
||||||
|
groupId: '',
|
||||||
|
name: '',
|
||||||
|
status: '',
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
sortBy: 'CreateTime',
|
||||||
|
sortOrder: 'Desc'
|
||||||
|
},
|
||||||
|
totalCount: 0,
|
||||||
|
items: [],
|
||||||
|
detailVisible: false,
|
||||||
|
detailData: null,
|
||||||
|
listPageSizeOptions: LIST_PAGE_SIZE_OPTIONS,
|
||||||
|
thumbError: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
detailText() {
|
||||||
|
return this.detailData ? JSON.stringify(this.detailData, null, 2) : '{}'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadGroups()
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
createAssetVisible(val) {
|
||||||
|
if (!val) this.clearCreateUploadPreview()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clearCreateUploadPreview() {
|
||||||
|
if (this.createForm?.localPreviewUrl) {
|
||||||
|
try {
|
||||||
|
URL.revokeObjectURL(this.createForm.localPreviewUrl)
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
this.createForm.previewUrl = ''
|
||||||
|
this.createForm.localPreviewUrl = ''
|
||||||
|
},
|
||||||
|
setCreateFormFile(file) {
|
||||||
|
this.createForm.file = file || null
|
||||||
|
this.createForm.fileName = file?.name || ''
|
||||||
|
|
||||||
|
const mime = String(file?.type || '').toLowerCase()
|
||||||
|
// 根据文件类型自动设置素材类型,避免与文件不匹配
|
||||||
|
if (mime.startsWith('image/')) this.createForm.assetType = 'Image'
|
||||||
|
else if (mime.startsWith('video/')) this.createForm.assetType = 'Video'
|
||||||
|
else if (mime.startsWith('audio/')) this.createForm.assetType = 'Audio'
|
||||||
|
|
||||||
|
// 图片预览
|
||||||
|
this.clearCreateUploadPreview()
|
||||||
|
if (mime.startsWith('image/')) {
|
||||||
|
try {
|
||||||
|
this.createForm.localPreviewUrl = URL.createObjectURL(file)
|
||||||
|
this.createForm.previewUrl = this.createForm.localPreviewUrl
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatAssetTypeLabel(it) {
|
||||||
|
const t = String(it?.AssetType || it?.assetType || '').trim()
|
||||||
|
if (!t) return '-'
|
||||||
|
const map = {
|
||||||
|
Image: '图片',
|
||||||
|
Video: '视频',
|
||||||
|
Audio: '音频'
|
||||||
|
}
|
||||||
|
return map[t] || t
|
||||||
|
},
|
||||||
|
formatAssetStatusLabel(it) {
|
||||||
|
const s = String(it?.Status || it?.status || '').trim()
|
||||||
|
if (!s) return '-'
|
||||||
|
const map = {
|
||||||
|
Active: '可用',
|
||||||
|
Processing: '处理中',
|
||||||
|
Failed: '失败'
|
||||||
|
}
|
||||||
|
return map[s] || s
|
||||||
|
},
|
||||||
|
assetUrl(it) {
|
||||||
|
return String(it?.URL || it?.url || '').trim()
|
||||||
|
},
|
||||||
|
/** image | video | link */
|
||||||
|
assetUrlPreviewKind(it) {
|
||||||
|
const u = this.assetUrl(it)
|
||||||
|
if (!u || !/^https?:\/\//i.test(u)) return 'link'
|
||||||
|
const t = String(it?.AssetType || it?.assetType || '')
|
||||||
|
if (/image/i.test(t)) return 'image'
|
||||||
|
if (/video/i.test(t)) return 'video'
|
||||||
|
if (/audio/i.test(t)) return 'link'
|
||||||
|
if (/\.(png|jpe?g|gif|webp|bmp|svg)(\?|#|$)/i.test(u)) return 'image'
|
||||||
|
if (/\.(mp4|webm|mov|m4v|ogg)(\?|#|$)/i.test(u)) return 'video'
|
||||||
|
return 'link'
|
||||||
|
},
|
||||||
|
onAssetThumbError(it) {
|
||||||
|
const id = it?.Id || it?.id
|
||||||
|
if (id) this.thumbError = { ...this.thumbError, [id]: true }
|
||||||
|
},
|
||||||
|
assetUrlCaptionVisible(it) {
|
||||||
|
const u = this.assetUrl(it)
|
||||||
|
if (!u) return false
|
||||||
|
const k = this.assetUrlPreviewKind(it)
|
||||||
|
if (k === 'video') return true
|
||||||
|
if (k === 'image' && !this.thumbError[it?.Id || it?.id]) return true
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
clampAssetPage(n) {
|
||||||
|
const page = Number(n) || 1
|
||||||
|
const size = Number(this.filters.pageSize) || 10
|
||||||
|
let maxPage = LIST_PAGE_NUMBER_MAX
|
||||||
|
if (this.totalCount > 0) {
|
||||||
|
const totalPages = Math.ceil(this.totalCount / size)
|
||||||
|
maxPage = Math.min(LIST_PAGE_NUMBER_MAX, Math.max(1, totalPages))
|
||||||
|
}
|
||||||
|
return Math.min(Math.max(1, page), maxPage)
|
||||||
|
},
|
||||||
|
onAssetPageChange(page) {
|
||||||
|
this.searchAssets(this.clampAssetPage(page))
|
||||||
|
},
|
||||||
|
async openCreateAssetDialog() {
|
||||||
|
await this.loadGroups()
|
||||||
|
this.createForm.groupId = this.selectedGroupId || this.createForm.groupId || ''
|
||||||
|
this.createForm.file = null
|
||||||
|
this.createForm.fileName = ''
|
||||||
|
this.createForm.name = ''
|
||||||
|
this.createForm.assetType = 'Image'
|
||||||
|
this.clearCreateUploadPreview()
|
||||||
|
this.createAssetVisible = true
|
||||||
|
},
|
||||||
|
async loadGroups() {
|
||||||
|
this.groupLoading = true
|
||||||
|
try {
|
||||||
|
const res = await this.$axios({
|
||||||
|
url: GROUP_LIST_API,
|
||||||
|
method: 'POST',
|
||||||
|
data: { ...GROUP_LIST_REQUEST_BODY }
|
||||||
|
})
|
||||||
|
this.groups = byteApiItems(res?.data)
|
||||||
|
if (!this.selectedGroupId && this.groups.length) {
|
||||||
|
const gid = String(this.groups[0].Id || this.groups[0].id || '')
|
||||||
|
this.selectedGroupId = gid
|
||||||
|
this.createForm.groupId = gid
|
||||||
|
this.filters.groupId = gid
|
||||||
|
this.searchAssets(1)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(e?.message || '加载分组失败')
|
||||||
|
} finally {
|
||||||
|
this.groupLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectGroup(g) {
|
||||||
|
const gid = String(g?.Id || g?.id || '')
|
||||||
|
this.selectedGroupId = gid
|
||||||
|
this.createForm.groupId = gid
|
||||||
|
this.filters.groupId = gid
|
||||||
|
this.searchAssets(1)
|
||||||
|
},
|
||||||
|
onFilterGroupChange(val) {
|
||||||
|
const gid = val != null && val !== '' ? String(val) : ''
|
||||||
|
this.selectedGroupId = gid
|
||||||
|
this.createForm.groupId = gid
|
||||||
|
this.searchAssets(1)
|
||||||
|
},
|
||||||
|
triggerCreateFilePicker() {
|
||||||
|
const el = this.$refs.createAssetFileInput
|
||||||
|
if (el && typeof el.click === 'function') el.click()
|
||||||
|
},
|
||||||
|
onCreateUploadDragEnter() {
|
||||||
|
this.createUploadDragOver = true
|
||||||
|
},
|
||||||
|
onCreateUploadDragOver() {
|
||||||
|
this.createUploadDragOver = true
|
||||||
|
},
|
||||||
|
onCreateUploadDragLeave() {
|
||||||
|
this.createUploadDragOver = false
|
||||||
|
},
|
||||||
|
onCreateUploadDrop(e) {
|
||||||
|
this.createUploadDragOver = false
|
||||||
|
const file = e?.dataTransfer?.files?.[0]
|
||||||
|
if (!file) return
|
||||||
|
this.setCreateFormFile(file)
|
||||||
|
},
|
||||||
|
onFileChange(e) {
|
||||||
|
const file = e?.target?.files?.[0]
|
||||||
|
this.setCreateFormFile(file)
|
||||||
|
},
|
||||||
|
async createAsset() {
|
||||||
|
const groupId = String(this.createForm.groupId || '').trim()
|
||||||
|
if (!groupId) return this.$message.error('请选择素材组')
|
||||||
|
if (!this.createForm.file) return this.$message.error('请选择上传文件')
|
||||||
|
this.createLoading = true
|
||||||
|
try {
|
||||||
|
// 1) 先走 /api/file/upload 拿到上传后的 url
|
||||||
|
const uploadRes = await uploadFile({
|
||||||
|
url: PORTAL_TENCENT_COS_UPLOAD_URL,
|
||||||
|
file: this.createForm.file,
|
||||||
|
name: 'file'
|
||||||
|
})
|
||||||
|
const uploadUrl = extractUploadUrlFromResponse(uploadRes)
|
||||||
|
if (!uploadUrl) throw new Error(uploadRes?.msg || '上传后未返回文件地址')
|
||||||
|
|
||||||
|
// 图片上传成功后,在上传组件展示远程预览(替换本地预览)
|
||||||
|
if (this.createForm.assetType === 'Image') {
|
||||||
|
this.clearCreateUploadPreview()
|
||||||
|
this.createForm.previewUrl = uploadUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 再通过 /api/byteAsset/createAsset(JSON) 用 url 创建素材
|
||||||
|
const res = await this.$axios({
|
||||||
|
url: ASSET_CREATE_API,
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
URL: uploadUrl,
|
||||||
|
groupId,
|
||||||
|
assetType: this.createForm.assetType,
|
||||||
|
name: String(this.createForm.name || '').trim()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.$message.success(this.createForm.assetType === 'Image' ? '上传图片成功' : '上传成功')
|
||||||
|
this.createForm.file = null
|
||||||
|
this.createForm.name = ''
|
||||||
|
const isImage = this.createForm.assetType === 'Image'
|
||||||
|
if (isImage) {
|
||||||
|
// 图片模式下延迟关闭,避免用户来不及看到上传组件缩略图
|
||||||
|
setTimeout(() => {
|
||||||
|
this.createAssetVisible = false
|
||||||
|
}, 500)
|
||||||
|
} else {
|
||||||
|
this.createAssetVisible = false
|
||||||
|
}
|
||||||
|
this.searchAssets(1)
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '新增素材失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(e?.message || '新增素材失败')
|
||||||
|
} finally {
|
||||||
|
this.createLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buildListPayload() {
|
||||||
|
const filter = {
|
||||||
|
groupType: 'AIGC'
|
||||||
|
}
|
||||||
|
const gid = String(this.filters.groupId || '').trim()
|
||||||
|
if (gid) filter.groupIds = [gid]
|
||||||
|
const name = String(this.filters.name || '').trim()
|
||||||
|
if (name) filter.name = name
|
||||||
|
if (this.filters.status) filter.statuses = [this.filters.status]
|
||||||
|
return {
|
||||||
|
filter,
|
||||||
|
pageNumber: this.filters.pageNumber,
|
||||||
|
pageSize: this.filters.pageSize,
|
||||||
|
sortBy: this.filters.sortBy,
|
||||||
|
sortOrder: this.filters.sortOrder
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async searchAssets(page = this.filters.pageNumber) {
|
||||||
|
this.filters.pageNumber = this.clampAssetPage(page)
|
||||||
|
this.listLoading = true
|
||||||
|
this.thumbError = {}
|
||||||
|
try {
|
||||||
|
const res = await this.$axios({
|
||||||
|
url: ASSET_LIST_API,
|
||||||
|
method: 'POST',
|
||||||
|
data: this.buildListPayload()
|
||||||
|
})
|
||||||
|
this.totalCount = byteApiTotalCount(res?.data)
|
||||||
|
this.items = byteApiItems(res?.data)
|
||||||
|
if (res.code !== 200) {
|
||||||
|
this.$message.error(res.msg || '查询素材失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(e?.message || '查询素材失败')
|
||||||
|
} finally {
|
||||||
|
this.listLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPageSizeChange(size) {
|
||||||
|
const s = Number(size) || 10
|
||||||
|
this.filters.pageSize = LIST_PAGE_SIZE_OPTIONS.includes(s) ? s : 10
|
||||||
|
this.searchAssets(1)
|
||||||
|
},
|
||||||
|
resetFilters() {
|
||||||
|
const gid = this.groups.length
|
||||||
|
? String(this.groups[0].Id || this.groups[0].id || '')
|
||||||
|
: String(this.selectedGroupId || '')
|
||||||
|
this.selectedGroupId = gid
|
||||||
|
this.createForm.groupId = gid
|
||||||
|
this.filters = {
|
||||||
|
groupId: gid,
|
||||||
|
name: '',
|
||||||
|
status: '',
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
sortBy: 'CreateTime',
|
||||||
|
sortOrder: 'Desc'
|
||||||
|
}
|
||||||
|
this.searchAssets(1)
|
||||||
|
},
|
||||||
|
async getAsset(it) {
|
||||||
|
const id = it?.Id || it?.id
|
||||||
|
if (!id) return
|
||||||
|
try {
|
||||||
|
const res = await this.$axios({
|
||||||
|
url: ASSET_GET_API,
|
||||||
|
method: 'POST',
|
||||||
|
data: { id: id }
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.detailData = res.data || {}
|
||||||
|
this.detailVisible = true
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '查询详情失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(e?.message || '查询详情失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteAsset(it) {
|
||||||
|
const id = it?.Id || it?.id
|
||||||
|
if (!id) return
|
||||||
|
this.$confirm({
|
||||||
|
title: '删除素材',
|
||||||
|
content: `确认删除素材 ${id} 吗?`,
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
const res = await this.$axios({
|
||||||
|
url: ASSET_DELETE_API,
|
||||||
|
method: 'POST',
|
||||||
|
data: { id: id }
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.$message.success('删除成功')
|
||||||
|
this.searchAssets(this.filters.pageNumber)
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '删除失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(e?.message || '删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.asset-manage-page {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 300px 1fr;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 16px;
|
||||||
|
min-height: 100%;
|
||||||
|
background: #121826;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
.asset-left,
|
||||||
|
.asset-right {
|
||||||
|
background: rgba(22, 24, 30, 0.92);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
.asset-right {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.panel-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.asset-left-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.group-tree {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
max-height: 72vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.group-node {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.group-node:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
.group-node.active {
|
||||||
|
border-color: rgba(0, 202, 224, 0.45);
|
||||||
|
background: rgba(0, 202, 224, 0.12);
|
||||||
|
}
|
||||||
|
.folder-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.group-name {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.group-id {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.55);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(180px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.field label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
}
|
||||||
|
.field.actions {
|
||||||
|
align-self: end;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.mtop {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
.total-line {
|
||||||
|
margin: 10px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
}
|
||||||
|
.table-wrap {
|
||||||
|
overflow: auto;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.asset-table {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.asset-table th:nth-child(1),
|
||||||
|
.asset-table td:nth-child(1) {
|
||||||
|
width: 88px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.asset-table th:nth-child(2),
|
||||||
|
.asset-table td:nth-child(2) {
|
||||||
|
width: 14%;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.asset-table th:nth-child(3),
|
||||||
|
.asset-table td:nth-child(3) {
|
||||||
|
width: 30%;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
.asset-table th:nth-child(4),
|
||||||
|
.asset-table td:nth-child(4) {
|
||||||
|
width: 10%;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.asset-table th:nth-child(5),
|
||||||
|
.asset-table td:nth-child(5) {
|
||||||
|
width: 9%;
|
||||||
|
}
|
||||||
|
.asset-table th:nth-child(6),
|
||||||
|
.asset-table td:nth-child(6) {
|
||||||
|
width: 9%;
|
||||||
|
}
|
||||||
|
.asset-table th:nth-child(7),
|
||||||
|
.asset-table td:nth-child(7) {
|
||||||
|
width: 14%;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.asset-table th:nth-child(8),
|
||||||
|
.asset-table td:nth-child(8) {
|
||||||
|
width: 16%;
|
||||||
|
}
|
||||||
|
.asset-table th,
|
||||||
|
.asset-table td {
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.asset-table th {
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
.asset-url-cell {
|
||||||
|
min-width: 0;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.asset-thumb-link {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100%;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.asset-url-thumb {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 120px;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
.asset-url-video {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 160px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
.asset-url-plain {
|
||||||
|
color: rgba(0, 202, 224, 0.95);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.asset-url-empty {
|
||||||
|
color: rgba(255, 255, 255, 0.35);
|
||||||
|
}
|
||||||
|
.asset-url-caption {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.pager {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.empty-tip {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
.detail-box {
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
border-radius: 8px;
|
||||||
|
max-height: 420px;
|
||||||
|
overflow: auto;
|
||||||
|
color: #d8f4f7;
|
||||||
|
}
|
||||||
|
@media (max-width: 980px) {
|
||||||
|
.asset-manage-page {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
/* 弹层挂载到 body,与深色页搭配的标签可读性 */
|
||||||
|
.asset-create-modal-form.arco-form .arco-form-item-label-col .arco-form-item-label,
|
||||||
|
.ag-create-modal-form.arco-form .arco-form-item-label-col .arco-form-item-label {
|
||||||
|
color: var(--color-text-2, rgba(255, 255, 255, 0.78));
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-file-input {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 4px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-file-dropzone {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px dashed rgba(255, 255, 255, 0.18);
|
||||||
|
background: rgba(0, 0, 0, 0.18);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease-in-out;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-file-dropzone--dragover {
|
||||||
|
border-color: rgba(0, 202, 224, 0.65);
|
||||||
|
background: rgba(0, 202, 224, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-file-dropzone-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-file-dropzone-icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(0, 202, 224, 0.35);
|
||||||
|
background: rgba(0, 202, 224, 0.08);
|
||||||
|
color: rgba(0, 202, 224, 0.8);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-file-dropzone-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-file-preview {
|
||||||
|
margin-top: 10px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 160px;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-file-preview img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-file-hint {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -240,14 +240,10 @@ export default {
|
||||||
this.imageUrl = res.msg
|
this.imageUrl = res.msg
|
||||||
this.showResult = true
|
this.showResult = true
|
||||||
} else if (res.code == -1) {
|
} else if (res.code == -1) {
|
||||||
this.$confirm({
|
this.$modal.error({
|
||||||
title: this.$t('common.notice'),
|
title: this.$t('common.notice'),
|
||||||
content: this.$t('common.balenceLow'),
|
content: this.$t('common.balenceLow'),
|
||||||
okText: this.$t('common.confirm'),
|
okText: this.$t('common.confirm')
|
||||||
cancelText: this.$t('common.cancel'),
|
|
||||||
onOk: (_) => {
|
|
||||||
this.$router.push('/recharge')
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
} else if (res.code == -2) {
|
} else if (res.code == -2) {
|
||||||
this.$modal.error({
|
this.$modal.error({
|
||||||
|
|
|
||||||
|
|
@ -186,14 +186,10 @@ export default {
|
||||||
this.showResult = true
|
this.showResult = true
|
||||||
this.imageUrl = res.msg
|
this.imageUrl = res.msg
|
||||||
} else if (res.code == -1) {
|
} else if (res.code == -1) {
|
||||||
this.$confirm({
|
this.$modal.error({
|
||||||
title: this.$t('common.notice'),
|
title: this.$t('common.notice'),
|
||||||
content: this.$t('common.balenceLow'),
|
content: this.$t('common.balenceLow'),
|
||||||
okText: this.$t('common.confirm'),
|
okText: this.$t('common.confirm')
|
||||||
cancelText: this.$t('common.cancel'),
|
|
||||||
onOk: (_) => {
|
|
||||||
this.$router.push('/recharge')
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
} else if (res.code == -2) {
|
} else if (res.code == -2) {
|
||||||
this.$modal.error({
|
this.$modal.error({
|
||||||
|
|
|
||||||
|
|
@ -68,11 +68,28 @@
|
||||||
</a-radio>
|
</a-radio>
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="text">
|
<!-- 模型选择 -->
|
||||||
<a-textarea
|
<div class="model-select">
|
||||||
v-model="text"
|
<div class="model-title">{{ $t('common.selectModel') || '选择模型' }}</div>
|
||||||
:placeholder="$t('common.textVideoPlaceholder')" />
|
<a-select v-model="selectedModel" style="width: 100%;">
|
||||||
</div> -->
|
<a-option
|
||||||
|
v-for="option in modelOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:value="option.value">
|
||||||
|
{{ option.label }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 富文本编辑器 -->
|
||||||
|
<RichTextEditor
|
||||||
|
v-model="editorContent"
|
||||||
|
:placeholder="$t('common.textVideoPlaceholder') || '请输入文本生成视频...'"
|
||||||
|
:uploaded-images="uploadedImages"
|
||||||
|
@text-change="handleTextChange"
|
||||||
|
@image-upload="handleImageUpload"
|
||||||
|
/>
|
||||||
|
|
||||||
<mf-button
|
<mf-button
|
||||||
class="submit"
|
class="submit"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|
@ -175,6 +192,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
|
import RichTextEditor from '@/components/RichTextEditor.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -182,6 +201,7 @@ export default {
|
||||||
firstUrl: '',
|
firstUrl: '',
|
||||||
lastUrl: '',
|
lastUrl: '',
|
||||||
text: '',
|
text: '',
|
||||||
|
editorContent: '',
|
||||||
interval: null,
|
interval: null,
|
||||||
videoUrl: null,
|
videoUrl: null,
|
||||||
videoLoading: false,
|
videoLoading: false,
|
||||||
|
|
@ -202,9 +222,20 @@ export default {
|
||||||
templateLoading: false,
|
templateLoading: false,
|
||||||
selectedTemplate: null,
|
selectedTemplate: null,
|
||||||
selectedTemplatePreview: '', // 选中的模版预览图URL
|
selectedTemplatePreview: '', // 选中的模版预览图URL
|
||||||
maxPollAttempts: 40 // 最大轮询次数(3秒 * 40 = 120秒超时)
|
maxPollAttempts: 40, // 最大轮询次数(3秒 * 40 = 120秒超时)
|
||||||
|
// 模型选择
|
||||||
|
modelOptions: [
|
||||||
|
{ label: 'Seedance 2.0', value: 'ep-20260326165811-dlkth' },
|
||||||
|
{ label: 'Seedance 2.0 Fast', value: 'ep-20260326170056-dkj9m' }
|
||||||
|
],
|
||||||
|
selectedModel: 'ep-20260326165811-dlkth',
|
||||||
|
// 已上传的图片列表(用于@功能)
|
||||||
|
uploadedImages: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
components: {
|
||||||
|
RichTextEditor
|
||||||
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
this.destroyInterval()
|
this.destroyInterval()
|
||||||
},
|
},
|
||||||
|
|
@ -316,8 +347,25 @@ export default {
|
||||||
// 繁体中文显示 chineseContent,其他语言显示 englishContent
|
// 繁体中文显示 chineseContent,其他语言显示 englishContent
|
||||||
if (this.lang === 'zh_HK') {
|
if (this.lang === 'zh_HK') {
|
||||||
return template.chineseContent || template.name || ''
|
return template.chineseContent || template.name || ''
|
||||||
}
|
}else{
|
||||||
return template.englishContent || template.name || ''
|
return template.englishContent || template.name || ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 处理富文本内容变化
|
||||||
|
handleTextChange(content) {
|
||||||
|
this.editorContent = content
|
||||||
|
// 同步到旧的 text 字段以保持兼容性
|
||||||
|
this.text = content
|
||||||
|
},
|
||||||
|
// 处理上传的图片(用于@功能)
|
||||||
|
handleImageUpload(imageInfo) {
|
||||||
|
if (imageInfo && imageInfo.url) {
|
||||||
|
this.uploadedImages.push({
|
||||||
|
url: imageInfo.url,
|
||||||
|
name: imageInfo.name || 'image'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
// 确认选择模板
|
// 确认选择模板
|
||||||
handleConfirmTemplate() {
|
handleConfirmTemplate() {
|
||||||
|
|
@ -477,9 +525,10 @@ export default {
|
||||||
|
|
||||||
this.generateLoading = true
|
this.generateLoading = true
|
||||||
let params = {
|
let params = {
|
||||||
text: this.text,
|
text: this.editorContent || this.text,
|
||||||
firstUrl: firstImageUrl,
|
firstUrl: firstImageUrl,
|
||||||
functionType: '21'
|
functionType: '21',
|
||||||
|
model: this.selectedModel // 新增模型参数
|
||||||
}
|
}
|
||||||
if (this.lastUrl && this.lastUrl.url) {
|
if (this.lastUrl && this.lastUrl.url) {
|
||||||
params.lastUrl = this.lastUrl.url
|
params.lastUrl = this.lastUrl.url
|
||||||
|
|
@ -516,14 +565,10 @@ export default {
|
||||||
this.$store.dispatch('main/setPrevent', true)
|
this.$store.dispatch('main/setPrevent', true)
|
||||||
this.getVideo(res.data.id)
|
this.getVideo(res.data.id)
|
||||||
} else if (res.code == -1) {
|
} else if (res.code == -1) {
|
||||||
this.$confirm({
|
this.$modal.error({
|
||||||
title: this.$t('common.notice'),
|
title: this.$t('common.notice'),
|
||||||
content: this.$t('common.balenceLow'),
|
content: this.$t('common.balenceLow'),
|
||||||
okText: this.$t('common.confirm'),
|
okText: this.$t('common.confirm')
|
||||||
cancelText: this.$t('common.cancel'),
|
|
||||||
onOk: (_) => {
|
|
||||||
this.$router.push('/recharge')
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
} else if (res.code == -2) {
|
} else if (res.code == -2) {
|
||||||
this.$modal.error({
|
this.$modal.error({
|
||||||
|
|
@ -603,6 +648,74 @@ export default {
|
||||||
|
|
||||||
.upload {
|
.upload {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.model-select {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.model-title {
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-select) {
|
||||||
|
background-color: #1a1b20;
|
||||||
|
border-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 富文本编辑器样式 */
|
||||||
|
.rich-editor-root {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #1a1b20;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
|
||||||
|
.tool-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background-color: rgba(255,255,255,0.05);
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
border-color: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-editor {
|
||||||
|
min-height: 160px;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #1a1b20;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: rgb(var(--primary-6));
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-placeholder]:empty::before {
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
color: #5c5d68;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
&-title {
|
&-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,995 @@
|
||||||
|
<template>
|
||||||
|
<div class="generated-assets-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="panel-title">作品库</div>
|
||||||
|
</div>
|
||||||
|
<div class="view-tabs">
|
||||||
|
<a-tabs v-model="activeTab" @change="handleTabChange" type="card">
|
||||||
|
<a-tab-pane key="personal" title="个人" />
|
||||||
|
<a-tab-pane key="department" title="团队" />
|
||||||
|
</a-tabs>
|
||||||
|
</div>
|
||||||
|
<!-- 查询区域 -->
|
||||||
|
<div class="query-section">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="field">
|
||||||
|
<label>收藏状态</label>
|
||||||
|
<a-select v-model="filters.isTop" clearable placeholder="全部">
|
||||||
|
<a-option :value="null">全部</a-option>
|
||||||
|
<a-option value="Y">已收藏</a-option>
|
||||||
|
<a-option value="N">未收藏</a-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>开始时间</label>
|
||||||
|
<a-date-picker v-model="filters.beginTime" placeholder="选择开始时间" style="width: 100%" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>结束时间</label>
|
||||||
|
<a-date-picker v-model="filters.endTime" placeholder="选择结束时间" style="width: 100%" />
|
||||||
|
</div>
|
||||||
|
<div class="field actions">
|
||||||
|
<a-button type="primary" :loading="loading" @click="search(1)">查询</a-button>
|
||||||
|
<a-button @click="resetFilters">重置</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表格区域 -->
|
||||||
|
<a-spin :loading="loading">
|
||||||
|
<div class="total-line">总数:{{ pagination.total }}</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table class="asset-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>订单编号</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>模型参数</th>
|
||||||
|
<th>生成结果</th>
|
||||||
|
<th>创建时间</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="item in dataList" :key="item.id">
|
||||||
|
<td class="td-id">{{ item.id }}</td>
|
||||||
|
<td>{{ item.orderNum || '-' }}</td>
|
||||||
|
<td>{{ formatType(item.type) }}</td>
|
||||||
|
<td>
|
||||||
|
<a-tag :color="getStatusColor(item.status)">
|
||||||
|
{{ formatStatus(item.status) }}
|
||||||
|
</a-tag>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="params-cell">
|
||||||
|
<div v-if="item.model" class="param-tag">模型: {{ item.model }}</div>
|
||||||
|
<div v-if="item.duration" class="param-tag">时长: {{ item.duration }}s</div>
|
||||||
|
<div v-if="item.resolution" class="param-tag">分辨率: {{ item.resolution }}</div>
|
||||||
|
<div v-if="item.ratio" class="param-tag">比例: {{ item.ratio }}</div>
|
||||||
|
<div v-if="item.mode" class="param-tag">模式: {{ formatMode(item.mode) }}</div>
|
||||||
|
<div v-if="!item.model && !item.duration && !item.resolution && !item.ratio && !item.mode" class="param-empty">-</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="result-cell">
|
||||||
|
<div v-if="isVideoResult(item.result)" class="media-preview">
|
||||||
|
<video
|
||||||
|
class="video-thumb"
|
||||||
|
:src="item.result"
|
||||||
|
controls
|
||||||
|
preload="metadata"
|
||||||
|
@click.stop="openPreview(item.result, 'video')" />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="isImageResult(item.result)" class="media-preview">
|
||||||
|
<img
|
||||||
|
class="image-thumb"
|
||||||
|
:src="item.result"
|
||||||
|
@click.stop="openPreview(item.result, 'image')" />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="item.result && item.result.startsWith('cgt-')" class="task-id">
|
||||||
|
{{ item.result }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="result-empty">-</div>
|
||||||
|
</td>
|
||||||
|
<td>{{ item.createTime || '-' }}</td>
|
||||||
|
<td>
|
||||||
|
<a-button
|
||||||
|
size="mini"
|
||||||
|
:type="item.isTop === 'Y' ? 'primary' : 'outline'"
|
||||||
|
:status="item.isTop === 'Y' ? 'success' : 'default'"
|
||||||
|
@click="toggleFavorite(item)">
|
||||||
|
<template v-if="item.isTop === 'Y'">
|
||||||
|
<a-icon name="star-fill" /> 已收藏
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a-icon name="star" /> 收藏
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
size="mini"
|
||||||
|
type="outline"
|
||||||
|
@click="viewDetail(item)">
|
||||||
|
详情
|
||||||
|
</a-button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!dataList.length">
|
||||||
|
<td colspan="8" class="empty-tip">暂无数据</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="pager">
|
||||||
|
<a-pagination
|
||||||
|
:total="pagination.total"
|
||||||
|
:current="pagination.pageNum"
|
||||||
|
:page-size="pagination.pageSize"
|
||||||
|
show-total
|
||||||
|
show-jumper
|
||||||
|
show-page-size
|
||||||
|
:page-size-options="pagination.pageSizes"
|
||||||
|
@change="changePage"
|
||||||
|
@page-size-change="changePageSize" />
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
|
||||||
|
<!-- 详情弹窗 -->
|
||||||
|
<a-modal v-model:visible="detailVisible" title="订单详情" :footer="false" width="850px">
|
||||||
|
<div class="detail-content" v-if="detailData">
|
||||||
|
<div class="detail-form">
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<div class="detail-group">
|
||||||
|
<div class="group-title">基本信息</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">订单编号</div>
|
||||||
|
<div class="value">{{ detailData.orderNum || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">功能类型</div>
|
||||||
|
<div class="value">{{ formatType(detailData.type) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">扣除积分</div>
|
||||||
|
<div class="value">{{ detailData.amount ? detailData.amount + ' 积分' : '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">订单状态</div>
|
||||||
|
<div class="value">
|
||||||
|
<a-tag :color="getStatusColor(detailData.status)">
|
||||||
|
{{ formatStatus(detailData.status) }}
|
||||||
|
</a-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">执行状态</div>
|
||||||
|
<div class="value">{{ formatExtStatus(detailData.extStatus) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">收藏状态</div>
|
||||||
|
<div class="value">
|
||||||
|
<a-tag v-if="detailData.isTop === 'Y'" color="orange">已收藏</a-tag>
|
||||||
|
<span v-else class="text-muted">未收藏</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">请求来源</div>
|
||||||
|
<div class="value">{{ detailData.source || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 生成参数 -->
|
||||||
|
<div class="detail-group">
|
||||||
|
<div class="group-title">生成参数</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">提示词</div>
|
||||||
|
<div class="value long-text">{{ detailData.text || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">生成模式</div>
|
||||||
|
<div class="value">{{ formatMode(detailData.mode) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">使用模型</div>
|
||||||
|
<div class="value">{{ detailData.model || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">视频时长</div>
|
||||||
|
<div class="value">{{ detailData.duration ? detailData.duration + ' 秒' : '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">分辨率</div>
|
||||||
|
<div class="value">{{ detailData.resolution || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">画面比例</div>
|
||||||
|
<div class="value">{{ detailData.ratio || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 生成结果 -->
|
||||||
|
<div class="detail-group">
|
||||||
|
<div class="group-title">生成结果</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">生成内容</div>
|
||||||
|
<div class="value media-preview">
|
||||||
|
<video
|
||||||
|
v-if="isVideoResult(detailData.result)"
|
||||||
|
class="detail-video"
|
||||||
|
:src="detailData.result"
|
||||||
|
controls
|
||||||
|
preload="metadata" />
|
||||||
|
<img
|
||||||
|
v-else-if="isImageResult(detailData.result)"
|
||||||
|
class="detail-image"
|
||||||
|
:src="detailData.result"
|
||||||
|
@click="viewImageFull(detailData.result)" />
|
||||||
|
<div v-else class="result-text">{{ detailData.result || '无结果' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row" v-if="detailData.img1">
|
||||||
|
<div class="label">首帧图片</div>
|
||||||
|
<div class="value media-preview">
|
||||||
|
<img class="detail-image small" :src="detailData.img1" @click="viewImageFull(detailData.img1)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row" v-if="detailData.img2">
|
||||||
|
<div class="label">尾帧图片</div>
|
||||||
|
<div class="value media-preview">
|
||||||
|
<img class="detail-image small" :src="detailData.img2" @click="viewImageFull(detailData.img2)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 完整参数 -->
|
||||||
|
<div class="detail-group" v-if="detailData.videoParams">
|
||||||
|
<div class="group-title">完整参数</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">JSON参数</div>
|
||||||
|
<div class="value">
|
||||||
|
<pre class="json-block">{{ formatVideoParams(detailData.videoParams) }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 时间信息 -->
|
||||||
|
<div class="detail-group">
|
||||||
|
<div class="group-title">时间信息</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">创建时间</div>
|
||||||
|
<div class="value">{{ detailData.createTime || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">更新时间</div>
|
||||||
|
<div class="value">{{ detailData.updateTime || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 预览弹窗 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="previewVisible"
|
||||||
|
:title="previewType === 'video' ? '视频预览' : '图片预览'"
|
||||||
|
:footer="false"
|
||||||
|
width="800px"
|
||||||
|
@cancel="closePreview">
|
||||||
|
<div class="preview-content">
|
||||||
|
<video
|
||||||
|
v-if="previewType === 'video' && previewUrl"
|
||||||
|
class="preview-video"
|
||||||
|
:src="previewUrl"
|
||||||
|
controls
|
||||||
|
autoplay />
|
||||||
|
<img
|
||||||
|
v-else-if="previewType === 'image' && previewUrl"
|
||||||
|
class="preview-image"
|
||||||
|
:src="previewUrl" />
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'GeneratedAssets',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
dataList: [],
|
||||||
|
filters: {
|
||||||
|
isTop: null,
|
||||||
|
beginTime: '',
|
||||||
|
endTime: ''
|
||||||
|
},
|
||||||
|
activeTab: 'personal', // 'personal'个人, 'department'团队
|
||||||
|
pagination: {
|
||||||
|
total: 0,
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
pageSizes: [10, 20, 50, 100]
|
||||||
|
},
|
||||||
|
detailVisible: false,
|
||||||
|
detailData: null,
|
||||||
|
previewVisible: false,
|
||||||
|
previewUrl: '',
|
||||||
|
previewType: 'video'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.search()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 查询数据
|
||||||
|
search(page = 1) {
|
||||||
|
this.pagination.pageNum = page
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
pageNum: this.pagination.pageNum,
|
||||||
|
pageSize: this.pagination.pageSize
|
||||||
|
}
|
||||||
|
console.log("this.activeTab", this.activeTab)
|
||||||
|
// 视图模式参数:personal = 个人, department = 团队视图(传 dept=true)
|
||||||
|
if (this.activeTab === 'department') {
|
||||||
|
params.dept = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 明确传递所有查询条件 - 使用 is_top 作为参数名
|
||||||
|
if (this.filters.isTop != null && String(this.filters.isTop) !== '') {
|
||||||
|
// 如果isTop是null或者空字符串或者全部,则不传递
|
||||||
|
if (this.filters.isTop === null || this.filters.isTop === '' || this.filters.isTop === '全部') {
|
||||||
|
delete params.is_top
|
||||||
|
} else {
|
||||||
|
params.is_top = String(this.filters.isTop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.filters.beginTime) {
|
||||||
|
params.beginTime = this.formatDate(this.filters.beginTime)
|
||||||
|
}
|
||||||
|
if (this.filters.endTime) {
|
||||||
|
params.endTime = this.formatDate(this.filters.endTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.$axios({
|
||||||
|
url: 'api/portal/assets/list',
|
||||||
|
method: 'GET',
|
||||||
|
data: params
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
this.loading = false
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.dataList = res.rows || []
|
||||||
|
this.pagination.total = res.total || 0
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '查询失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.loading = false
|
||||||
|
this.$message.error(err?.message || '查询失败')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 重置筛选
|
||||||
|
resetFilters() {
|
||||||
|
this.filters.isTop = null
|
||||||
|
this.filters.beginTime = ''
|
||||||
|
this.filters.endTime = ''
|
||||||
|
this.search(1)
|
||||||
|
},
|
||||||
|
// Tabs 切换处理(personal/部门视图)
|
||||||
|
handleTabChange(tabKey) {
|
||||||
|
if (tabKey) {
|
||||||
|
this.activeTab = tabKey
|
||||||
|
}
|
||||||
|
this.pagination.pageNum = 1
|
||||||
|
this.search(1)
|
||||||
|
},
|
||||||
|
// 分页变化
|
||||||
|
changePage(page) {
|
||||||
|
this.pagination.pageNum = page
|
||||||
|
this.search(page)
|
||||||
|
},
|
||||||
|
// 每页条数变化
|
||||||
|
changePageSize(pageSize) {
|
||||||
|
this.pagination.pageSize = pageSize
|
||||||
|
this.pagination.pageNum = 1
|
||||||
|
this.search(1)
|
||||||
|
},
|
||||||
|
// 格式化日期
|
||||||
|
formatDate(date) {
|
||||||
|
if (!date) return ''
|
||||||
|
const d = new Date(date)
|
||||||
|
const year = d.getFullYear()
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(d.getDate()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
},
|
||||||
|
// 格式化类型
|
||||||
|
formatType(type) {
|
||||||
|
const typeMap = {
|
||||||
|
'11': '图生图',
|
||||||
|
'12': '图生图2',
|
||||||
|
'13': '换脸',
|
||||||
|
'1': '快速生图',
|
||||||
|
'21': '快速生视频'
|
||||||
|
}
|
||||||
|
return typeMap[String(type)] || type || '-'
|
||||||
|
},
|
||||||
|
// 格式化状态
|
||||||
|
formatStatus(status) {
|
||||||
|
const statusMap = {
|
||||||
|
0: '进行中',
|
||||||
|
1: '已完成',
|
||||||
|
2: '失败'
|
||||||
|
}
|
||||||
|
return statusMap[status] || status || '-'
|
||||||
|
},
|
||||||
|
// 格式化扩展状态
|
||||||
|
formatExtStatus(extStatus) {
|
||||||
|
const statusMap = {
|
||||||
|
'running': '执行中',
|
||||||
|
'queued': '队列中',
|
||||||
|
'succeeded': '已完成',
|
||||||
|
'failed': '失败',
|
||||||
|
'cancelled': '已取消',
|
||||||
|
'expired': '超时'
|
||||||
|
}
|
||||||
|
return statusMap[extStatus] || '未知'
|
||||||
|
},
|
||||||
|
// 获取状态颜色
|
||||||
|
getStatusColor(status) {
|
||||||
|
const colorMap = {
|
||||||
|
0: 'blue',
|
||||||
|
1: 'green',
|
||||||
|
2: 'red'
|
||||||
|
}
|
||||||
|
return colorMap[status] || 'default'
|
||||||
|
},
|
||||||
|
// 格式化模式
|
||||||
|
formatMode(mode) {
|
||||||
|
const modeMap = {
|
||||||
|
'text-to-video': '文生视频',
|
||||||
|
'image-first-frame': '图生视频·首帧',
|
||||||
|
'image-first-last-frame': '图生视频·首尾帧',
|
||||||
|
'image-reference': '图生视频·参考图'
|
||||||
|
}
|
||||||
|
return modeMap[mode] || mode || '-'
|
||||||
|
},
|
||||||
|
// 格式化videoParams
|
||||||
|
formatVideoParams(params) {
|
||||||
|
if (!params) return ''
|
||||||
|
try {
|
||||||
|
const obj = typeof params === 'string' ? JSON.parse(params) : params
|
||||||
|
return JSON.stringify(obj, null, 2)
|
||||||
|
} catch (e) {
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 判断是否为视频结果
|
||||||
|
isVideoResult(url) {
|
||||||
|
if (!url) return false
|
||||||
|
return /\.(mp4|mov|webm|ogg|m4v|avi|mkv)(\?.*)?$/i.test(url)
|
||||||
|
},
|
||||||
|
// 判断是否为图片结果
|
||||||
|
isImageResult(url) {
|
||||||
|
if (!url) return false
|
||||||
|
return /\.(jpeg|jpg|png|gif|webp|bmp)(\?.*)?$/i.test(url)
|
||||||
|
},
|
||||||
|
// 打开预览
|
||||||
|
openPreview(url, type) {
|
||||||
|
this.previewUrl = url
|
||||||
|
this.previewType = type
|
||||||
|
this.previewVisible = true
|
||||||
|
},
|
||||||
|
// 关闭预览
|
||||||
|
closePreview() {
|
||||||
|
this.previewVisible = false
|
||||||
|
this.previewUrl = ''
|
||||||
|
},
|
||||||
|
// 查看图片全屏
|
||||||
|
viewImageFull(url) {
|
||||||
|
this.$viewerApi({
|
||||||
|
options: {
|
||||||
|
initialViewIndex: 0,
|
||||||
|
toolbar: true
|
||||||
|
},
|
||||||
|
images: [url]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 查看详情
|
||||||
|
viewDetail(item) {
|
||||||
|
this.detailData = item
|
||||||
|
this.detailVisible = true
|
||||||
|
},
|
||||||
|
// 切换收藏状态
|
||||||
|
toggleFavorite(item) {
|
||||||
|
const newIsTop = item.isTop === 'Y' ? 'N' : 'Y'
|
||||||
|
this.$axios({
|
||||||
|
url: '/api/portal/assets/favorite',
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
id: item.id,
|
||||||
|
isTop: newIsTop
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res.code === 200) {
|
||||||
|
item.isTop = newIsTop
|
||||||
|
this.$message.success(newIsTop === 'Y' ? '收藏成功' : '取消收藏成功')
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '操作失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.$message.error(err?.message || '操作失败')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.generated-assets-page {
|
||||||
|
padding: 16px;
|
||||||
|
min-height: 100%;
|
||||||
|
background: #121826;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-section {
|
||||||
|
background: #1a1f2e;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(77, 137, 255, 0.1),
|
||||||
|
0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(180px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
grid-template-columns: repeat(3, minmax(180px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
grid-template-columns: repeat(2, minmax(180px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.actions {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-line {
|
||||||
|
margin: 10px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrap {
|
||||||
|
overflow: auto;
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(77, 137, 255, 0.1),
|
||||||
|
0 4px 20px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-table {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 12px 10px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列宽定义
|
||||||
|
th:nth-child(1), td:nth-child(1) { width: 60px; }
|
||||||
|
th:nth-child(2), td:nth-child(2) { width: 120px; }
|
||||||
|
th:nth-child(3), td:nth-child(3) { width: 80px; }
|
||||||
|
th:nth-child(4), td:nth-child(4) { width: 80px; }
|
||||||
|
th:nth-child(5), td:nth-child(5) { width: 180px; }
|
||||||
|
th:nth-child(6), td:nth-child(6) { width: 200px; }
|
||||||
|
th:nth-child(7), td:nth-child(7) { width: 140px; }
|
||||||
|
th:nth-child(8), td:nth-child(8) { width: 140px; }
|
||||||
|
|
||||||
|
// 操作列按钮间距
|
||||||
|
td:nth-child(8) {
|
||||||
|
.arco-btn + .arco-btn {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.td-id {
|
||||||
|
word-break: break-all;
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.params-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.param-tag {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-empty {
|
||||||
|
color: rgba(255, 255, 255, 0.35);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-cell {
|
||||||
|
.media-preview {
|
||||||
|
.video-thumb {
|
||||||
|
width: 160px;
|
||||||
|
height: 90px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #000;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-thumb {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-id {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-empty {
|
||||||
|
color: rgba(255, 255, 255, 0.35);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pager {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
// 分页器总计文字颜色调整,确保在深色背景下清晰可见
|
||||||
|
::v-deep(.arco-pagination-total) {
|
||||||
|
color: rgba(255, 255, 255, 0.9) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页器页码和跳转文字颜色
|
||||||
|
::v-deep(.arco-pagination) {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页器输入框文字颜色
|
||||||
|
::v-deep(.arco-pagination-jumper) {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-tip {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 详情弹窗 - label:value 形式(深色背景白字) */
|
||||||
|
.detail-content {
|
||||||
|
padding: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: #1a1f2e;
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
min-height: 100%;
|
||||||
|
|
||||||
|
.detail-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-group {
|
||||||
|
background: #252b3a;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(77, 137, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #4d89ff;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid rgba(77, 137, 255, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
width: 100px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 13px;
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
flex: 1;
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
word-break: break-all;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
&.long-text {
|
||||||
|
max-height: 110px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-video {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 260px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.03);
|
||||||
|
box-shadow: 0 0 0 3px rgba(77, 137, 255, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
max-height: 160px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-text {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-style: italic;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-block {
|
||||||
|
background: #1a1f2e;
|
||||||
|
color: #a5d6ff;
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
max-height: 220px;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre;
|
||||||
|
font-family: 'Consolas', monospace;
|
||||||
|
line-height: 1.4;
|
||||||
|
border: 1px solid rgba(165, 214, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览弹窗样式
|
||||||
|
// Tabs 样式 - 深色主题卡片风格(适配 Arco Design Vue + 项目深色UI)
|
||||||
|
.view-tabs {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 0 4px;
|
||||||
|
|
||||||
|
::v-deep(.arco-tabs) {
|
||||||
|
.arco-tabs-nav {
|
||||||
|
background: #1a1f2e !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
padding: 6px !important;
|
||||||
|
margin-bottom: 0;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(77, 137, 255, 0.1),
|
||||||
|
0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-tabs-tab {
|
||||||
|
color: #e5e7eb !important;
|
||||||
|
margin: 0 4px !important;
|
||||||
|
padding: 10px 28px !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
transition: all 0.22s ease !important;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 1px solid transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-tabs-tab:hover {
|
||||||
|
color: #e5e7eb !important;
|
||||||
|
background: rgba(77, 137, 255, 0.14) !important;
|
||||||
|
border-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-tabs-tab-active {
|
||||||
|
color: #fff !important;
|
||||||
|
background: #4d89ff !important;
|
||||||
|
border-color: transparent !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
box-shadow: 0 0 12px rgba(77, 137, 255, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-tabs-ink-bar {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前 tabs 仅用于视图切换,不需要内容区域,避免出现白色空框
|
||||||
|
.arco-tabs-content {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-field {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.preview-video {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 500px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 500px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 作品详情弹窗深色底样式 - 严格符合 GeneratedAssets.vue 深色主题(适配 Arco Design) */
|
||||||
|
::v-deep(.arco-modal) {
|
||||||
|
.arco-modal-content {
|
||||||
|
background-color: #1a1f2e !important;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(77, 137, 255, 0.1),
|
||||||
|
0 4px 20px rgba(0, 0, 0, 0.35),
|
||||||
|
0 16px 48px rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-modal-header {
|
||||||
|
background: transparent !important;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
padding: 20px 24px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-modal-title {
|
||||||
|
color: rgba(255, 255, 255, 0.95) !important;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-modal-close {
|
||||||
|
color: rgba(255, 255, 255, 0.65) !important;
|
||||||
|
top: 18px;
|
||||||
|
right: 20px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #fff !important;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-modal-body {
|
||||||
|
padding: 0 !important;
|
||||||
|
background: transparent;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保 detail-content 与页面卡片风格一致(深色背景白字)
|
||||||
|
.detail-content {
|
||||||
|
background: #1a1f2e !important;
|
||||||
|
color: rgba(255, 255, 255, 0.95) !important;
|
||||||
|
padding: 20px !important;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -61,13 +61,28 @@
|
||||||
</a-radio>
|
</a-radio>
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div
|
<!-- 模型选择 -->
|
||||||
class="text"
|
<div class="model-select">
|
||||||
v-if="current == 2">
|
<div class="model-title">{{ $t('common.selectModel') || '选择模型' }}</div>
|
||||||
<a-textarea
|
<a-select v-model="selectedModel" style="width: 100%;">
|
||||||
v-model="text"
|
<a-option
|
||||||
:placeholder="$t('common.textPlaceholder')" />
|
v-for="option in modelOptions"
|
||||||
</div> -->
|
:key="option.value"
|
||||||
|
:value="option.value">
|
||||||
|
{{ option.label }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 富文本编辑器 -->
|
||||||
|
<RichTextEditor
|
||||||
|
v-model="editorContent"
|
||||||
|
:placeholder="$t('common.textPlaceholder') || '请输入文本生成图片...'"
|
||||||
|
:uploaded-images="uploadedImages"
|
||||||
|
@text-change="handleTextChange"
|
||||||
|
@image-upload="handleImageUpload"
|
||||||
|
/>
|
||||||
|
|
||||||
<mf-button
|
<mf-button
|
||||||
class="submit"
|
class="submit"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|
@ -157,6 +172,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
|
import RichTextEditor from '@/components/RichTextEditor.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -179,9 +195,20 @@ export default {
|
||||||
templateList: [],
|
templateList: [],
|
||||||
templateLoading: false,
|
templateLoading: false,
|
||||||
selectedTemplate: null,
|
selectedTemplate: null,
|
||||||
selectedTemplatePreview: '' // 选中的模版预览图URL
|
selectedTemplatePreview: '', // 选中的模版预览图URL
|
||||||
|
// 模型选择 - 支持 Seedance 2.0
|
||||||
|
modelOptions: [
|
||||||
|
{ label: 'Seedance 2.0', value: 'ep-20260326165811-dlkth' },
|
||||||
|
{ label: 'Seedance 2.0 Fast', value: 'ep-20260326170056-dkj9m' }
|
||||||
|
],
|
||||||
|
selectedModel: 'ep-20260326165811-dlkth',
|
||||||
|
// 已上传的图片列表(用于@功能)
|
||||||
|
uploadedImages: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
components: {
|
||||||
|
RichTextEditor
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['lang']),
|
...mapGetters(['lang']),
|
||||||
},
|
},
|
||||||
|
|
@ -333,6 +360,21 @@ export default {
|
||||||
this.templateDialogVisible = false
|
this.templateDialogVisible = false
|
||||||
// 不重置selectedTemplate,保持用户的选择状态,方便二次选择
|
// 不重置selectedTemplate,保持用户的选择状态,方便二次选择
|
||||||
},
|
},
|
||||||
|
// 处理富文本内容变化
|
||||||
|
handleTextChange(content) {
|
||||||
|
this.editorContent = content
|
||||||
|
// 同步到旧的 text 字段以保持兼容性
|
||||||
|
this.text = content
|
||||||
|
},
|
||||||
|
// 处理上传的图片(用于@功能)
|
||||||
|
handleImageUpload(imageInfo) {
|
||||||
|
if (imageInfo && imageInfo.url) {
|
||||||
|
this.uploadedImages.push({
|
||||||
|
url: imageInfo.url,
|
||||||
|
name: imageInfo.name || 'image'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
close() {
|
close() {
|
||||||
this.showResult = false
|
this.showResult = false
|
||||||
},
|
},
|
||||||
|
|
@ -396,10 +438,11 @@ export default {
|
||||||
url: 'api/ai/imgToImg',
|
url: 'api/ai/imgToImg',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
text: this.text,
|
text: this.editorContent || this.text,
|
||||||
firstUrl: this.firstUrl.url,
|
firstUrl: this.firstUrl.url,
|
||||||
functionType: this.current == 1 ? '11' : '12',
|
functionType: this.current == 1 ? '11' : '12',
|
||||||
tags: tags.join(',')
|
tags: tags.join(','),
|
||||||
|
model: this.selectedModel // 新增模型参数
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
|
@ -408,14 +451,10 @@ export default {
|
||||||
this.imageUrl = res.msg
|
this.imageUrl = res.msg
|
||||||
this.showResult = true
|
this.showResult = true
|
||||||
} else if (res.code == -1) {
|
} else if (res.code == -1) {
|
||||||
this.$confirm({
|
this.$modal.error({
|
||||||
title: this.$t('common.notice'),
|
title: this.$t('common.notice'),
|
||||||
content: this.$t('common.balenceLow'),
|
content: this.$t('common.balenceLow'),
|
||||||
okText: this.$t('common.confirm'),
|
okText: this.$t('common.confirm')
|
||||||
cancelText: this.$t('common.cancel'),
|
|
||||||
onOk: (_) => {
|
|
||||||
this.$router.push('/recharge')
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
} else if (res.code == -2) {
|
} else if (res.code == -2) {
|
||||||
this.$modal.error({
|
this.$modal.error({
|
||||||
|
|
@ -496,6 +535,74 @@ export default {
|
||||||
.upload {
|
.upload {
|
||||||
// margin-top: 20px;
|
// margin-top: 20px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.model-select {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.model-title {
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-select) {
|
||||||
|
background-color: #1a1b20;
|
||||||
|
border-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 富文本编辑器样式 */
|
||||||
|
.rich-editor-root {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #1a1b20;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
|
||||||
|
.tool-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background-color: rgba(255,255,255,0.05);
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
border-color: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-editor {
|
||||||
|
min-height: 160px;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #1a1b20;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: rgb(var(--primary-6));
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-placeholder]:empty::before {
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
color: #5c5d68;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
&-title {
|
&-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -274,9 +274,9 @@ const showRecharge = () => {
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
90deg,
|
90deg,
|
||||||
rgba(39, 20, 51, 0.5) 0%,
|
rgba(18, 24, 38, 0.65) 0%,
|
||||||
rgba(230, 33, 122, 0.5) 49%,
|
rgba(77, 137, 255, 0.22) 50%,
|
||||||
rgba(39, 20, 51, 0.5) 100%
|
rgba(18, 24, 38, 0.65) 100%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -295,10 +295,10 @@ const showRecharge = () => {
|
||||||
&-content {
|
&-content {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 0 40px;
|
padding: 0 40px;
|
||||||
background-color: #000;
|
background-color: #121826;
|
||||||
|
|
||||||
* {
|
* {
|
||||||
color: #999999 !important;
|
color: #9ca3af !important;
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -33,7 +33,7 @@ onMounted(() => {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #0f0f12;
|
background: #121826;
|
||||||
|
|
||||||
.desc {
|
.desc {
|
||||||
font-size: 120px;
|
font-size: 120px;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="Video icon">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="video-grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#4d89ff"/>
|
||||||
|
<stop offset="100%" stop-color="#9d6bff"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="32" height="32" rx="8" fill="#121826"/>
|
||||||
|
<rect x="6" y="9" width="14" height="14" rx="3" fill="url(#video-grad)"/>
|
||||||
|
<path d="M22 13.2l4-2.1a1 1 0 0 1 1.46.9v8a1 1 0 0 1-1.46.9l-4-2.1v-5.6z" fill="#7aa6ff"/>
|
||||||
|
<path d="M12 13.2l5 2.8-5 2.8v-5.6z" fill="#e5e7eb"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 593 B |
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue