更新新需求-采集

This commit is contained in:
super
2026-06-09 21:07:50 +08:00
parent b0462df3c1
commit c3d43aa26a
20 changed files with 2084 additions and 44185 deletions
@@ -0,0 +1,192 @@
<template>
<div class="ai-asset-dropzone">
<div class="ai-asset-dropzone__head">
<div>
<div class="ai-asset-dropzone__title">{{ title }}</div>
<p v-if="description" class="ai-asset-dropzone__description">{{ description }}</p>
</div>
<el-button
v-if="fileName"
text
type="primary"
class="ai-asset-dropzone__clear"
@click="$emit('clear')"
>
清空
</el-button>
</div>
<el-upload
drag
:auto-upload="false"
:show-file-list="false"
:accept="accept"
class="ai-asset-dropzone__upload"
@change="handleChange"
>
<div v-if="previewUrl || (fileName && previewKind === 'file')" class="ai-asset-dropzone__preview">
<img
v-if="previewKind === 'image'"
:src="previewUrl"
class="ai-asset-dropzone__media"
alt=""
/>
<video
v-else-if="previewKind === 'video'"
:src="previewUrl"
class="ai-asset-dropzone__media"
controls
/>
<div v-else class="ai-asset-dropzone__file">
<span>{{ fileName }}</span>
</div>
</div>
<div v-else class="ai-asset-dropzone__empty">
<div class="ai-asset-dropzone__placeholder">{{ placeholder }}</div>
<div v-if="helper" class="ai-asset-dropzone__helper">{{ helper }}</div>
</div>
</el-upload>
<div v-if="fileName" class="ai-asset-dropzone__footer">
<span class="ai-asset-dropzone__filename">{{ fileName }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import type { UploadFile, UploadFiles } from 'element-plus'
withDefaults(
defineProps<{
title: string
description?: string
placeholder?: string
helper?: string
accept?: string
fileName?: string
previewUrl?: string
previewKind?: 'image' | 'video' | 'file'
}>(),
{
description: '',
placeholder: '点击或拖拽上传素材',
helper: '',
accept: '',
fileName: '',
previewUrl: '',
previewKind: 'file',
},
)
const emit = defineEmits<{
(event: 'select', file: File): void
(event: 'clear'): void
}>()
function handleChange(uploadFile: UploadFile, _files: UploadFiles) {
const rawFile = uploadFile.raw
if (!rawFile) {
return
}
emit('select', rawFile)
}
</script>
<style scoped>
.ai-asset-dropzone {
display: flex;
flex-direction: column;
gap: 12px;
}
.ai-asset-dropzone__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.ai-asset-dropzone__title {
color: #f2f2f2;
font-size: 14px;
font-weight: 600;
}
.ai-asset-dropzone__description {
margin: 6px 0 0;
color: #9d9d9d;
font-size: 12px;
line-height: 1.45;
}
:deep(.ai-asset-dropzone__upload .el-upload) {
width: 100%;
}
:deep(.ai-asset-dropzone__upload .el-upload-dragger) {
width: 100%;
min-height: 176px;
padding: 12px;
border: 1px dashed #474747;
border-radius: 14px;
background: #1f1f1f;
transition: border-color 0.2s ease, background 0.2s ease;
}
:deep(.ai-asset-dropzone__upload .el-upload-dragger:hover) {
border-color: #8c63ff;
background: #232323;
}
.ai-asset-dropzone__preview,
.ai-asset-dropzone__empty {
display: flex;
align-items: center;
justify-content: center;
min-height: 150px;
border-radius: 10px;
overflow: hidden;
}
.ai-asset-dropzone__empty {
flex-direction: column;
gap: 8px;
}
.ai-asset-dropzone__placeholder {
color: #f0f0f0;
font-size: 14px;
font-weight: 500;
}
.ai-asset-dropzone__helper {
color: #8d8d8d;
font-size: 12px;
}
.ai-asset-dropzone__media {
width: 100%;
height: 150px;
object-fit: cover;
border-radius: 10px;
}
.ai-asset-dropzone__file {
color: #d8d8d8;
font-size: 13px;
line-height: 1.5;
}
.ai-asset-dropzone__footer {
padding: 10px 12px;
border-radius: 10px;
background: #1f1f1f;
border: 1px solid #383838;
}
.ai-asset-dropzone__filename {
color: #d8d8d8;
font-size: 12px;
word-break: break-all;
}
</style>
@@ -0,0 +1,100 @@
<template>
<div class="ai-choice-pills" :class="{ 'ai-choice-pills--wrap': wrap }">
<button
v-for="option in normalizedOptions"
:key="option.value"
type="button"
class="ai-choice-pills__item"
:class="{ 'ai-choice-pills__item--active': isActive(option.value) }"
@click="toggle(option.value)"
>
{{ option.label }}
</button>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
type Option = string | { label: string; value: string }
const props = withDefaults(
defineProps<{
modelValue: string | string[]
options: Option[]
multiple?: boolean
wrap?: boolean
}>(),
{
multiple: false,
wrap: true,
},
)
const emit = defineEmits<{
(event: 'update:modelValue', value: string | string[]): void
}>()
const normalizedOptions = computed(() =>
props.options.map((option) =>
typeof option === 'string' ? { label: option, value: option } : option,
),
)
function isActive(value: string) {
return props.multiple
? Array.isArray(props.modelValue) && props.modelValue.includes(value)
: props.modelValue === value
}
function toggle(value: string) {
if (!props.multiple) {
emit('update:modelValue', value)
return
}
const current = Array.isArray(props.modelValue) ? [...props.modelValue] : []
const index = current.indexOf(value)
if (index >= 0) {
current.splice(index, 1)
} else {
current.push(value)
}
emit('update:modelValue', current)
}
</script>
<style scoped>
.ai-choice-pills {
display: flex;
gap: 10px;
}
.ai-choice-pills--wrap {
flex-wrap: wrap;
}
.ai-choice-pills__item {
min-height: 40px;
padding: 0 18px;
border: 1px solid #4b4b4b;
border-radius: 999px;
background: #1b1b1b;
color: #dddddd;
font: inherit;
font-size: 13px;
cursor: pointer;
transition: border-color 0.18s ease, color 0.18s ease, background 0.18s ease;
}
.ai-choice-pills__item:hover {
border-color: #8a8a8a;
color: #ffffff;
}
.ai-choice-pills__item--active {
border-color: #8c63ff;
background: rgba(140, 99, 255, 0.16);
color: #ffffff;
}
</style>
@@ -0,0 +1,105 @@
<template>
<div class="ai-module-switch" role="tablist" aria-label="功能模块切换">
<button
v-for="item in options"
:key="item.value"
type="button"
class="ai-module-switch__item"
:class="{
'ai-module-switch__item--active': modelValue === item.value,
'ai-module-switch__item--disabled': item.disabled,
}"
:disabled="item.disabled"
@click="$emit('update:modelValue', item.value)"
>
<span class="ai-module-switch__label">{{ item.label }}</span>
<span v-if="item.description" class="ai-module-switch__description">
{{ item.description }}
</span>
</button>
</div>
</template>
<script setup lang="ts">
export type AiModuleSwitchOption = {
label: string
value: string
description?: string
disabled?: boolean
}
defineProps<{
modelValue: string
options: AiModuleSwitchOption[]
}>()
defineEmits<{
(event: 'update:modelValue', value: string): void
}>()
</script>
<style scoped>
.ai-module-switch {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.ai-module-switch__item {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 6px;
min-height: 84px;
padding: 16px 18px;
border: 1px solid rgba(71, 85, 105, 0.65);
border-radius: 16px;
background: rgba(15, 23, 42, 0.72);
color: #cbd5e1;
text-align: left;
cursor: pointer;
transition:
border-color 0.2s ease,
transform 0.2s ease,
box-shadow 0.2s ease,
background 0.2s ease;
}
.ai-module-switch__item:hover:not(:disabled) {
border-color: rgba(96, 165, 250, 0.7);
box-shadow: 0 14px 24px rgba(8, 15, 31, 0.34);
transform: translateY(-1px);
}
.ai-module-switch__item--active {
border-color: rgba(96, 165, 250, 0.82);
background:
linear-gradient(180deg, rgba(30, 41, 59, 0.96), rgba(15, 23, 42, 0.96));
box-shadow:
0 0 0 1px rgba(59, 130, 246, 0.18),
0 18px 28px rgba(8, 15, 31, 0.36);
}
.ai-module-switch__item--disabled {
opacity: 0.62;
cursor: not-allowed;
}
.ai-module-switch__label {
color: #f8fafc;
font-size: 16px;
font-weight: 600;
}
.ai-module-switch__description {
color: #94a3b8;
font-size: 12px;
line-height: 1.45;
}
@media (max-width: 900px) {
.ai-module-switch {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,101 @@
<template>
<section class="ai-section-card">
<header class="ai-section-card__header">
<div class="ai-section-card__title-wrap">
<span v-if="step" class="ai-section-card__badge">{{ step }}</span>
<div class="ai-section-card__title-group">
<h3 class="ai-section-card__title">{{ title }}</h3>
<p v-if="description" class="ai-section-card__description">{{ description }}</p>
</div>
</div>
<div v-if="$slots.extra" class="ai-section-card__extra">
<slot name="extra" />
</div>
</header>
<div class="ai-section-card__body">
<slot />
</div>
</section>
</template>
<script setup lang="ts">
defineProps<{
title: string
step?: string
description?: string
}>()
</script>
<style scoped>
.ai-section-card {
display: flex;
flex-direction: column;
min-width: 0;
border: 1px solid #343434;
border-radius: 18px;
background: #171717;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02);
}
.ai-section-card__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 18px;
border-bottom: 1px solid #303030;
}
.ai-section-card__title-wrap {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.ai-section-card__badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
flex-shrink: 0;
border-radius: 999px;
background: linear-gradient(135deg, #6a5cff 0%, #4f7bff 100%);
color: #fff;
font-size: 13px;
font-weight: 700;
}
.ai-section-card__title-group {
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
}
.ai-section-card__title {
margin: 0;
color: #fff;
font-size: 15px;
font-weight: 700;
}
.ai-section-card__description {
margin: 6px 0 0;
color: #9d9d9d;
font-size: 12px;
line-height: 1.5;
}
.ai-section-card__extra {
flex-shrink: 0;
}
.ai-section-card__body {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px 18px 18px;
}
</style>
@@ -0,0 +1,94 @@
<template>
<div class="ai-workflow-shell">
<div class="ai-workflow-shell__grid" aria-hidden="true"></div>
<div class="ai-workflow-shell__inner">
<header class="ai-workflow-shell__header">
<div class="ai-workflow-shell__meta">
<slot name="meta" />
</div>
<div class="ai-workflow-shell__actions">
<slot name="actions" />
</div>
</header>
<div class="ai-workflow-shell__toolbar">
<slot name="toolbar" />
</div>
<main class="ai-workflow-shell__content">
<slot />
</main>
</div>
</div>
</template>
<style scoped>
.ai-workflow-shell {
position: relative;
min-height: 100vh;
overflow: hidden;
background:
radial-gradient(circle at top center, rgba(59, 130, 246, 0.14), transparent 32%),
radial-gradient(circle at right bottom, rgba(99, 102, 241, 0.12), transparent 24%),
linear-gradient(180deg, #060913 0%, #09111d 40%, #070b14 100%);
}
.ai-workflow-shell__grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(77, 104, 145, 0.08) 1px, transparent 1px),
linear-gradient(90deg, rgba(77, 104, 145, 0.08) 1px, transparent 1px);
background-size: 40px 40px;
mask-image: radial-gradient(circle at 50% 18%, rgba(0, 0, 0, 0.9), transparent 75%);
pointer-events: none;
}
.ai-workflow-shell__inner {
position: relative;
z-index: 1;
min-height: 100vh;
padding: 28px;
}
.ai-workflow-shell__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20px;
margin-bottom: 18px;
}
.ai-workflow-shell__meta,
.ai-workflow-shell__actions {
min-width: 0;
}
.ai-workflow-shell__actions {
display: flex;
align-items: center;
gap: 12px;
}
.ai-workflow-shell__toolbar {
margin-bottom: 20px;
}
.ai-workflow-shell__content {
min-width: 0;
}
@media (max-width: 960px) {
.ai-workflow-shell__inner {
padding: 20px;
}
.ai-workflow-shell__header {
flex-direction: column;
align-items: stretch;
}
.ai-workflow-shell__actions {
justify-content: flex-start;
flex-wrap: wrap;
}
}
</style>