Merge pull request 'feature/0915_素材中心新增手动上传' (#49) from feature/0915_素材中心新增手动上传 into main

Reviewed-on: ai-team/lingji-work-fe#49
This commit is contained in:
2025-09-23 03:01:09 +00:00
54 changed files with 1943 additions and 194 deletions

View File

@ -8,18 +8,20 @@
<script setup> <script setup>
import { useUserStore } from '@/stores'; import { useUserStore } from '@/stores';
// import { useChatStore } from '@/stores/modules/chat'; // import { useChatStore } from '@/stores/modules/chat';
import { useRoute } from 'vue-router';
import { initApp } from '@/utils/user'; import { handleUserHome, initApp } from '@/utils/user';
import { useSidebarStore } from '@/stores/modules/side-bar'; import { useSidebarStore } from '@/stores/modules/side-bar';
import { useEnterpriseStore } from '@/stores/modules/enterprise';
import { ConfigProvider } from 'ant-design-vue'; import { ConfigProvider } from 'ant-design-vue';
import zhCN from 'ant-design-vue/es/locale/zh_CN'; import zhCN from 'ant-design-vue/es/locale/zh_CN';
import 'dayjs/locale/zh-cn'; import 'dayjs/locale/zh-cn';
const userStore = useUserStore(); const userStore = useUserStore();
// const route = useRoute(); const route = useRoute();
const sidebarStore = useSidebarStore(); const sidebarStore = useSidebarStore();
// const chatStore = useChatStore(); // const chatStore = useChatStore();
const enterpriseStore = useEnterpriseStore();
const redTheme = { const redTheme = {
token: { token: {
@ -34,6 +36,10 @@ const init = async () => {
// 已开通 // 已开通
if (isLogin) { if (isLogin) {
await initApp(); await initApp();
if (enterpriseStore.isOpenEnterprise && route.name === 'Trial') {
handleUserHome();
}
} else { } else {
sidebarStore.stopUnreadInfoPolling(); sidebarStore.stopUnreadInfoPolling();
} }

View File

@ -73,6 +73,11 @@ export const getVideoPreSignedUrl = (params = {}) => {
return Http.get('/v1/oss/video-pre-signed-url', params); return Http.get('/v1/oss/video-pre-signed-url', params);
}; };
// 获取文件上传地址
export const getFilePreSignedUrl = (params = {}) => {
return Http.get('/v1/oss/file-pre-signed-url', params);
};
// 清除限流 // 清除限流
export const postClearRateLimiter = (params = {}) => { export const postClearRateLimiter = (params = {}) => {
return Http.post(`/v1/rate-limiter/clear`, params); return Http.post(`/v1/rate-limiter/clear`, params);

View File

@ -170,3 +170,40 @@ export const batchDeleteRawMaterials = (params = {}) => {
export const postRawMaterialsAI = (params = {}) => { export const postRawMaterialsAI = (params = {}) => {
return Http.post('/v1/raw-materials/ai', params); return Http.post('/v1/raw-materials/ai', params);
}; };
// 原料库标签-列表
export const getRawMaterialTagsList = (params = {}) => {
return Http.get('/v1/raw-material-tags/list', params);
};
// 原料库标签-添加
export const posRawMaterialTags = (params = {}) => {
return Http.post('/v1/raw-material-tags', params);
};
// 原料库标签-修改
export const putRawMaterialTag = (params = {}) => {
const { id, ...rest } = params as { id: string; [key: string]: any };
return Http.put(`/v1/raw-material-tags/${id}`, rest);
};
// 原料库标签-删除
export const deleteRawMaterialTag = (id: string) => {
return Http.delete(`/v1/raw-material-tags/${id}`);
};
// 原料库-本地批量添加
export const postBatchRawMaterial = (params = {}) => {
return Http.post('/v1/raw-materials/batch', params);
};
// 原料库-修改
export const putRawMaterial = (params = {}) => {
const { id, ...rest } = params as { id: string; [key: string]: any };
return Http.put(`/v1/raw-materials/${id}`, rest);
};
// 原料库-详情
export const getRawMaterialDetail = (id: string) => {
return Http.get(`/v1/raw-materials/${id}`);
};

View File

@ -28,7 +28,7 @@ export const fetchLogOut = (params = {}) => {
}; };
// 导出一个名为fetchProfileInfo的函数用于获取用户信息 // 导出一个名为fetchProfileInfo的函数用于获取用户信息
export const fetchProfileInfo = (params = {}) => { export const fetchUserInfo = (params = {}) => {
// 使用Http.put方法向/v1/me接口发送put请求并将params作为参数传递 // 使用Http.put方法向/v1/me接口发送put请求并将params作为参数传递
return Http.get('/v1/me', params); return Http.get('/v1/me', params);
}; };

View File

@ -20,12 +20,22 @@
<!-- 头像设置 --> <!-- 头像设置 -->
<Dropdown trigger="click" overlayClassName="layout-avatar-dropdown"> <Dropdown trigger="click" overlayClassName="layout-avatar-dropdown">
<div class="cursor-pointer"> <div
<Avatar :src="userInfo.head_image" :size="32" v-if="userInfo.head_image" /> class="w-126px h-32px flex items-center justify-between px-4px pl-4px pr-8px rounded-30px bg-white bg-opacity-60 group"
<div v-else class="w-32px h-32px rounded-50% bg-#6D4CFE flex items-center justify-center"> >
<span class="color-#FFF text-14px font-400 lh-22px">{{ userInfo.mobile?.slice(-3) }}</span> <div class="flex mr-4px overflow-hidden h-24px lh-24px items-center">
<div class="mr-4px">
<Avatar v-if="userData.head_image" :size="24" :src="userData.head_image" />
<div v-else class="w-24px h-24px rounded-50% bg-#6D4CFE flex items-center justify-center">
<span class="color-#FFF text-11px font-400 lh-20px">{{ userData.mobile?.slice(-3) }}</span>
</div> </div>
</div> </div>
<TextOverTips :context="userData.name || userData.mobile" />
</div>
<icon-caret-down class="icon-caret color-#939499" size="14" />
</div>
<template #overlay> <template #overlay>
<Menu> <Menu>
<MenuItem> <MenuItem>
@ -49,6 +59,7 @@
</div> </div>
</template> </template>
<div v-for="(item, index) in enterprises" :key="index"> <div v-for="(item, index) in enterprises" :key="index">
<!--非申请企业信息-->
<MenuItem <MenuItem
class="rounded-8px hover:bg-#F2F3F5" class="rounded-8px hover:bg-#F2F3F5"
@click="onEnterpriseItemClick(item)" @click="onEnterpriseItemClick(item)"
@ -65,7 +76,9 @@
<template v-else> <template v-else>
<template v-if="item.audit_status === 1"> <template v-if="item.audit_status === 1">
<div v-if="enterprises.length > 1" class="w-full h-1px bg-#E6E6E8 mb-8px"></div> <div v-if="enterprises.length > 1" class="px-12px my-8px">
<div class="w-full h-1px bg-#E6E6E8"></div>
</div>
<MenuItem class="rounded-8px hover:bg-#F2F3F5 h-36px !mb-0" @click="onCreate(item)"> <MenuItem class="rounded-8px hover:bg-#F2F3F5 h-36px !mb-0" @click="onCreate(item)">
<div class="flex items-center"> <div class="flex items-center">
<SvgIcon size="16.5" name="svg-organization" class="color-#737478 mr-8px" /> <SvgIcon size="16.5" name="svg-organization" class="color-#737478 mr-8px" />
@ -83,7 +96,7 @@
class="flex items-center w-100% flex-1 overflow-hidden" class="flex items-center w-100% flex-1 overflow-hidden"
:class="enterpriseInfo?.id === item.id ? '!color-#6D4CFE' : ''" :class="enterpriseInfo?.id === item.id ? '!color-#6D4CFE' : ''"
> >
<TextoverTips :context="item.name" /> <TextOverTips :context="item.name" />
<img :src="icon4" width="12" height="12" class="ml-4px" /> <img :src="icon4" width="12" height="12" class="ml-4px" />
</div> </div>
<div <div
@ -137,6 +150,7 @@ import { useEnterpriseStore } from '@/stores/modules/enterprise';
import { useSidebarStore } from '@/stores/modules/side-bar'; import { useSidebarStore } from '@/stores/modules/side-bar';
import { useUserStore } from '@/stores'; import { useUserStore } from '@/stores';
import { handleUserHome } from '@/utils/user'; import { handleUserHome } from '@/utils/user';
import { initApp } from '@/utils/user';
import ExitAccountModal from '../exit-account-modal'; import ExitAccountModal from '../exit-account-modal';
import DownloadCenterModal from '../task-center-modal'; import DownloadCenterModal from '../task-center-modal';
@ -145,7 +159,7 @@ import icon1 from '@/assets/option.svg';
import icon2 from '@/assets/exit.svg'; import icon2 from '@/assets/exit.svg';
import icon3 from '@/assets/change.svg'; import icon3 from '@/assets/change.svg';
import icon4 from './img/admin.png'; import icon4 from './img/admin.png';
import TextoverTips from '@/components/text-over-tips/index.vue'; import TextOverTips from '@/components/text-over-tips/index.vue';
const props = defineProps({ const props = defineProps({
isAgentRoute: { isAgentRoute: {
@ -174,7 +188,8 @@ const primary_enterprise = computed(() => userStore.userInfo?.primary_enterprise
const enterprises = computed(() => { const enterprises = computed(() => {
return userStore.userInfo?.enterprises ?? []; return userStore.userInfo?.enterprises ?? [];
}); });
const userInfo = computed(() => userStore.userInfo); const userData = computed(() => userStore.userInfo ?? {});
const enterpriseInfo = computed(() => { const enterpriseInfo = computed(() => {
return enterpriseStore?.enterpriseInfo ?? {}; return enterpriseStore?.enterpriseInfo ?? {};
}); });
@ -184,7 +199,13 @@ const openDownloadCenter = () => {
}; };
const onEnterpriseItemClick = async (item) => { const onEnterpriseItemClick = async (item) => {
enterpriseStore.setEnterpriseInfo(item); enterpriseStore.setEnterpriseInfo(item);
if (route.name === 'Trial') {
router.push({ name: 'Home' });
initApp();
} else {
window.location.reload(); window.location.reload();
}
}; };
const clickExit = async () => { const clickExit = async () => {

View File

@ -31,4 +31,17 @@
background: #fff; background: #fff;
} }
} }
.group {
&.ant-dropdown-open {
.icon-caret {
color: #6D4CFE;
transform: rotate(180deg);
}
:deep(.overflow-text) {
color: #6D4CFE;
}
}
}
} }

View File

@ -39,6 +39,7 @@ export const useChatStore = defineStore('chat', {
clearAgentInfo() { clearAgentInfo() {
this.agentInfo = {}; this.agentInfo = {};
this.searchValue = ''; this.searchValue = '';
rlsWithCatch('agentInfo');
}, },
async onCreateSession() { async onCreateSession() {
const { code, data } = await createSession(); const { code, data } = await createSession();

View File

@ -1,5 +1,5 @@
import { fetchEnterpriseInfo } from '@/api/all/login'; import { fetchEnterpriseInfo } from '@/api/all/login';
import { glsWithCatch, slsWithCatch } from '@/utils/stroage'; import { glsWithCatch, slsWithCatch, rlsWithCatch } from '@/utils/stroage';
interface EnterpriseInfo { interface EnterpriseInfo {
id: number | string; id: number | string;
@ -34,7 +34,7 @@ export const useEnterpriseStore = defineStore('enterprise', {
}, },
clearUserEnterpriseInfo() { clearUserEnterpriseInfo() {
this.enterpriseInfo = null; this.enterpriseInfo = null;
localStorage.removeItem('enterpriseInfo'); rlsWithCatch('enterpriseInfo');
}, },
setEnterpriseName(name: string) { setEnterpriseName(name: string) {
if (this.enterpriseInfo) { if (this.enterpriseInfo) {

View File

@ -1,5 +1,5 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { fetchProfileInfo } from '@/api/all/login'; import { fetchUserInfo } from '@/api/all/login';
// import { useSidebarStore } from '@/stores/modules/side-bar'; // import { useSidebarStore } from '@/stores/modules/side-bar';
import { glsWithCatch, rlsWithCatch, slsWithCatch } from '@/utils/stroage'; import { glsWithCatch, rlsWithCatch, slsWithCatch } from '@/utils/stroage';
import { useEnterpriseStore } from '@/stores/modules/enterprise'; import { useEnterpriseStore } from '@/stores/modules/enterprise';
@ -72,7 +72,7 @@ export const useUserStore = defineStore('user', {
async getUserInfo() { async getUserInfo() {
const enterpriseStore = useEnterpriseStore(); const enterpriseStore = useEnterpriseStore();
const { code, data } = await fetchProfileInfo(); const { code, data } = await fetchUserInfo();
if (code === 200) { if (code === 200) {
this.setUserInfo(data); this.setUserInfo(data);

View File

@ -71,7 +71,9 @@
} }
&:active { &:active {
background: $color-primary-7 !important; color: #fff !important;
border-color: transparent !important;
background-color: $color-primary-7 !important;
} }
} }
@ -109,6 +111,7 @@
&:active { &:active {
border-color: $color-primary-7 !important; border-color: $color-primary-7 !important;
color: $color-primary-7 !important; color: $color-primary-7 !important;
background-color: #fff !important;
} }
} }
&.ant-btn-dangerous { &.ant-btn-dangerous {
@ -190,6 +193,7 @@
&:active { &:active {
color: $color-primary-7 !important; color: $color-primary-7 !important;
border-color: transparent !important;
} }
} }
&.ant-btn-dangerous { &.ant-btn-dangerous {
@ -225,6 +229,7 @@
&:active { &:active {
color: $color-primary-7 !important; color: $color-primary-7 !important;
border-color: transparent !important;
} }
} }
&.ant-btn-dangerous { &.ant-btn-dangerous {

View File

@ -0,0 +1,20 @@
.ant-drawer {
.ant-drawer-header {
.ant-drawer-header-title {
justify-content: space-between;
.ant-drawer-close {
order: 2;
}
.ant-drawer-title {
color: #211f24;
font-family: $font-family-medium;
font-size: 18px;
font-style: normal;
font-weight: 500;
line-height: 26px;
}
}
}
}

View File

@ -27,7 +27,7 @@
&:focus-within, &:focus-within,
&.ant-input-focus, &.ant-input-focus,
&.ant-textarea-focus { &.ant-textarea-focus {
background-color: var(--color-bg-2) !important; background-color: #fff !important;
border-color: $color-primary !important; border-color: $color-primary !important;
box-shadow: 0 0 0 0 var(--color-primary-light-2) !important; box-shadow: 0 0 0 0 var(--color-primary-light-2) !important;
} }

View File

@ -22,7 +22,7 @@
} }
} }
.ant-modal-body { .ant-modal-body {
padding: 24px 20px; padding: 20px 24px;
} }
.ant-modal-footer { .ant-modal-footer {
margin-top: 0; margin-top: 0;
@ -38,5 +38,36 @@
} }
} }
} }
.ant-modal-confirm-body-wrapper {
.ant-modal-confirm-title {
font-family: $font-family-medium;
font-weight: 500;
color: #211F24;
font-size: 16px;
font-style: normal;
line-height: 24px;
.anticon {
font-size: 24px;
}
}
.ant-modal-confirm-content {
margin-top: 8px;
color: var(--Text-2, #55585F);
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
}
.ant-modal-confirm-btns {
display: flex;
justify-content: end;
margin-top: 24px;
}
}
} }
} }

View File

@ -24,11 +24,31 @@
font-weight: 400; font-weight: 400;
} }
} }
.ant-select-arrow {
color: #737478;
.anticon {
&.anticon-down {
color: #737478;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14' fill='none'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M11.9629 5.5L11.1379 4.67504L7.01314 8.79983L2.88835 4.67504L2.0634 5.5L6.60063 10.0372C6.60063 10.0372 6.60066 10.0373 7.01314 9.62479L6.60063 10.0372L7.01314 10.4497L11.9629 5.5Z' fill='%23737478'/%3E%3C/svg%3E");
background-size: 14px;
width: 14px;
height: 14px;
background-repeat: no-repeat;
background-position: center right;
> svg {
display: none;
}
}
}
}
&:focus, &:focus,
&-focused { &-focused {
.ant-select-selector { .ant-select-selector {
background-color: var(--color-bg-2) !important; background-color: #fff !important;
border-color: rgb(var(--primary-6)) !important; border-color: $color-primary !important;
box-shadow: 0 0 0 0 var(--color-primary-light-2) !important; box-shadow: 0 0 0 0 var(--color-primary-light-2) !important;
} }
} }
@ -36,6 +56,29 @@
border-color: $color-error !important; border-color: $color-error !important;
} }
&:not(.ant-select-disabled) {
&:hover {
.ant-select-selector {
border-color: $color-primary !important;
}
}
}
&-disabled {
.ant-select-selector {
background-color: var(--BG-200, #f2f3f5) !important;
}
}
&.ant-select-open {
.ant-select-arrow {
.anticon {
&.anticon-down {
transform: rotate(180deg);
}
}
}
}
} }
@ -56,12 +99,16 @@
} }
} }
// 复选
&.ant-select-multiple { &.ant-select-multiple {
.ant-select-selector { .ant-select-selector {
padding: 0 12px 0 4px !important; height: fit-content !important;
padding: 0 28px 0 4px !important;
.ant-select-selection-overflow-item { .ant-select-selection-overflow-item {
margin-right: 4px;
.ant-select-selection-item { .ant-select-selection-item {
margin-right: 0;
border-radius: 4px; border-radius: 4px;
background: #E6E6E8; background: #E6E6E8;
border: none; border: none;
@ -71,6 +118,22 @@
color: #737478; color: #737478;
} }
} }
.ant-tag {
padding: 1px 8px;
border-radius: 4px;
background: var(--BG-300, #E6E6E8);
color: var(--Text-1, #211F24);
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
.anticon-close {
font-size: 12px;
}
}
} }
} }
@ -98,25 +161,78 @@
} }
} }
.ant-tag {
font-size: 12px;
line-height: 20px;
}
} }
} }
} }
} }
} &.ant-select-single {
.ant-select.ant-select-single {
.ant-select-selector { .ant-select-selector {
height: 32px !important; height: 32px !important;
} }
&.ant-select-lg { &.ant-select-lg {
.ant-select-selector { .ant-select-selector {
height: 36px !important; height: 36px !important;
} }
} }
&.ant-select-sm { &.ant-select-sm {
.ant-select-selector { .ant-select-selector {
height: 28px !important; height: 28px !important;
} }
} }
} }
}
.ant-select-dropdown {
padding: 4px 0;
.ant-select-item-option {
padding: 0 12px;
height: 36px;
align-items: center;
.ant-select-item-option-state {
display: none;
}
&:not(.ant-select-item-option-disabled) {
&:hover {
background-color: #F2F3F5 !important;
}
.ant-select-item-option-content {
color: #211F24;
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
}
&.ant-select-item-option-selected {
background-color: transparent !important;
color: #6D4CFE !important;
&:hover {
background-color: #F2F3F5 !important;
}
.ant-select-item-option-content {
color: #6D4CFE;
font-weight: 500;
font-family: $font-family-medium;
}
}
&.ant-select-item-option-active {
}
}
}
}

View File

@ -56,20 +56,40 @@
} }
.ant-table-bordered { .ant-table-bordered {
.ant-table-container { .ant-table-container {
.ant-table-content {
> table {
border-top: none !important;
.ant-table-thead { .ant-table-thead {
.ant-table-cell { .ant-table-cell {
border-inline-end: none !important; border-inline-end: none !important;
border-bottom: 1px solid var(--Border-1, #d7d7d9); border-bottom: 1px solid var(--Border-1, #d7d7d9);
} }
.ant-table-cell-fix-right-first {
&::after {
border-inline-end: none !important;
} }
}
}
.ant-table-tbody { .ant-table-tbody {
.ant-table-row { .ant-table-row {
.ant-table-cell { .ant-table-cell {
border-inline-end: none !important; border-inline-end: none !important;
border-bottom: 1px solid var(--Border-1, #e6e6e8); border-bottom: 1px solid var(--Border-1, #e6e6e8);
} }
.ant-table-cell-fix-right-first {
&::after {
border-inline-end: none !important;
} }
} }
} }
} }
} }
}
}
}
}

View File

@ -1,4 +1,7 @@
.ant-tag { .ant-tag {
border-radius: 2px; border-radius: 4px;
border: none; border: none;
font-family: $font-family-regular;
font-style: normal;
font-weight: 400;
} }

View File

@ -2,4 +2,25 @@
.ant-input { .ant-input {
padding: 8px 12px 4px 12px; padding: 8px 12px 4px 12px;
} }
&:not(.ant-input-textarea-disabled) {
&:hover {
.ant-input {
border-color: $color-primary !important;
}
}
}
&.ant-input-textarea-show-count {
&::after {
position: absolute;
right: 8px;
bottom: 4px;
font-family: $font-family-regular;
color: #939499;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
}
} }

View File

@ -27,3 +27,4 @@
@import "./ant-switch.scss"; @import "./ant-switch.scss";
@import "./ant-step.scss"; @import "./ant-step.scss";
@import "./ant-spin.scss"; @import "./ant-spin.scss";
@import "./ant-drawer.scss";

View File

@ -4,6 +4,7 @@
*/ */
import dayjs from 'dayjs'; import dayjs from 'dayjs';
export function toFixed(num: number | string, n: number): number { export function toFixed(num: number | string, n: number): number {
return parseFloat(parseFloat(num.toString()).toFixed(n)); return parseFloat(parseFloat(num.toString()).toFixed(n));
} }
@ -111,11 +112,15 @@ export function genRandomId() {
return `id_${dayjs().unix()}_${Math.floor(Math.random() * 10000)}`; return `id_${dayjs().unix()}_${Math.floor(Math.random() * 10000)}`;
} }
export function getFileExtension(filename) {
const match = filename.match(/\.([^.]+)$/);
return match ? match[1].toLowerCase() : '';
}
export function formatFileSize(bytes: number): string { export function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 Bytes'; if (bytes === 0) return '0 Bytes';
const k = 1024; const k = 1024;
const sizes = ['Bytes', 'kb', 'mB', 'gB', 'tB']; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
@ -217,7 +222,7 @@ export function getVideoInfo(file: File): Promise<{ duration: number; firstFrame
}; };
// 设置视频源以触发加载 // 设置视频源以触发加载
video.src = URL.createObjectURL(file); video.src = window.URL.createObjectURL(file);
// 设置超时,防止长时间无响应 // 设置超时,防止长时间无响应
setTimeout(() => { setTimeout(() => {

View File

@ -42,9 +42,9 @@ export async function initApp() {
const enterpriseStore = useEnterpriseStore(); const enterpriseStore = useEnterpriseStore();
await userStore.getUserInfo(); // 初始化用户信息 await userStore.getUserInfo(); // 初始化用户信息
await getUserEnterpriseInfo(); // 初始化企业信息
if (enterpriseStore.isOpenEnterprise) { if (enterpriseStore.isOpenEnterprise) {
await getUserEnterpriseInfo(); // 初始化企业信息
chatStore.getAgentInfo(); // 初始化智能体信息 chatStore.getAgentInfo(); // 初始化智能体信息
sidebarStore.startUnreadInfoPolling(); // 初始化未读信息 sidebarStore.startUnreadInfoPolling(); // 初始化未读信息

View File

@ -522,12 +522,6 @@ function reset() {
const emit = defineEmits(['submit']); const emit = defineEmits(['submit']);
</script> </script>
<script>
export default {
name: 'PuzzleVerification',
};
</script>
<style lang="scss" scoped> <style lang="scss" scoped>
.vue-puzzle-vcode { .vue-puzzle-vcode {
position: fixed; position: fixed;

View File

@ -33,7 +33,7 @@
</div> </div>
</template> </template>
</Input> </Input>
<p class="color-#F64B31 text-12px font-400 h-20px lh-20px font-family-regular"> <p class="color-#F64B31 text-12px font-400 lh-20px font-family-regular" v-show="errMsg">
{{ errMsg }} {{ errMsg }}
</p> </p>
</FormItem> </FormItem>
@ -43,14 +43,14 @@
<img :src="visible ? icon2 : icon1" width="20" height="20" class="cursor-pointer" /> <img :src="visible ? icon2 : icon1" width="20" height="20" class="cursor-pointer" />
</template> </template>
</Input.Password> </Input.Password>
<p class="color-#F64B31 h-20px text-12px font-400 lh-20px font-family-regular"> <p v-show="errMsg" class="color-#F64B31 h-20px text-12px font-400 lh-20px font-family-regular">
{{ errMsg }} {{ errMsg }}
</p> </p>
</FormItem> </FormItem>
<FormItem class="mt-32px"> <FormItem class="mt-32px">
<div class="text-12px flex justify-center items-center mb-16px"> <div class="text-12px flex justify-center items-center mb-16px">
<Checkbox v-model:checked="hasAgree" class="mr-8px"></Checkbox> <Checkbox v-model:checked="hasCheck" class="mr-8px"></Checkbox>
<span class="text-12px color-#737478 font-400 lh-20px font-family-regular" <span class="text-12px color-#737478 font-400 lh-20px font-family-regular"
>登录即代表同意<span class="color-#6D4CFE"> 用户协议 </span><span class="color-#6D4CFE"> >登录即代表同意<span class="color-#6D4CFE"> 用户协议 </span><span class="color-#6D4CFE">
隐私政策</span 隐私政策</span
@ -70,7 +70,7 @@
<div> <div>
<Button <Button
v-show="!isCaptchaLogin" v-show="!isCaptchaLogin"
class="!color-#939499 !p-0 !h-22px !hover:color-#6D4CFE !active:color-#573DCB" class="!color-#939499 !p-0 !h-22px hover:color-#6D4CFE"
size="small" size="small"
type="text" type="text"
@click="onForgetPassword" @click="onForgetPassword"
@ -81,7 +81,7 @@
<Button <Button
type="text" type="text"
class="!color-#939499 !p-0 !h-22px !hover:color-#6D4CFE !active:color-#573DCB" class="!color-#939499 !p-0 !h-22px hover:color-#6D4CFE"
size="small" size="small"
@click="onRegister" @click="onRegister"
> >
@ -105,7 +105,7 @@
import { Button, Checkbox, Form, FormItem, Input, message, Tabs, Typography } from 'ant-design-vue'; import { Button, Checkbox, Form, FormItem, Input, message, Tabs, Typography } from 'ant-design-vue';
import PuzzleVerification from '../PuzzleVerification.vue'; import PuzzleVerification from '../PuzzleVerification.vue';
import SelectAccountModal from '../select-account-modal/index.vue'; import SelectAccountModal from '../select-account-modal/index.vue';
import { fetchAuthorizationsCaptcha, fetchLoginCaptCha, fetchProfileInfo, postLoginPassword } from '@/api/all/login'; import { fetchAuthorizationsCaptcha, fetchLoginCaptCha, fetchUserInfo, postLoginPassword } from '@/api/all/login';
import { postClearRateLimiter } from '@/api/all/common'; import { postClearRateLimiter } from '@/api/all/common';
import { joinEnterpriseByInviteCode } from '@/api/all'; import { joinEnterpriseByInviteCode } from '@/api/all';
import { computed, onUnmounted, reactive, ref } from 'vue'; import { computed, onUnmounted, reactive, ref } from 'vue';
@ -134,7 +134,7 @@ const isLogin = ref(true);
const isVerificationVisible = ref(false); const isVerificationVisible = ref(false);
const hasGetCode = ref(false); const hasGetCode = ref(false);
const submitting = ref(false); const submitting = ref(false);
const hasAgree = ref(false); const hasCheck = ref(false);
const mobileNumber = ref(''); const mobileNumber = ref('');
const selectAccountModalRef = ref(null); const selectAccountModalRef = ref(null);
const accounts = ref([]); const accounts = ref([]);
@ -215,16 +215,13 @@ const clearErrorMsg = () => {
const disabledSubmitBtn = computed(() => { const disabledSubmitBtn = computed(() => {
if (isCaptchaLogin.value) { if (isCaptchaLogin.value) {
return !hasAgree.value || !isLegalMobile.value || !loginForm.captcha.trim() || !/^\d{6}$/.test(loginForm.captcha); return !hasCheck.value || !isLegalMobile.value || !loginForm.captcha.trim() || !/^\d{6}$/.test(loginForm.captcha);
} }
// 密码登录时的验证逻辑 // 密码登录时的验证逻辑
return !hasAgree.value || !isLegalMobile.value || !loginForm.password.trim(); return !hasCheck.value || !isLegalMobile.value || !loginForm.password.trim();
}); });
const validateField = (field: string) => { const validateField = (field: string) => {
if (field === 'mobile') {
errMsg.value = '';
}
formRef.value.validateFields(field); formRef.value.validateFields(field);
}; };
@ -271,7 +268,7 @@ const onTabChange = () => {
// 获取用户信息 // 获取用户信息
const getProfileInfo = async () => { const getProfileInfo = async () => {
const { code, data } = await fetchProfileInfo(); const { code, data } = await fetchUserInfo();
if (code === 200) { if (code === 200) {
const enterprises = data['enterprises']; const enterprises = data['enterprises'];
mobileNumber.value = data['mobile']; mobileNumber.value = data['mobile'];
@ -286,6 +283,7 @@ const getProfileInfo = async () => {
selectAccountModalRef.value.open(); selectAccountModalRef.value.open();
} }
} else { } else {
userStore.setUserInfo(data);
router.push({ name: 'Trial' }); router.push({ name: 'Trial' });
} }
} }
@ -299,7 +297,7 @@ const handleSubmit = async () => {
// 校验所有字段 // 校验所有字段
await formRef.value.validate(); await formRef.value.validate();
if (!hasAgree.value) { if (!hasCheck.value) {
message.error('请先勾选同意用户协议'); message.error('请先勾选同意用户协议');
return; return;
} }
@ -351,6 +349,7 @@ const onForgetPassword = () => {
setPageType('resetPasswordForm'); setPageType('resetPasswordForm');
}; };
const onRegister = () => { const onRegister = () => {
console.log('onRegister');
setPageType('registerForm'); setPageType('registerForm');
}; };

View File

@ -92,7 +92,7 @@ import PuzzleVerification from '../PuzzleVerification.vue';
import SelectAccountModal from '../select-account-modal/index.vue'; import SelectAccountModal from '../select-account-modal/index.vue';
import { postClearRateLimiter } from '@/api/all/common'; import { postClearRateLimiter } from '@/api/all/common';
import { import {
fetchProfileInfo, fetchUserInfo,
postForgetPassword, postForgetPassword,
postForgetPasswordCaptcha, postForgetPasswordCaptcha,
postRegister, postRegister,
@ -295,8 +295,9 @@ const handleVerificationSubmit = async () => {
// 获取用户信息 // 获取用户信息
const getProfileInfo = async () => { const getProfileInfo = async () => {
const { code, data } = await fetchProfileInfo(); const { code, data } = await fetchUserInfo();
if (code === 200) { if (code === 200) {
const enterprises = data['enterprises']; const enterprises = data['enterprises'];
mobileNumber.value = data['mobile']; mobileNumber.value = data['mobile'];
accounts.value = enterprises; accounts.value = enterprises;
@ -311,6 +312,7 @@ const getProfileInfo = async () => {
selectAccountModalRef.value.open(); selectAccountModalRef.value.open();
} }
} else { } else {
userStore.setUserInfo(data);
router.push({ name: 'Trial' }); router.push({ name: 'Trial' });
} }
} }
@ -348,7 +350,7 @@ const handleSubmit = async () => {
}, 1500); }, 1500);
return; return;
} }
;
userStore.setToken(data.access_token); userStore.setToken(data.access_token);

View File

@ -23,23 +23,23 @@
</Input> </Input>
</Space> </Space>
</div> </div>
<div class="filter-row-item"> <!-- <div class="filter-row-item">-->
<span class="label">序号</span> <!-- <span class="label">序号</span>-->
<Space size="medium"> <!-- <Space size="medium">-->
<Input <!-- <Input-->
v-model:value="query.uid" <!-- v-model:value="query.uid"-->
class="!w-160px" <!-- class="!w-160px"-->
placeholder="请输入序号" <!-- placeholder="请输入序号"-->
size="medium" <!-- size="medium"-->
allowClear <!-- allowClear-->
@change="handleSearch" <!-- @change="handleSearch"-->
> <!-- >-->
<template #prefix> <!-- <template #prefix>-->
<icon-search /> <!-- <icon-search />-->
</template> <!-- </template>-->
</Input> <!-- </Input>-->
</Space> <!-- </Space>-->
</div> <!-- </div>-->
<div class="filter-row-item" v-if="query.audit_status === AuditStatus.Pending"> <div class="filter-row-item" v-if="query.audit_status === AuditStatus.Pending">
<span class="label">上传时间</span> <span class="label">上传时间</span>
<DatePicker.RangePicker <DatePicker.RangePicker

View File

@ -60,6 +60,9 @@
<template v-else-if="column.dataIndex === 'title'" #customRender="{ record }"> <template v-else-if="column.dataIndex === 'title'" #customRender="{ record }">
<TextOverTips :context="record.title" :line="3" class="title" @click="onDetail(record)" /> <TextOverTips :context="record.title" :line="3" class="title" @click="onDetail(record)" />
</template> </template>
<template v-else-if="column.dataIndex === 'origin'" #customRender="{ record }">
{{ ORIGIN_LIST.find((item) => item.value === record.origin)?.label ?? '-' }}
</template>
<template v-else-if="column.dataIndex === 'type'" #customRender="{ record }"> <template v-else-if="column.dataIndex === 'type'" #customRender="{ record }">
<div class="flex items-center"> <div class="flex items-center">
<img :src="record.type === EnumManuscriptType.Image ? icon2 : icon3" width="16" height="16" class="mr-4px" /> <img :src="record.type === EnumManuscriptType.Image ? icon2 : icon3" width="16" height="16" class="mr-4px" />
@ -113,6 +116,7 @@ import { ref } from 'vue';
import { Button, Tooltip, Table, Image } from 'ant-design-vue'; import { Button, Tooltip, Table, Image } from 'ant-design-vue';
const { Column } = Table; const { Column } = Table;
import { formatTableField, exactFormatTime } from '@/utils/tools'; import { formatTableField, exactFormatTime } from '@/utils/tools';
import { ORIGIN_LIST } from '@/views/material-center/components/raw-material/constants';
import { EnumManuscriptType } from '@/views/material-center/components/finished-products/manuscript/list/constants'; import { EnumManuscriptType } from '@/views/material-center/components/finished-products/manuscript/list/constants';
import { patchWorkAuditsAudit } from '@/api/all/generationWorkshop'; import { patchWorkAuditsAudit } from '@/api/all/generationWorkshop';
import { import {
@ -123,8 +127,8 @@ import { AuditStatus } from '@/views/material-center/components/finished-product
import { slsWithCatch } from '@/utils/stroage.ts'; import { slsWithCatch } from '@/utils/stroage.ts';
import TextOverTips from '@/components/text-over-tips' import TextOverTips from '@/components/text-over-tips';
import ImgLazyLoad from "@/components/img-lazy-load"; import ImgLazyLoad from '@/components/img-lazy-load';
import ShareModal from '@/views/material-center/components/finished-products/manuscript/components/share-manuscript-modal/share-modal.vue'; import ShareModal from '@/views/material-center/components/finished-products/manuscript/components/share-manuscript-modal/share-modal.vue';
import HoverImagePreview from '@/components/hover-image-preview'; import HoverImagePreview from '@/components/hover-image-preview';

View File

@ -20,6 +20,11 @@ export const TABLE_COLUMNS1 = [
dataIndex: 'title', dataIndex: 'title',
width: 300, width: 300,
}, },
{
title: '来源',
dataIndex: 'origin',
width: 120,
},
{ {
title: '客户意见', title: '客户意见',
dataIndex: 'customer_opinion', dataIndex: 'customer_opinion',

View File

@ -309,7 +309,7 @@ export default {
placeholder="请输入标题" placeholder="请输入标题"
size="large" size="large"
class="!w-500px" class="!w-500px"
maxlength={30} maxlength={20}
showCount showCount
/> />
</FormItem> </FormItem>

View File

@ -25,20 +25,20 @@
@change="handleSearch" @change="handleSearch"
/> />
</div> --> </div> -->
<div class="filter-row-item"> <!-- <div class="filter-row-item">-->
<span class="label">序号</span> <!-- <span class="label">序号</span>-->
<Input <!-- <Input-->
v-model:value="query.uid" <!-- v-model:value="query.uid"-->
class="!w-160px" <!-- class="!w-160px"-->
placeholder="请输入序号" <!-- placeholder="请输入序号"-->
allowClear <!-- allowClear-->
@change="handleSearch" <!-- @change="handleSearch"-->
> <!-- >-->
<template #prefix> <!-- <template #prefix>-->
<icon-search /> <!-- <icon-search />-->
</template> <!-- </template>-->
</Input> <!-- </Input>-->
</div> <!-- </div>-->
<div class="filter-row-item"> <div class="filter-row-item">
<span class="label">审核状态</span> <span class="label">审核状态</span>
<CommonSelect <CommonSelect

View File

@ -38,6 +38,11 @@ export const TABLE_COLUMNS = [
dataIndex: 'audit_status', dataIndex: 'audit_status',
width: 120, width: 120,
}, },
{
title: '来源',
dataIndex: 'origin',
width: 120,
},
{ {
title: '上传时间', title: '上传时间',
dataIndex: 'created_at', dataIndex: 'created_at',

View File

@ -47,6 +47,9 @@
<template v-else-if="column.dataIndex === 'title'" #customRender="{ record }"> <template v-else-if="column.dataIndex === 'title'" #customRender="{ record }">
<TextOverTips :context="record.title" :line="3" class="title" @click="onDetail(record)" /> <TextOverTips :context="record.title" :line="3" class="title" @click="onDetail(record)" />
</template> </template>
<template v-else-if="column.dataIndex === 'origin'" #customRender="{ record }">
{{ ORIGIN_LIST.find((item) => item.value === record.origin)?.label ?? '-' }}
</template>
<template v-else-if="column.dataIndex === 'audit_status'" #customRender="{ record }"> <template v-else-if="column.dataIndex === 'audit_status'" #customRender="{ record }">
<div <div
class="flex items-center w-fit h-28px px-8px rounded-2px" class="flex items-center w-fit h-28px px-8px rounded-2px"
@ -96,6 +99,7 @@ import { Button, Tooltip, Table, Image } from 'ant-design-vue';
const { Column } = Table; const { Column } = Table;
import { formatTableField, exactFormatTime } from '@/utils/tools'; import { formatTableField, exactFormatTime } from '@/utils/tools';
import { TABLE_COLUMNS } from './constants'; import { TABLE_COLUMNS } from './constants';
import { ORIGIN_LIST } from '@/views/material-center/components/raw-material/constants';
import { import {
CHECK_STATUS, CHECK_STATUS,
EnumManuscriptType, EnumManuscriptType,
@ -104,7 +108,7 @@ import { CUSTOMER_OPINION } from '@/views/material-center/components/finished-pr
import TextOverTips from '@/components/text-over-tips'; import TextOverTips from '@/components/text-over-tips';
import HoverImagePreview from '@/components/hover-image-preview'; import HoverImagePreview from '@/components/hover-image-preview';
import ImgLazyLoad from "@/components/img-lazy-load"; import ImgLazyLoad from '@/components/img-lazy-load';
import icon1 from '@/assets/img/media-account/icon-delete.png'; import icon1 from '@/assets/img/media-account/icon-delete.png';
import icon2 from '@/assets/img/creative-generation-workshop/icon-photo.png'; import icon2 from '@/assets/img/creative-generation-workshop/icon-photo.png';

View File

@ -1,7 +1,7 @@
export const INITIAL_QUERY = { export const INITIAL_QUERY = {
title: '', title: '',
// project_ids: [], // project_ids: [],
uid: '', // uid: '',
audit_status: undefined, audit_status: undefined,
created_at: [], created_at: [],
sort_column: undefined, sort_column: undefined,

View File

@ -0,0 +1,540 @@
<script lang="tsx">
import { Drawer, Button, Upload, Table, Input, Progress, message, Select, Modal, Tag, Tooltip } from 'ant-design-vue';
const { Column } = Table;
const { TextArea } = Input;
const { Option } = Select;
// import CommonSelect from '@/components/common-select';
import ImgLazyLoad from '@/components/img-lazy-load';
// import TextOverTips from '@/components/text-over-tips';
import axios from 'axios';
import { formatFileSize, getVideoInfo, getFileExtension } from '@/utils/tools';
import { getRawMaterialTagsList, postBatchRawMaterial, posRawMaterialTags } from '@/api/all/generationWorkshop';
import { getFilePreSignedUrl } from '@/api/all/common';
import {
imageExtensions,
videoExtensions,
documentExtensions,
} from '@/views/material-center/components/raw-material/constants';
// import icon1 from '@/assets/img/media-account/icon-delete.png';
import icon2 from '../../img/icon-no-text.png';
enum EnumUploadStatus {
done = 'done',
error = 'error',
uploading = 'uploading',
}
export default defineComponent({
emits: ['update'],
setup(_, { emit, expose }) {
const visible = ref(false);
const submitLoading = ref(false);
const uploadData = ref([]);
const tagData = ref([]);
const modalRef = ref(null);
const isDragOver = ref(false);
const uploadSuccessNum = computed(() => {
return uploadData.value.filter((item) => item.uploadStatus === EnumUploadStatus.done).length;
});
// 添加拖拽事件处理函数
const handleDragEnter = (e) => {
e.preventDefault();
isDragOver.value = true;
};
const handleDragOver = (e) => {
e.preventDefault();
isDragOver.value = true;
};
const handleDragLeave = (e) => {
e.preventDefault();
isDragOver.value = false;
};
const handleDrop = (e) => {
e.preventDefault();
isDragOver.value = false;
};
const getTagData = async () => {
const { code, data } = await getRawMaterialTagsList();
if (code === 200) {
tagData.value = data ?? [];
}
};
const handleTagChange = (value, option, record) => {
// console.log('handleTagChange', value);
if (value.length < 6) {
record.tag_ids = value;
} else {
message.warning('最多选择5个');
}
};
// 添加处理标签输入的函数
const handleTagInputPressEnter = async (e, record) => {
const inputValue = e.target.value.trim();
if (!inputValue) return;
const _target = tagData.value.find((item) => item.name === inputValue);
if (_target) {
return;
}
if (record.tag_ids.length === 5) {
// message.warning('最多选择5个');
return;
}
try {
const { code, data } = await posRawMaterialTags({ name: inputValue });
if (code === 200 && data) {
tagData.value.push({
id: data.id,
name: data.name,
});
e.target.value = '';
record.tag_ids = record.tag_ids.filter((item) => item !== inputValue);
record.tag_ids.push(data.id);
}
} catch (error) {
message.error('添加标签失败');
}
};
const onCancel = () => {
if (!uploadData.value.length) {
onClose();
return;
}
modalRef.value = Modal.warning({
title: '确定要取消上传吗?',
content: '取消后上传将中断,已传输的内容不会保存',
cancelText: '取消',
okText: '确定',
centered: true,
footer: (
<div class="flex items-center justify-end mt-24px">
<Button
onClick={() => {
modalRef.value.destroy();
onClose();
}}
class="mr-12px"
>
确认取消
</Button>
<Button
type="primary"
onClick={() => {
modalRef.value.destroy();
}}
>
继续上传
</Button>
</div>
),
});
};
const open = () => {
getTagData();
visible.value = true;
};
const onClose = () => {
visible.value = false;
uploadData.value = [];
tagData.value = [];
modalRef.value?.destroy();
modalRef.value = null;
submitLoading.value = false;
isDragOver.value = false;
};
const handleUpload = async (option) => {
const { file } = option;
const { name, size, type, uid } = file;
let statusText = '';
const ext = name.slice(name.lastIndexOf('.')).toLowerCase();
const isImage = imageExtensions.includes(ext);
const isVideo = videoExtensions.includes(ext);
const isDocument = documentExtensions.includes(ext);
if (!isImage && !isVideo && !isDocument) {
statusText = '当前格式不支持';
} else if (
(isImage && size > 20 * 1024 * 1024) || // 图片大于20MB
(isVideo && size > 1000 * 1024 * 1024) || // 视频大于1000MB
(isDocument && size > 20 * 1024 * 1024) // 文档大于20MB
) {
statusText = '文件大小超出限制';
}
const { fileTypeLabel, cover, fileType } = await getFileInfo(file);
const currentData = {
uid,
name: name.slice(0, 20),
// type,
type: fileType,
uploadStatus: statusText ? EnumUploadStatus.error : EnumUploadStatus.uploading,
statusText,
percent: 0,
size,
fileTypeLabel,
tag_ids: [],
cover,
file: '',
};
uploadData.value.push(currentData);
if (statusText) return;
try {
const response = await getFilePreSignedUrl({ suffix: getFileExtension(name) });
const { upload_url, file_url } = response?.data;
const _target = uploadData.value.find((item) => item.uid === uid);
_target.file = file_url;
if (!upload_url) {
throw new Error('未能获取有效的预签名上传地址');
}
const blob = new Blob([file], { type });
await axios.put(upload_url, blob, {
headers: { 'Content-Type': type },
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(progressEvent.progress * 100);
_target.percent = percentCompleted;
percentCompleted === 100 && (_target.uploadStatus = EnumUploadStatus.done);
},
});
} catch {
currentData.uploadStatus = EnumUploadStatus.error;
}
};
const getFileInfo = async (file: any) => {
const { name } = file;
const ext = name.slice(name.lastIndexOf('.')).toLowerCase();
if (imageExtensions.includes(ext)) {
return {
fileTypeLabel: '图片',
fileType: 0,
cover: URL.createObjectURL(file),
};
} else if (videoExtensions.includes(ext)) {
const { firstFrame } = await getVideoInfo(file);
return {
fileTypeLabel: '视频',
fileType: 1,
cover: firstFrame,
};
} else if (documentExtensions.includes(ext)) {
return {
fileTypeLabel: '文本',
fileType: 2,
cover: icon2,
};
} else {
return {
fileType: '',
fileTypeLabel: '其他',
cover: '',
};
}
};
const onConfirm = async () => {
try {
const hasUploading = uploadData.value.some((item) => item.uploadStatus === EnumUploadStatus.uploading);
if (hasUploading) {
modalRef.value = Modal.warning({
title: '上传未完成',
content: <p>当前原料正在上传中关闭弹窗将导致上传失败请等待上传完成后再点击确定</p>,
okText: '我知道了',
centered: true,
});
return;
}
submitLoading.value = true;
const raw_materials = uploadData.value.filter((item) => item.uploadStatus !== EnumUploadStatus.error);
const { code } = await postBatchRawMaterial({ raw_materials });
if (code === 200) {
message.success('上传成功');
emit('update');
onClose();
}
} finally {
submitLoading.value = false;
}
};
const openDeleteModal = (file) => {
modalRef.value = Modal.warning({
title: '确定删除该文件吗?',
content: <p class="h-22px"></p>,
cancelText: '取消',
okText: '确定',
centered: true,
footer: (
<div class="flex items-center justify-end mt-24px">
<Button
onClick={() => {
modalRef.value.destroy();
}}
class="mr-12px"
>
取消
</Button>
<Button
type="primary"
onClick={() => {
uploadData.value = uploadData.value.filter((item) => item.uid !== file.uid);
modalRef.value.destroy();
}}
>
确定
</Button>
</div>
),
});
};
const renderUploadStatus = (record) => {
if (record.uploadStatus === EnumUploadStatus.error) {
return (
<div class="w-100px">
<p class="upload-text">上传失败</p>
<p class="upload-text !color-#F64B31">{record.statusText}</p>
</div>
);
}
return (
<div>
<p class="upload-text w-100px">{record.uploadStatus === EnumUploadStatus.done ? '上传成功' : '上传中'}</p>
<Progress percent={record.percent} class="m-0 p-0 " />
</div>
);
};
const renderUploadList = () => {
if (!uploadData.value.length) return null;
return (
<>
<p class="cts !color-#939499 mb-10px">
已上传<span class="!color-#211F24">{`${uploadSuccessNum.value}/${uploadData.value.length}`}</span>
</p>
<Table
ref="tableRef"
dataSource={uploadData.value}
pagination={false}
class="w-100% flex-1 overflow-hidden"
scroll={{ y: '100%' }}
>
<Column
title="文件名称"
dataIndex="name"
key="name"
width={380}
ellipsis={true}
customRender={({ text, record }) => {
return (
<div class="flex items-center justify-end">
<ImgLazyLoad width={64} height={64} src={record.cover} class="!rounded-8px mr-16px flex-shrink-0" />
<TextArea
v-model:value={record.name}
placeholder="请输入文件名称"
size="large"
class="w-full !h-72px no-resize"
showCount
maxlength={20}
autoSize={false}
/>
</div>
);
}}
/>
<Column
title="上传状态"
dataIndex="uploadStatus"
key="uploadStatus"
width={164}
customRender={({ text, record }) => renderUploadStatus(record)}
/>
<Column
title="标签"
dataIndex="tag_ids"
key="tag_ids"
width={243}
customRender={({ text, record }) => {
return (
<Select
disabled={record.uploadStatus === EnumUploadStatus.uploading}
value={record.tag_ids}
mode="tags"
size="middle"
placeholder="请选择标签"
allowClear
autoClearSearchValue
class="w-full"
showSearch
showArrow
maxTagCount={5}
options={tagData.value}
optionFilterProp="name"
field-names={{ label: 'name', value: 'id' }}
onChange={(value, option) => handleTagChange(value, option, record)}
onInputKeyDown={(e) => {
// 检测回车键
if (e.key === 'Enter') {
e.preventDefault();
handleTagInputPressEnter(e, record);
}
}}
v-slots={{
tagRender: (val) => {
const { label, value } = val;
if (label.length > 4) {
return (
<div class="my-2px">
<Tooltip title={label}>
<Tag
closable
onClose={() => (record.tag_ids = record.tag_ids.filter((item) => item !== value))}
class="mr-0 !color-#55585F h-20px !lh-20px !text-12px"
>
{label.slice(0, 3) + '...'}
</Tag>
</Tooltip>
</div>
);
} else {
return (
<div class="my-2px">
<Tag
closable
onClose={() => (record.tag_ids = record.tag_ids.filter((item) => item !== value))}
class="mr-0 !color-#55585F h-20px !lh-20px !text-12px"
>
{label}
</Tag>
</div>
);
}
},
}}
/>
);
}}
/>
<Column title="类型" dataIndex="fileTypeLabel" key="fileTypeLabel" width={80} />
<Column
title="大小"
dataIndex="size"
key="size"
width={100}
customRender={({ record }) => {
return formatFileSize(record.size);
}}
/>
<Column
title="操作"
key="action"
width={80}
fixed="right"
align="right"
customRender={({ text, record }) => {
return (
<div class="flex items-center justify-end">
<icon-delete
class="mr-8px color-#737478 cursor-pointer"
size="14"
onClick={() => openDeleteModal(record)}
/>
</div>
);
}}
/>
</Table>
</>
);
};
expose({
open,
});
return () => (
<Drawer
width={1100}
title="上传到原料库"
rootClassName="xt-add-raw-material-drawer"
v-model:open={visible.value}
onClose={onClose}
maskClosable={false}
>
<section class="content flex-1 pt-8px px-24px pb-24px overflow-hidden flex flex-col">
<div class="rounded-16px bg-#F7F8FA p-16px flex flex-col items-center mb-24px">
<Upload
file-list={uploadData.value}
action="/"
multiple
customRequest={handleUpload}
class="w-full mb-16px"
showUploadList={false}
>
<div
class={`upload-box rounded-8px cursor-pointer h-100px w-full border border-dashed border-#D7D7D9 flex flex-col items-center justify-center w-full ${
isDragOver.value ? 'bg-#F0EDFF' : 'bg-#F7F8FA'
}`}
onDragenter={handleDragEnter}
onDragover={handleDragOver}
onDragleave={handleDragLeave}
onDrop={handleDrop}
>
<icon-plus size="14" class="mb-10px color-#55585F" />
<span class="cts">点击或拖拽文件到此处上传</span>
</div>
</Upload>
<p class="mb-4px cts !color-#939499 !text-12px !lh-20px">{`视频格式MP4、AVI、MOV大小<1000MB`}</p>
<p class="mb-4px cts !color-#939499 !text-12px !lh-20px">{`图片格式PNG、JPG、JPEG、GIF、WEBP、BMP大小<20MB`}</p>
<p class="cts !color-#939499 !text-12px !lh-20px">{`文本格式TXT、DOC、DOCX、PDF大小<20MB`}</p>
</div>
{renderUploadList()}
</section>
<footer class="footer h-68px px-24px flex items-center justify-end">
<div class="flex items-center">
<Button size="large" onClick={onCancel} class="mr-12px">
取消
</Button>
<Button size="large" type="primary" onClick={onConfirm} loading={submitLoading.value}>
确定
</Button>
</div>
</footer>
</Drawer>
);
},
});
</script>
<style lang="scss">
@import './style.scss';
</style>

View File

@ -0,0 +1,134 @@
.xt-add-raw-material-drawer {
.ant-drawer-content {
border-radius: 16px 0 0 16px;
.ant-drawer-header {
height: 58px;
border-bottom: none;
}
.ant-upload {
width: 100%;
}
.ant-select {
//.ant-select-selection-overflow-item {
// max-width: 50%;
//}
}
.ant-table {
.ant-table-container {
display: flex;
flex-direction: column;
}
.ant-table-header {
flex-shrink: 0;
.ant-table-cell {
&::before {
display: none;
}
}
}
}
.ant-input-textarea {
&.no-resize {
textarea {
resize: none;
}
}
}
.ant-drawer-body {
padding: 0;
display: flex;
flex-direction: column;
.cts {
color: #211f24;
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
}
.content {
.upload-box {
transition: all 0.3s;
&:hover {
border-color: #6d4cfe;
}
}
.upload-text {
color: #000;
text-align: center;
font-family: $font-family-regular;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
.ant-progress {
display: flex;
align-items: center;
justify-content: center;
.ant-progress-outer {
position: relative;
width: 100px !important;
height: 6px !important;
margin: 0;
padding: 0;
.ant-progress-inner {
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
height: 6px !important;
background: var(--BG-200, #f2f3f5);
.ant-progress-bg {
height: 6px !important;
background-color: #6d4cfe;
}
}
}
.ant-progress-text {
color: var(--Text-1, #211f24);
font-family: $font-family-regular;
font-size: 12px;
font-style: normal;
font-weight: 400;
.anticon {
font-size: 16px;
}
}
&.ant-progress-status-success {
.ant-progress-outer {
.ant-progress-bg {
background-color: #25c883;
}
}
.ant-progress-text {
color: #25c883;
}
}
}
}
.footer {
}
}
}
}

View File

@ -0,0 +1,292 @@
<script lang="tsx">
import { ref } from 'vue';
import { Button, Modal, Form, FormItem, Input, message, Select, Tag, Tooltip } from 'ant-design-vue';
import TextOverTips from '@/components/text-over-tips';
const { TextArea } = Input;
import { formatFileSize, exactFormatTime } from '@/utils/tools';
import {
getRawMaterialDetail,
putRawMaterial,
getRawMaterialTagsList,
posRawMaterialTags,
} from '@/api/all/generationWorkshop';
import { TABS_LIST, ORIGIN_LIST } from '@/views/material-center/components/raw-material/constants';
const INITIAL_FORM = {
name: '',
tag_ids: [],
uid: '',
type: '',
origin: '',
size: '',
created_at: '',
uploader: {
name: '',
mobile: '',
},
};
export default {
setup(props, { emit, expose }) {
const formRef = ref();
const id = ref('');
const visible = ref(false);
const submitLoading = ref(false);
const form = ref(cloneDeep(INITIAL_FORM));
const tagOptions = ref([]);
const rules = {
mobile: [
{
required: true,
validator: (_rule, value) => {
if (!value) {
return Promise.reject('请填写手机号');
}
if (!/^1[3-9]\d{9}$/.test(value)) {
return Promise.reject('手机号格式不正确');
} else {
return Promise.resolve();
}
},
trigger: ['blur', 'change'],
},
],
operator_name: [{ required: true, message: '请输入运营人员' }],
};
const getMaterialDetail = async () => {
const { code, data } = await getRawMaterialDetail(id.value);
if (code === 200) {
form.value = data;
}
};
const getTags = async () => {
const { code, data } = await getRawMaterialTagsList();
if (code === 200) {
tagOptions.value = data;
}
};
const reset = () => {
form.value = cloneDeep(INITIAL_FORM);
tagOptions.value = [];
submitLoading.value = false;
id.value = '';
};
const open = (materialId = '') => {
id.value = materialId;
getMaterialDetail();
getTags();
visible.value = true;
};
const onClose = () => {
reset();
visible.value = false;
};
const onSubmit = () => {
formRef.value.validate().then(async () => {
try {
submitLoading.value = true;
const { code } = await putRawMaterial({ id: id.value, ...form.value });
if (code === 200) {
message.success('修改成功');
emit('update');
onClose();
}
} finally {
submitLoading.value = false;
}
});
};
const handleTagChange = (value) => {
if (value.length < 6) {
form.value.tag_ids = value;
} else {
message.warning('最多选择5个');
}
};
const deleteTag = (tagId) => {
form.value.tag_ids = form.value.tag_ids.filter((item) => item !== tagId);
};
// 添加处理标签输入的函数
const handleTagInputPressEnter = async (e) => {
const inputValue = e.target.value.trim();
if (!inputValue) return;
const _target = tagOptions.value.find((item) => item.name === inputValue);
if (_target) {
return;
}
if (form.value.tag_ids.length === 5) {
// message.warning('最多选择5个');
return;
}
try {
const { code, data } = await posRawMaterialTags({ name: inputValue });
if (code === 200 && data) {
tagOptions.value.push({
id: data.id,
name: data.name,
});
e.target.value = '';
form.value.tag_ids = form.value.tag_ids.filter((item) => item !== inputValue);
form.value.tag_ids.push(data.id);
}
} catch (error) {
message.error('添加标签失败');
}
};
expose({ open });
return () => (
<Modal
v-model:open={visible.value}
title="编辑素材"
wrapClassName="edit-material-modal"
width="480px"
centered
maskClosable={false}
onCancel={onClose}
v-slots={{
footer: () => (
<div class="flex">
<Button onClick={onClose}>取消</Button>
<Button type="primary" loading={submitLoading.value} onClick={onSubmit}>
确定
</Button>
</div>
),
}}
>
<Form
ref={formRef}
model={form.value}
rules={rules}
layout="horizontal"
labelAlign="right"
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
>
<FormItem label="文件名称" name="name">
<TextArea
v-model:value={form.value.name}
placeholder="请输入..."
size="large"
class="!h-72px"
showCount
maxlength={20}
/>
</FormItem>
<FormItem label="序号" name="uid">
<Input v-model:value={form.value.uid} placeholder="请输入..." size="large" disabled />
</FormItem>
<FormItem label="类型" name="type">
<Input
value={TABS_LIST.find((item) => item.value === form.value.type)?.label ?? '-'}
placeholder="请输入..."
size="large"
disabled
/>
</FormItem>
<FormItem label="标签" name="tag_ids" v-model:value={form.value.tag_ids}>
<Select
value={form.value.tag_ids}
mode="tags"
size="large"
placeholder="请选择标签"
allowClear
autoClearSearchValue
class="w-full"
showSearch
showArrow
maxTagCount={5}
optionFilterProp="name"
options={tagOptions.value}
field-names={{ label: 'name', value: 'id' }}
onChange={handleTagChange}
onInputKeyDown={(e) => {
// 检测回车键
if (e.key === 'Enter') {
e.preventDefault();
handleTagInputPressEnter(e);
}
}}
v-slots={{
tagRender: (val) => {
const { label, value } = val;
if (label.length > 5) {
return (
<div class="my-2px">
<Tooltip title={label}>
<Tag
closable
onClose={() => (form.value.tag_ids = form.value.tag_ids.filter((item) => item !== value))}
class="mr-0"
>
{`${label.slice(0, 5)}...`}
</Tag>
</Tooltip>
</div>
);
} else {
return (
<div class="my-2px">
<Tag
closable
onClose={() => (form.value.tag_ids = form.value.tag_ids.filter((item) => item !== value))}
class=" mr-0 "
>
{label}
</Tag>
</div>
);
}
},
}}
/>
</FormItem>
<FormItem label="来源" name="origin">
<Input
value={ORIGIN_LIST.find((item) => item.value === form.value.origin)?.label ?? '-'}
placeholder="请输入..."
size="large"
disabled
/>
</FormItem>{' '}
<FormItem label="大小" name="size">
<Input value={formatFileSize(form.value.size)} placeholder="请输入..." size="large" disabled />
</FormItem>
<FormItem label="上传时间" name="created_at">
<Input value={exactFormatTime(form.value.created_at)} placeholder="请输入..." size="large" disabled />
</FormItem>
<FormItem label="上传人员" name="uploader">
<Input
value={form.value.uploader.name || form.value.uploader.mobile}
placeholder="请输入..."
size="large"
disabled
/>
</FormItem>
</Form>
</Modal>
);
},
};
</script>
<style lang="scss">
@import './style.scss';
</style>

View File

@ -16,13 +16,30 @@
</Input> </Input>
</div> </div>
<div class="filter-row-item"> <div class="filter-row-item">
<span class="label">序号</span> <span class="label">标签</span>
<Input v-model:value="query.uid" class="!w-160px" placeholder="请输入序号" allowClear @change="handleSearch"> <Select
<template #prefix> v-model:value="query.tag_ids"
<icon-search /> :field-names="{ label: 'name', value: 'id' }"
</template> :maxTagCount="1"
</Input> :options="tagData"
allowClear
class="!w-200px"
mode="multiple"
optionFilterProp="name"
placeholder="请选择标签"
showArrow
showSearch
@change="handleSearch"
/>
</div> </div>
<!-- <div class="filter-row-item">-->
<!-- <span class="label">序号</span>-->
<!-- <Input v-model:value="query.uid" class="!w-160px" placeholder="请输入序号" allowClear @change="handleSearch">-->
<!-- <template #prefix>-->
<!-- <icon-search />-->
<!-- </template>-->
<!-- </Input>-->
<!-- </div>-->
<div class="filter-row-item"> <div class="filter-row-item">
<span class="label">上传时间</span> <span class="label">上传时间</span>
<DatePicker.RangePicker <DatePicker.RangePicker
@ -54,8 +71,9 @@
<script setup> <script setup>
import { defineEmits, defineProps, ref, nextTick } from 'vue'; import { defineEmits, defineProps, ref, nextTick } from 'vue';
import { Button, Input, DatePicker } from 'ant-design-vue'; import { Button, Input, DatePicker, Select } from 'ant-design-vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { getRawMaterialTagsList } from '@/api/all/generationWorkshop';
const props = defineProps({ const props = defineProps({
query: { query: {
@ -67,6 +85,14 @@ const props = defineProps({
const emits = defineEmits(['search', 'reset', 'update:query']); const emits = defineEmits(['search', 'reset', 'update:query']);
const created_at = ref([]); const created_at = ref([]);
const tagData = ref([]);
const getTagData = async () => {
const { code, data } = await getRawMaterialTagsList();
if (code === 200) {
tagData.value = data ?? [];
}
};
const handleSearch = () => { const handleSearch = () => {
emits('update:query', props.query); emits('update:query', props.query);
@ -96,4 +122,18 @@ const handleReset = () => {
created_at.value = []; created_at.value = [];
emits('reset'); emits('reset');
}; };
onMounted(() => {
getTagData();
});
</script> </script>
<style lang="scss" scoped>
.common-filter-wrap {
:deep(.ant-select) {
.ant-select-selection-overflow-item {
max-width: 50%;
}
}
}
</style>

View File

@ -6,7 +6,7 @@
</div> </div>
<template #footer> <template #footer>
<Button size="medium" @click="onClose">取消</Button> <Button size="medium" @click="onClose">取消</Button>
<Button type="primary" class="ml-16px" danger size="medium" @click="onDelete">确认删除</Button> <Button :loading="loading" class="ml-16px" danger size="medium" type="primary" @click="onDelete">确认删除</Button>
</template> </template>
</Modal> </Modal>
</template> </template>
@ -17,17 +17,21 @@ import { Button, Modal, message } from 'ant-design-vue';
import { deleteRawMaterial, batchDeleteRawMaterials } from '@/api/all/generationWorkshop'; import { deleteRawMaterial, batchDeleteRawMaterials } from '@/api/all/generationWorkshop';
import icon1 from '@/assets/img/media-account/icon-warn-1.png'; import icon1 from '@/assets/img/media-account/icon-warn-1.png';
const update = inject('update'); // const update = inject('update');
const emits = defineEmits(['update', 'batchUpdate']);
const visible = ref(false); const visible = ref(false);
const fileId = ref(null); const fileId = ref(null);
const fileName = ref(''); const fileName = ref('');
const loading = ref(false);
const isBatch = computed(() => Array.isArray(fileId.value)); const isBatch = computed(() => Array.isArray(fileId.value));
function onClose() { function onClose() {
visible.value = false; visible.value = false;
fileId.value = null; fileId.value = null;
loading.value = false;
fileName.value = ''; fileName.value = '';
} }
@ -40,6 +44,8 @@ const open = (record) => {
}; };
async function onDelete() { async function onDelete() {
try {
loading.value = true;
const _fn = isBatch.value ? batchDeleteRawMaterials : deleteRawMaterial; const _fn = isBatch.value ? batchDeleteRawMaterials : deleteRawMaterial;
const _params = isBatch.value ? { ids: fileId.value } : fileId.value; const _params = isBatch.value ? { ids: fileId.value } : fileId.value;
const { code } = await _fn(_params); const { code } = await _fn(_params);
@ -49,6 +55,9 @@ async function onDelete() {
onClose(); onClose();
} }
} finally {
loading.value = false;
}
} }
defineExpose({ open }); defineExpose({ open });

View File

@ -33,8 +33,13 @@
</template> </template>
<template v-if="column.dataIndex === 'name'" #customRender="{ record }"> <template v-if="column.dataIndex === 'name'" #customRender="{ record }">
<div class="flex items-center"> <div class="flex items-center">
<HoverImagePreview :src="record.cover"> <HoverImagePreview :src="record.type === RawMaterialType.Text ? '' : record.cover">
<ImgLazyLoad :width="64" :height="64" :src="record.cover" class="!rounded-6px mr-16px" /> <ImgLazyLoad
:height="64"
:src="record.type === RawMaterialType.Text ? icon2 : record.cover"
:width="64"
class="!rounded-8px mr-16px"
/>
</HoverImagePreview> </HoverImagePreview>
<div class="flex-1 flex flex-col overflow-hidden"> <div class="flex-1 flex flex-col overflow-hidden">
<TextOverTips :context="record.name" :line="1" class="cts mb-4px regular" /> <TextOverTips :context="record.name" :line="1" class="cts mb-4px regular" />
@ -45,6 +50,17 @@
<template v-else-if="column.dataIndex === 'type'" #customRender="{ record }"> <template v-else-if="column.dataIndex === 'type'" #customRender="{ record }">
{{ TABS_LIST.find((item) => item.value === record.type)?.label ?? '-' }} {{ TABS_LIST.find((item) => item.value === record.type)?.label ?? '-' }}
</template> </template>
<template v-else-if="column.dataIndex === 'tags'" #customRender="{ record }">
<div v-if="record.tags.length > 0" class="flex flex-wrap gap-4px">
<Tag v-for="tag in record.tags" :key="tag.id" class="mr-0 rounded-4px bg-#F2F3F5 px-8px">
<Tooltip v-if="tag.name.length > 5" :title="tag.name">
<span class="cts !color-#55585F !lh-20px !text-12px regular"> {{ `${tag.name.slice(0, 5)}...` }} </span>
</Tooltip>
<span v-else class="cts !color-#55585F !lh-20px !text-12px regular"> {{ tag.name }} </span>
</Tag>
</div>
<template v-else> -</template>
</template>
<template v-else-if="column.dataIndex === 'size'" #customRender="{ record }"> <template v-else-if="column.dataIndex === 'size'" #customRender="{ record }">
<span class="cts num">{{ formatFileSize(record.size) }}</span> <span class="cts num">{{ formatFileSize(record.size) }}</span>
</template> </template>
@ -59,8 +75,11 @@
</template> </template>
<template v-else-if="column.dataIndex === 'operation'" #customRender="{ record }"> <template v-else-if="column.dataIndex === 'operation'" #customRender="{ record }">
<div class="flex items-center"> <div class="flex items-center">
<img class="mr-8px cursor-pointer" :src="icon1" width="14" height="14" @click="onDelete(record)" /> <icon-delete class="mr-8px color-#737478 cursor-pointer" size="14" @click="onDelete(record)" />
<Button type="primary" ghost size="small" @click="onDownload(record)">下载</Button> <Button class="mr-8px !h-24px !px-12px" ghost size="small" type="primary" @click="onEdit(record)"
>编辑
</Button>
<Button class="!h-24px !px-12px" ghost size="small" type="primary" @click="onDownload(record)">下载</Button>
</div> </div>
</template> </template>
<template v-else #customRender="{ record }"> <template v-else #customRender="{ record }">
@ -72,23 +91,21 @@
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { Button, Tooltip, Table, Image } from 'ant-design-vue'; import { Button, Tooltip, Table, Tag } from 'ant-design-vue';
const { Column } = Table; const { Column } = Table;
import { formatTableField, exactFormatTime, formatFileSize, downloadByUrl } from '@/utils/tools'; import { formatTableField, exactFormatTime, formatFileSize, downloadByUrl } from '@/utils/tools';
import { slsWithCatch } from '@/utils/stroage.ts'; // import { slsWithCatch } from '@/utils/stroage.ts';
import { TABS_LIST, ORIGIN_LIST } from '../../constants'; import { TABS_LIST, ORIGIN_LIST, RawMaterialType } from '../../constants';
import TextOverTips from '@/components/text-over-tips'; import TextOverTips from '@/components/text-over-tips';
// import ShareModal from '@/views/material-center/components/finished-products/manuscript/components/share-manuscript-modal/share-modal.vue'; // import ShareModal from '@/views/material-center/components/finished-products/manuscript/components/share-manuscript-modal/share-modal.vue';
import HoverImagePreview from '@/components/hover-image-preview'; import HoverImagePreview from '@/components/hover-image-preview';
import ImgLazyLoad from '@/components/img-lazy-load'; import ImgLazyLoad from '@/components/img-lazy-load';
import icon1 from '@/assets/img/media-account/icon-delete.png'; // import icon1 from '@/assets/img/media-account/icon-delete.png';
// import icon2 from '@/assets/img/creative-generation-workshop/icon-photo.png'; import icon2 from '../../img/icon-no-text.png';
// import icon3 from '@/assets/img/creative-generation-workshop/icon-video.png';
// import icon4 from '@/assets/img/error-img.png';
const emits = defineEmits(['sorterChange', 'delete', 'select', 'selectAll']); const emits = defineEmits(['sorterChange', 'delete', 'select', 'selectAll', 'edit']);
const router = useRouter(); const router = useRouter();
const props = defineProps({ const props = defineProps({
@ -129,6 +146,9 @@ const onDelete = (item) => {
const onDownload = (item) => { const onDownload = (item) => {
downloadByUrl(item.file); downloadByUrl(item.file);
}; };
const onEdit = (item) => {
emits('edit', item);
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -0,0 +1,74 @@
<template>
<Modal
v-model:open="visible"
:title="isEdit ? '编辑标签' : '添加新标签'"
centered
width="400px"
@cancel="onClose"
>
<Form ref="formRef" :model="form" :rules="rules" auto-label-width layout="horizontal">
<FormItem :label="isEdit ? '标签名称' : '新标签名称'" name="name" required>
<Input v-model:value="form.name" placeholder="请输入…" />
</FormItem>
</Form>
<template #footer>
<Button @click="onClose">取消</Button>
<Button class="ml-16px" type="primary" @click="onSubmit">确认</Button>
</template>
</Modal>
</template>
<script setup>
import { ref, nextTick } from 'vue';
import { Button, Modal, Form, FormItem, Input, message } from 'ant-design-vue';
import { posRawMaterialTags, putRawMaterialTag } from '@/api/all/generationWorkshop';
const emits = defineEmits(['success', 'close']);
const visible = ref(false);
const isEdit = ref(false);
const formRef = ref();
const form = ref({ name: '' });
const tagId = ref('');
const rules = {
name: [{ required: true, message: '请输入标签名称' }],
};
function resetForm() {
nextTick(() => {
formRef.value?.clearValidate();
});
}
function onClose() {
visible.value = false;
form.value.name = '';
tagId.value = '';
isEdit.value = false;
resetForm();
}
const open = (record = {}) => {
const { id = '', name = '' } = record;
tagId.value = id;
isEdit.value = !!id;
form.value.name = name;
visible.value = true;
};
async function onSubmit() {
formRef.value.validate().then(async () => {
const _fn = isEdit.value ? putRawMaterialTag : posRawMaterialTags;
const _params = isEdit.value ? { id: tagId.value, ...form.value } : form.value;
const { code } = await _fn(_params);
if (code === 200) {
message.success(isEdit.value ? '编辑成功' : '添加成功');
emits('success');
onClose();
}
});
}
defineExpose({ open });
</script>

View File

@ -0,0 +1,56 @@
<!--
* @Author: RenXiaoDong
* @Date: 2025-06-26 17:23:52
-->
<template>
<Modal v-model:open="visible" centered title="删除标签" width="400px" @cancel="onClose">
<div class="flex items-center">
<img :src="icon1" class="mr-12px" height="20" width="20" />
<span>确认删除 "{{ tagName }}" 这个标签吗</span>
</div>
<template #footer>
<Button size="large" @click="onClose">取消</Button>
<Button class="ml-16px" danger size="large" type="primary" @click="onSubmit">确认删除</Button>
</template>
</Modal>
</template>
<script setup>
import { ref } from 'vue';
import { Button, message, Modal } from 'ant-design-vue';
import { deleteRawMaterialTag } from '@/api/all/generationWorkshop';
import icon1 from '@/assets/img/media-account/icon-warn-1.png';
const emits = defineEmits(['success', 'close']);
const visible = ref(false);
const tagId = ref('');
const tagName = ref('');
const onClose = () => {
visible.value = false;
tagId.value = '';
tagName.value = '';
emits('close');
};
const open = (record) => {
const { id = '', name = '' } = record;
tagId.value = id;
tagName.value = name;
visible.value = true;
};
const onSubmit = async () => {
const { code } = await deleteRawMaterialTag(tagId.value);
if (code === 200) {
message.success('删除成功');
emits('success');
onClose();
}
};
defineExpose({ open });
</script>

View File

@ -0,0 +1,132 @@
<!--
* @Author: RenXiaoDong
* @Date: 2025-06-25 17:58:28
-->
<template>
<Modal
v-model:open="visible"
:footer="null"
:maskClosable="false"
centered
title="标签管理"
width="800px"
wrapClassName="raw-material-tags-manage-modal"
@cancel="close"
>
<div class="flex items-center justify-between mb-16px">
<div class="filter-row-item flex items-center">
<span class="s1 !color-#211F24 mr-12px">标签名称</span>
<Input
v-model:value="query.name"
allowClear
class="w-240px"
placeholder="请输入..."
size="medium"
@change="handleSearch"
>
<template #prefix>
<icon-search />
</template>
</Input>
</div>
<Button type="primary" @click="openAdd">
<template #icon>
<icon-plus class="mr-8px" size="16" />
</template>
<template #default>添加新标签</template>
</Button>
</div>
<div class="h-300px">
<div v-if="showDataSource.length" class="tag-list">
<Tooltip v-for="tag in showDataSource" :key="tag.id" placement="bottom" title="双击修改标签名">
<div class="tag-item cursor-pointer" @dblclick="openEdit(tag)">
<span>{{ tag.name }}</span>
<img :src="iconDelete" class="delete-icon" @click="openDelete(tag)" />
</div>
</Tooltip>
</div>
<template v-else>
<NoData>
<span class="s1 mb-32px mt-8px">暂无标签</span>
<Button size="large" type="primary" @click="openAdd">
<template #icon>
<icon-plus class="mr-8px" size="16" />
</template>
<template #default>去添加</template>
</Button>
</NoData>
</template>
</div>
<AddTag ref="addTagRef" @success="update" />
<DeleteTag ref="deleteTagRef" @success="update" />
</Modal>
</template>
<script setup>
import { ref } from 'vue';
import { Button, Modal, Input, Tooltip } from 'ant-design-vue';
import { getRawMaterialTagsList } from '@/api/all/generationWorkshop';
import AddTag from './add-tag.vue';
import DeleteTag from './delete-tag.vue';
import iconDelete from '@/assets/img/media-account/icon-delete-1.png';
const emit = defineEmits(['update']);
const visible = ref(false);
const allDataSource = ref([]);
const showDataSource = ref([]);
const addTagRef = ref(null);
const deleteTagRef = ref(null);
const query = ref({
name: '',
});
const getData = async () => {
const { code, data } = await getRawMaterialTagsList();
if (code === 200) {
allDataSource.value = data ?? [];
showDataSource.value = allDataSource.value;
}
};
const handleSearch = () => {
showDataSource.value = allDataSource.value.filter((item) => item.name.includes(query.value.name));
};
const open = () => {
visible.value = true;
getData();
};
const close = () => {
showDataSource.value = [];
allDataSource.value = [];
query.value.name = '';
visible.value = false;
};
const openAdd = () => {
// 打开新增标签弹窗
addTagRef.value.open();
};
function openEdit(record) {
addTagRef.value.open(record);
}
function openDelete(record) {
deleteTagRef.value.open(record);
}
const update = () => {
getData();
emit('update');
};
defineExpose({ open });
</script>
<style lang="scss">
@import './style.scss';
</style>

View File

@ -0,0 +1,51 @@
.raw-material-tags-manage-modal {
border-radius: 8px;
.ant-modal-body {
// padding: 24px 24px 44px !important;
overflow: hidden;
display: flex;
flex-direction: column;
.arcanto-btn {
width: fit-content;
.ant-btn-icon {
line-height: 16px;
}
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
.tag-item {
height: 24px;
display: flex;
align-items: center;
background: #f5f5f5;
border-radius: 4px;
padding: 4px 12px;
position: relative;
.delete-icon {
position: absolute;
z-index: 1;
top: -6px;
right: -6px;
cursor: pointer;
width: 12px;
height: 12px;
display: none;
}
&:hover {
.delete-icon {
display: block;
}
}
}
}
}
}

View File

@ -5,6 +5,10 @@ export enum RawMaterialType {
Text = 2, Text = 2,
} }
export const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'];
export const videoExtensions = ['.mp4', '.mov', '.avi', '.flv', '.wmv', '.m4v'];
export const documentExtensions = ['.txt', '.doc', '.docx', '.pdf', '.xls', '.xlsx'];
export const TABS_LIST = [ export const TABS_LIST = [
{ {
label: '全部', label: '全部',
@ -37,9 +41,10 @@ export const ORIGIN_LIST = [
export const INITIAL_QUERY = { export const INITIAL_QUERY = {
name: '', name: '',
uid: '', // uid: '',
type: RawMaterialType.All, type: RawMaterialType.All,
created_at: [], created_at: [],
tag_ids: [],
sort_column: undefined, sort_column: undefined,
sort_order: undefined, sort_order: undefined,
}; };
@ -55,6 +60,11 @@ export const TABLE_COLUMNS = [
dataIndex: 'type', dataIndex: 'type',
width: 80, width: 80,
}, },
{
title: '标签',
dataIndex: 'tags',
width: 190,
},
{ {
title: '来源', title: '来源',
dataIndex: 'origin', dataIndex: 'origin',
@ -81,7 +91,7 @@ export const TABLE_COLUMNS = [
{ {
title: '操作', title: '操作',
dataIndex: 'operation', dataIndex: 'operation',
width: 100, width: 180,
fixed: 'right', fixed: 'right',
} },
] ];

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,14 +1,20 @@
<script lang="tsx"> <script lang="tsx">
import { provide } from 'vue'; import { provide, ref } from 'vue';
import { Tabs, TabPane, Button, Pagination } from 'ant-design-vue';
import { TABS_LIST, RawMaterialType, INITIAL_QUERY, TABLE_COLUMNS } from './constants'; import { TABS_LIST, RawMaterialType, INITIAL_QUERY, TABLE_COLUMNS } from './constants';
import { Tabs, TabPane, Button, Pagination } from 'ant-design-vue';
import FilterBlock from './components/filter-block/index.vue'; import FilterBlock from './components/filter-block/index.vue';
import RawMaterialTable from './components/table/index.vue'; import RawMaterialTable from './components/table/index.vue';
import DeleteRawMaterialModal from './components/table/delete-file-modal.vue'; import DeleteRawMaterialModal from './components/table/delete-file-modal.vue';
import TagsManageModal from './components/tags-manage-modal';
import AddRawMaterialDrawer from './components/add-raw-material-drawer';
import EditRawMaterialDrawer from './components/edit-raw-material-modal';
import { useTableSelectionWithPagination } from '@/hooks/useTableSelectionWithPagination'; import { useTableSelectionWithPagination } from '@/hooks/useTableSelectionWithPagination';
import { getRawMaterialsPage } from '@/api/all/generationWorkshop'; import { getRawMaterialsPage } from '@/api/all/generationWorkshop';
import icon3 from '@/assets/img/media-account/icon-tag.png';
export default defineComponent({ export default defineComponent({
setup(_, { attrs, slots, expose }) { setup(_, { attrs, slots, expose }) {
const { const {
@ -27,6 +33,10 @@ export default defineComponent({
}); });
const deleteRawMaterialModalRef = ref(null); const deleteRawMaterialModalRef = ref(null);
const tagsManageModalRef = ref(null);
const addRawMaterialDrawerRef = ref(null);
const editRawMaterialDrawerRef = ref(null);
const query = ref(cloneDeep(INITIAL_QUERY)); const query = ref(cloneDeep(INITIAL_QUERY));
const handleSearch = () => { const handleSearch = () => {
getData(); getData();
@ -70,6 +80,10 @@ export default defineComponent({
deleteRawMaterialModalRef.value?.open({ id, name: `${name}` }); deleteRawMaterialModalRef.value?.open({ id, name: `${name}` });
}; };
const handleEdit = (item) => {
editRawMaterialDrawerRef.value?.open(item.id);
};
const handleTabClick = (key) => { const handleTabClick = (key) => {
query.value.type = key; query.value.type = key;
reload(); reload();
@ -81,6 +95,13 @@ export default defineComponent({
getData(); getData();
}; };
const handleOpenTagsModal = () => {
tagsManageModalRef.value?.open();
};
const handleAddMaterial = () => {
addRawMaterialDrawerRef.value?.open();
};
onMounted(() => { onMounted(() => {
getData(); getData();
}); });
@ -88,7 +109,37 @@ export default defineComponent({
return () => ( return () => (
<div class="raw-material-wrap h-full flex flex-col"> <div class="raw-material-wrap h-full flex flex-col">
<div class="bg-white rounded-t-8px"> <div class="bg-white rounded-t-8px">
<Tabs v-model:activeKey={query.value.type} onTabClick={handleTabClick}> <Tabs
v-model:activeKey={query.value.type}
onTabClick={handleTabClick}
v-slots={{
rightExtra: () => (
<div class="flex items-center">
<Button
type="primary"
ghost
size="medium"
onClick={handleOpenTagsModal}
v-slots={{
icon: () => <img src={icon3} width="16" height="16" class="mr-8px" />,
}}
>
标签管理
</Button>
<Button
type="primary"
size="medium"
class="ml-12px"
onClick={handleAddMaterial}
v-slots={{
icon: () => <icon-plus size="16" class="mr-8px" />,
default: () => '上传原料',
}}
/>
</div>
),
}}
>
{TABS_LIST.map((item) => ( {TABS_LIST.map((item) => (
<TabPane key={item.value} tab={item.label}> <TabPane key={item.value} tab={item.label}>
{item.label} {item.label}
@ -121,6 +172,7 @@ export default defineComponent({
onDelete={handleDelete} onDelete={handleDelete}
onSelect={handleSelect} onSelect={handleSelect}
onSelectAll={handleSelectAll} onSelectAll={handleSelectAll}
onEdit={handleEdit}
/> />
{pageInfo.value.total > 0 && ( {pageInfo.value.total > 0 && (
<div class="pagination-row"> <div class="pagination-row">
@ -137,6 +189,10 @@ export default defineComponent({
</div> </div>
)} )}
</div> </div>
<TagsManageModal ref={tagsManageModalRef} />
<AddRawMaterialDrawer ref={addRawMaterialDrawerRef} onUpdate={getData} />
<EditRawMaterialDrawer ref={editRawMaterialDrawerRef} onUpdate={getData} />
<DeleteRawMaterialModal ref={deleteRawMaterialModalRef} onBatchUpdate={onBatchSuccess} onUpdate={getData} /> <DeleteRawMaterialModal ref={deleteRawMaterialModalRef} onBatchUpdate={onBatchSuccess} onUpdate={getData} />
</div> </div>
); );

View File

@ -15,10 +15,10 @@
> >
<div class="flex items-center justify-between mb-16px"> <div class="flex items-center justify-between mb-16px">
<div class="filter-row-item flex items-center"> <div class="filter-row-item flex items-center">
<span class="s1 !color-#211F24 mr-12px">分组名称</span> <span class="s1 !color-#211F24 mr-12px">标签名称</span>
<Input <Input
v-model:value="query.name" v-model:value="query.name"
placeholder="请搜索..." placeholder="请输入..."
class="w-240px" class="w-240px"
size="medium" size="medium"
allowClear allowClear

View File

@ -23,23 +23,23 @@
</Input> </Input>
</Space> </Space>
</div> </div>
<div class="filter-row-item"> <!-- <div class="filter-row-item">-->
<span class="label">序号</span> <!-- <span class="label">序号</span>-->
<Space size="medium"> <!-- <Space size="medium">-->
<Input <!-- <Input-->
v-model:value="query.uid" <!-- v-model:value="query.uid"-->
class="!w-160px" <!-- class="!w-160px"-->
placeholder="请输入序号" <!-- placeholder="请输入序号"-->
size="middle" <!-- size="middle"-->
allowClear <!-- allowClear-->
@change="handleSearch" <!-- @change="handleSearch"-->
> <!-- >-->
<template #prefix> <!-- <template #prefix>-->
<icon-search /> <!-- <icon-search />-->
</template> <!-- </template>-->
</Input> <!-- </Input>-->
</Space> <!-- </Space>-->
</div> <!-- </div>-->
<div class="filter-row-item" v-if="query.audit_status === AuditStatus.Pending"> <div class="filter-row-item" v-if="query.audit_status === AuditStatus.Pending">
<span class="label">上传时间</span> <span class="label">上传时间</span>
<DatePicker.RangePicker <DatePicker.RangePicker

View File

@ -62,6 +62,9 @@
<template v-else-if="column.dataIndex === 'title'" #customRender="{ record }"> <template v-else-if="column.dataIndex === 'title'" #customRender="{ record }">
<TextOverTips :context="record.title" :line="3" class="title" @click="onDetail(record)" /> <TextOverTips :context="record.title" :line="3" class="title" @click="onDetail(record)" />
</template> </template>
<template v-else-if="column.dataIndex === 'origin'" #customRender="{ record }">
{{ ORIGIN_LIST.find((item) => item.value === record.origin)?.label ?? '-' }}
</template>
<template v-else-if="column.dataIndex === 'type'" #customRender="{ record }"> <template v-else-if="column.dataIndex === 'type'" #customRender="{ record }">
<div class="flex items-center"> <div class="flex items-center">
<img :src="record.type === EnumManuscriptType.Image ? icon2 : icon3" width="16" height="16" class="mr-4px" /> <img :src="record.type === EnumManuscriptType.Image ? icon2 : icon3" width="16" height="16" class="mr-4px" />
@ -109,6 +112,8 @@
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { Button, Tooltip, Table, Image } from 'ant-design-vue'; import { Button, Tooltip, Table, Image } from 'ant-design-vue';
const { Column } = Table; const { Column } = Table;
import { ORIGIN_LIST } from '@/views/material-center/components/raw-material/constants';
import { formatTableField, exactFormatTime } from '@/utils/tools'; import { formatTableField, exactFormatTime } from '@/utils/tools';
import { EnumManuscriptType } from '@/views/writer-material-center/components/finished-products/manuscript/list/constants'; import { EnumManuscriptType } from '@/views/writer-material-center/components/finished-products/manuscript/list/constants';
import { patchWorkAuditsAuditWriter } from '@/api/all/generationWorkshop-writer.ts'; import { patchWorkAuditsAuditWriter } from '@/api/all/generationWorkshop-writer.ts';
@ -122,7 +127,7 @@ import { slsWithCatch } from '@/utils/stroage.ts';
import TextOverTips from '@/components/text-over-tips'; import TextOverTips from '@/components/text-over-tips';
import HoverImagePreview from '@/components/hover-image-preview'; import HoverImagePreview from '@/components/hover-image-preview';
import ImgLazyLoad from "@/components/img-lazy-load"; import ImgLazyLoad from '@/components/img-lazy-load';
import icon1 from '@/assets/img/media-account/icon-delete.png'; import icon1 from '@/assets/img/media-account/icon-delete.png';
import icon2 from '@/assets/img/creative-generation-workshop/icon-photo.png'; import icon2 from '@/assets/img/creative-generation-workshop/icon-photo.png';

View File

@ -20,6 +20,11 @@ export const TABLE_COLUMNS1 = [
dataIndex: 'title', dataIndex: 'title',
width: 300, width: 300,
}, },
{
title: '来源',
dataIndex: 'origin',
width: 120,
},
// { // {
// title: '客户意见', // title: '客户意见',
// dataIndex: 'customer_opinion', // dataIndex: 'customer_opinion',

View File

@ -11,8 +11,6 @@ import { formatFileSize, getVideoInfo, formatDuration, formatUploadSpeed } from
import { EnumManuscriptType } from '@/views/writer-material-center/components/finished-products/manuscript/list/constants.ts'; import { EnumManuscriptType } from '@/views/writer-material-center/components/finished-products/manuscript/list/constants.ts';
import { getVideoPreSignedUrlWriter, getImagePreSignedUrlWriter } from '@/api/all/generationWorkshop-writer'; import { getVideoPreSignedUrlWriter, getImagePreSignedUrlWriter } from '@/api/all/generationWorkshop-writer';
// import icon1 from '@/assets/img/creative-generation-workshop/icon-close.png';
// 表单验证规则 // 表单验证规则
const FORM_RULES = { const FORM_RULES = {
title: [{ required: true, message: '请输入标题' }], title: [{ required: true, message: '请输入标题' }],
@ -309,7 +307,7 @@ export default {
placeholder="请输入标题" placeholder="请输入标题"
size="large" size="large"
class="!w-500px" class="!w-500px"
maxlength={30} maxlength={20}
showCount showCount
/> />
</FormItem> </FormItem>

View File

@ -26,23 +26,23 @@
@change="handleSearch" @change="handleSearch"
/> />
</div> --> </div> -->
<div class="filter-row-item"> <!-- <div class="filter-row-item">-->
<span class="label">序号</span> <!-- <span class="label">序号</span>-->
<Space size="medium"> <!-- <Space size="medium">-->
<Input <!-- <Input-->
v-model:value="query.uid" <!-- v-model:value="query.uid"-->
class="!w-160px" <!-- class="!w-160px"-->
placeholder="请输入序号" <!-- placeholder="请输入序号"-->
size="middle" <!-- size="middle"-->
allowClear <!-- allowClear-->
@change="handleSearch" <!-- @change="handleSearch"-->
> <!-- >-->
<template #prefix> <!-- <template #prefix>-->
<icon-search /> <!-- <icon-search />-->
</template> <!-- </template>-->
</Input> <!-- </Input>-->
</Space> <!-- </Space>-->
</div> <!-- </div>-->
<div class="filter-row-item"> <div class="filter-row-item">
<span class="label">审核状态</span> <span class="label">审核状态</span>
<CommonSelect <CommonSelect

View File

@ -33,6 +33,11 @@ export const TABLE_COLUMNS = [
dataIndex: 'audit_status', dataIndex: 'audit_status',
width: 180, width: 180,
}, },
{
title: '来源',
dataIndex: 'origin',
width: 120,
},
{ {
title: '上传时间', title: '上传时间',
dataIndex: 'created_at', dataIndex: 'created_at',
@ -63,6 +68,6 @@ export const TABLE_COLUMNS = [
title: '操作', title: '操作',
dataIndex: 'operation', dataIndex: 'operation',
width: 200, width: 200,
fixed: 'right' fixed: 'right',
}, },
]; ];

View File

@ -39,6 +39,9 @@
<template v-else-if="column.dataIndex === 'title'" #customRender="{ record }"> <template v-else-if="column.dataIndex === 'title'" #customRender="{ record }">
<TextOverTips :context="record.title" :line="3" class="title" @click="onDetail(record)" /> <TextOverTips :context="record.title" :line="3" class="title" @click="onDetail(record)" />
</template> </template>
<template v-else-if="column.dataIndex === 'origin'" #customRender="{ record }">
{{ ORIGIN_LIST.find((item) => item.value === record.origin)?.label ?? '-' }}
</template>
<template v-else-if="column.dataIndex === 'audit_status'" #customRender="{ record }"> <template v-else-if="column.dataIndex === 'audit_status'" #customRender="{ record }">
<div <div
class="flex items-center w-fit h-28px px-8px rounded-2px" class="flex items-center w-fit h-28px px-8px rounded-2px"
@ -88,6 +91,7 @@ import { Button, Tooltip, Table, Image } from 'ant-design-vue';
const { Column } = Table; const { Column } = Table;
import { formatTableField, exactFormatTime } from '@/utils/tools'; import { formatTableField, exactFormatTime } from '@/utils/tools';
import { TABLE_COLUMNS } from './constants'; import { TABLE_COLUMNS } from './constants';
import { ORIGIN_LIST } from '@/views/material-center/components/raw-material/constants';
import { import {
CHECK_STATUS, CHECK_STATUS,
EnumManuscriptType, EnumManuscriptType,
@ -95,7 +99,7 @@ import {
import TextOverTips from '@/components/text-over-tips'; import TextOverTips from '@/components/text-over-tips';
import HoverImagePreview from '@/components/hover-image-preview'; import HoverImagePreview from '@/components/hover-image-preview';
import ImgLazyLoad from "@/components/img-lazy-load"; import ImgLazyLoad from '@/components/img-lazy-load';
import icon1 from '@/assets/img/media-account/icon-delete.png'; import icon1 from '@/assets/img/media-account/icon-delete.png';
import icon2 from '@/assets/img/creative-generation-workshop/icon-photo.png'; import icon2 from '@/assets/img/creative-generation-workshop/icon-photo.png';

View File

@ -1,7 +1,7 @@
export const INITIAL_QUERY = { export const INITIAL_QUERY = {
title: '', title: '',
// project_ids: [], // project_ids: [],
uid: '', // uid: '',
audit_status: '', audit_status: '',
created_at: [], created_at: [],
sort_column: undefined, sort_column: undefined,