|
|
@@ -1,872 +0,0 @@
|
|
|
-<template>
|
|
|
- <div class="model-card">
|
|
|
- <div class="tabs-row">
|
|
|
- <nav class="tabs">
|
|
|
- <button type="button" class="tab" :class="{ on: pane === 'lib' }" @click="pane = 'lib'">模特库选择</button>
|
|
|
- <button type="button" class="tab" :class="{ on: pane === 'upm' }" @click="pane = 'upm'">我的上传</button>
|
|
|
- </nav>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div v-if="pane === 'lib'" class="lib-pane">
|
|
|
- <div class="chips-row">
|
|
|
- <div class="chips">
|
|
|
- <button type="button" class="chip" :class="{ on: gender === 'all' }" @click="gender = 'all'">全部</button>
|
|
|
- <button type="button" class="chip" :class="{ on: gender === 'male' }" @click="gender = 'male'">男</button>
|
|
|
- <button type="button" class="chip" :class="{ on: gender === 'female' }" @click="gender = 'female'">女</button>
|
|
|
- <button type="button" class="chip" :class="{ on: gender === 'child' }" @click="gender = 'child'">儿童</button>
|
|
|
- </div>
|
|
|
- <span class="hbtns">
|
|
|
- <button class="hbtn" type="button" title="向左" @click="scrollGrid(libScrollRef, -1)"><svg><use href="#i-left"/></svg></button>
|
|
|
- <button class="hbtn" type="button" title="向右" @click="scrollGrid(libScrollRef, 1)"><svg><use href="#i-right"/></svg></button>
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- <div class="hscroll" ref="libScrollRef">
|
|
|
- <div
|
|
|
- v-for="model in visibleLibModels"
|
|
|
- :key="model.id"
|
|
|
- class="model"
|
|
|
- :class="{ sel: isSelected(model) }"
|
|
|
- @click="selectModel(model)"
|
|
|
- >
|
|
|
- <el-image
|
|
|
- v-if="imagesReady"
|
|
|
- :src="thumbUrl(model.image_url)"
|
|
|
- :alt="model.name"
|
|
|
- fit="cover"
|
|
|
- lazy
|
|
|
- :scroll-container="libScrollRef || undefined"
|
|
|
- class="model-img"
|
|
|
- >
|
|
|
- <template #placeholder>
|
|
|
- <div class="model-ph" />
|
|
|
- </template>
|
|
|
- </el-image>
|
|
|
- <div v-else class="model-ph" />
|
|
|
- <span class="g-tag">{{ genderLabel(model) }}</span>
|
|
|
- <span class="ck">✓</span>
|
|
|
- </div>
|
|
|
- <div v-if="!visibleLibModels.length" class="empty">暂无模特</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div v-if="pane === 'upm'" class="up-pane">
|
|
|
- <div
|
|
|
- v-if="!pendingPreview"
|
|
|
- class="up"
|
|
|
- :class="hasMine ? 'bar' : 'fill'"
|
|
|
- @click="pickImage"
|
|
|
- @dragover.prevent
|
|
|
- @drop.prevent="onDrop"
|
|
|
- >
|
|
|
- <span class="uic"><svg><use href="#i-upl"/></svg></span>
|
|
|
- <span class="up-copy">
|
|
|
- <span class="t">上传我的模特图</span>
|
|
|
- <span class="s">{{ hasMine ? '继续添加,点击即可作为对应性别模特' : '不满意现有模特?上传即可更换,尺寸不限' }}</span>
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- <div v-else class="up-form" :class="hasMine ? 'bar' : 'fill'">
|
|
|
- <div class="up-preview">
|
|
|
- <el-image :src="pendingPreview" fit="cover" class="up-preview-img" />
|
|
|
- </div>
|
|
|
- <div class="up-form-side">
|
|
|
- <div class="up-label">选择类别后确认,将用于对应性别生成</div>
|
|
|
- <div class="chips">
|
|
|
- <button type="button" class="chip" :class="{ on: pendingKeywords === '男性' }" @click="pendingKeywords = '男性'">男模特</button>
|
|
|
- <button type="button" class="chip" :class="{ on: pendingKeywords === '女性' }" @click="pendingKeywords = '女性'">女模特</button>
|
|
|
- <button type="button" class="chip" :class="{ on: pendingKeywords === '儿童' }" @click="pendingKeywords = '儿童'">儿童</button>
|
|
|
- </div>
|
|
|
- <div class="up-actions">
|
|
|
- <button class="gbtn ghost" type="button" :disabled="uploading" @click="resetPending">重新选择</button>
|
|
|
- <button class="gbtn solid" type="button" :disabled="uploading" @click="confirmUpload">
|
|
|
- {{ uploading ? '上传中…' : '确认上传' }}
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div v-show="hasMine" class="chips-row">
|
|
|
- <div class="chips">
|
|
|
- <button type="button" class="chip" :class="{ on: gender === 'all' }" @click="gender = 'all'">全部</button>
|
|
|
- <button type="button" class="chip" :class="{ on: gender === 'male' }" @click="gender = 'male'">男</button>
|
|
|
- <button type="button" class="chip" :class="{ on: gender === 'female' }" @click="gender = 'female'">女</button>
|
|
|
- <button type="button" class="chip" :class="{ on: gender === 'child' }" @click="gender = 'child'">儿童</button>
|
|
|
- </div>
|
|
|
- <span class="hbtns">
|
|
|
- <button class="hbtn" type="button" title="向左" @click="scrollGrid(mineScrollRef, -1)"><svg><use href="#i-left"/></svg></button>
|
|
|
- <button class="hbtn" type="button" title="向右" @click="scrollGrid(mineScrollRef, 1)"><svg><use href="#i-right"/></svg></button>
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- <div v-show="hasMine" class="hscroll" ref="mineScrollRef">
|
|
|
- <div
|
|
|
- v-for="model in visibleMineModels"
|
|
|
- :key="model.id"
|
|
|
- class="model"
|
|
|
- :class="{ sel: isSelected(model) }"
|
|
|
- @click="selectModel(model)"
|
|
|
- >
|
|
|
- <el-image
|
|
|
- v-if="imagesReady"
|
|
|
- :src="thumbUrl(model.image_url)"
|
|
|
- :alt="model.name"
|
|
|
- fit="cover"
|
|
|
- lazy
|
|
|
- :scroll-container="mineScrollRef || undefined"
|
|
|
- class="model-img"
|
|
|
- >
|
|
|
- <template #placeholder>
|
|
|
- <div class="model-ph" />
|
|
|
- </template>
|
|
|
- </el-image>
|
|
|
- <div v-else class="model-ph" />
|
|
|
- <span class="g-tag">{{ genderLabel(model) }}</span>
|
|
|
- <span class="ck">✓</span>
|
|
|
- <button
|
|
|
- type="button"
|
|
|
- class="del"
|
|
|
- title="删除"
|
|
|
- :disabled="deletingId === model.id"
|
|
|
- @click.stop="deleteMine(model)"
|
|
|
- >×</button>
|
|
|
- </div>
|
|
|
- <div v-if="!visibleMineModels.length" class="empty">该类别暂无上传模特</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <input
|
|
|
- ref="fileInput"
|
|
|
- type="file"
|
|
|
- accept=".jpg,.jpeg,.png"
|
|
|
- hidden
|
|
|
- @change="onFilePicked"
|
|
|
- >
|
|
|
- </div>
|
|
|
-</template>
|
|
|
-
|
|
|
-<script setup lang="ts">
|
|
|
-import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
|
|
-import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
-import { getShoesModelTemplateApi, addShoesModelTemplate, delShoesModelTemplate, uploadBaseImg } from '@/apis/other'
|
|
|
-import { ossResize } from '@/utils/oss'
|
|
|
-import { imagesUpload } from '@/utils/appconfig'
|
|
|
-import client from '@/stores/modules/client'
|
|
|
-import icpList from '@/utils/ipc'
|
|
|
-import { useAdaptiveGrid } from '../composables/useAdaptiveGrid'
|
|
|
-
|
|
|
-interface ModelData {
|
|
|
- id: number
|
|
|
- name: string
|
|
|
- image_url: string
|
|
|
- gender?: 'male' | 'female' | 'child'
|
|
|
- keywords: string
|
|
|
- status: number
|
|
|
-}
|
|
|
-
|
|
|
-const props = defineProps<{
|
|
|
- modelValue: { female: any; male: any; child?: any } | null
|
|
|
-}>()
|
|
|
-
|
|
|
-const emit = defineEmits<{
|
|
|
- 'update:modelValue': [value: { female: any; male: any; child?: any }]
|
|
|
-}>()
|
|
|
-
|
|
|
-const clientStore = client()
|
|
|
-const pane = ref<'lib' | 'upm'>('lib')
|
|
|
-const gender = ref<'all' | 'male' | 'female' | 'child'>('all')
|
|
|
-const femaleModels = ref<ModelData[]>([])
|
|
|
-const maleModels = ref<ModelData[]>([])
|
|
|
-const childModels = ref<ModelData[]>([])
|
|
|
-const myModels = ref<ModelData[]>([])
|
|
|
-const libScrollRef = ref<HTMLElement | null>(null)
|
|
|
-const mineScrollRef = ref<HTMLElement | null>(null)
|
|
|
-const fileInput = ref<HTMLInputElement | null>(null)
|
|
|
-
|
|
|
-const pendingPreview = ref('')
|
|
|
-const pendingKeywords = ref<'男性' | '女性' | '儿童' | ''>('')
|
|
|
-const uploading = ref(false)
|
|
|
-const deletingId = ref<number | null>(null)
|
|
|
-const imagesReady = ref(false)
|
|
|
-
|
|
|
-const hasMine = computed(() => myModels.value.length > 0)
|
|
|
-
|
|
|
-function filterByGender(list: ModelData[]) {
|
|
|
- if (gender.value === 'female') return list.filter((model) => model.keywords === '女性')
|
|
|
- if (gender.value === 'male') return list.filter((model) => model.keywords === '男性')
|
|
|
- if (gender.value === 'child') return list.filter((model) => model.keywords === '儿童')
|
|
|
- return list
|
|
|
-}
|
|
|
-
|
|
|
-const visibleLibModels = computed(() => {
|
|
|
- if (gender.value === 'female') return femaleModels.value
|
|
|
- if (gender.value === 'male') return maleModels.value
|
|
|
- if (gender.value === 'child') return childModels.value
|
|
|
- return [...maleModels.value, ...femaleModels.value, ...childModels.value]
|
|
|
-})
|
|
|
-
|
|
|
-const visibleMineModels = computed(() => filterByGender(myModels.value))
|
|
|
-
|
|
|
-const gridOpts = {
|
|
|
- minItemWidth: 84,
|
|
|
- minItemHeight: 108,
|
|
|
- minRows: 1,
|
|
|
- maxRows: 4,
|
|
|
- cols: 2,
|
|
|
- gap: 11,
|
|
|
-}
|
|
|
-
|
|
|
-const { measure: measureLib } = useAdaptiveGrid(libScrollRef, {
|
|
|
- ...gridOpts,
|
|
|
- getItemCount: () => visibleLibModels.value.length,
|
|
|
-})
|
|
|
-const { measure: measureMine } = useAdaptiveGrid(mineScrollRef, {
|
|
|
- ...gridOpts,
|
|
|
- getItemCount: () => visibleMineModels.value.length,
|
|
|
-})
|
|
|
-
|
|
|
-function measureActive() {
|
|
|
- if (pane.value === 'lib') measureLib()
|
|
|
- else measureMine()
|
|
|
-}
|
|
|
-
|
|
|
-function modelKind(model: ModelData): 'female' | 'male' | 'child' {
|
|
|
- if (model.keywords === '女性') return 'female'
|
|
|
- if (model.keywords === '儿童') return 'child'
|
|
|
- return 'male'
|
|
|
-}
|
|
|
-
|
|
|
-function thumbUrl(url?: string) {
|
|
|
- if (!url) return ''
|
|
|
- if (/^(data:|blob:|file:)/i.test(url)) return url
|
|
|
- return ossResize(url, { m: 'fill', w: 280, h: 360 })
|
|
|
-}
|
|
|
-
|
|
|
-function genderLabel(model: ModelData) {
|
|
|
- if (model.keywords === '女性') return '女'
|
|
|
- if (model.keywords === '儿童') return '童'
|
|
|
- return '男'
|
|
|
-}
|
|
|
-
|
|
|
-function pickFields(model: ModelData | null) {
|
|
|
- if (!model) return null
|
|
|
- return {
|
|
|
- id: model.id,
|
|
|
- name: model.name,
|
|
|
- image_url: model.image_url,
|
|
|
- gender: model.gender,
|
|
|
- keywords: model.keywords,
|
|
|
- status: model.status,
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-function isSelected(model: ModelData) {
|
|
|
- const kind = modelKind(model)
|
|
|
- return props.modelValue?.[kind]?.id === model.id
|
|
|
-}
|
|
|
-
|
|
|
-function clearIfSelected(model: ModelData) {
|
|
|
- const kind = modelKind(model)
|
|
|
- if (props.modelValue?.[kind]?.id !== model.id) return
|
|
|
- emit('update:modelValue', {
|
|
|
- female: props.modelValue?.female || null,
|
|
|
- male: props.modelValue?.male || null,
|
|
|
- child: props.modelValue?.child || null,
|
|
|
- [kind]: null,
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-async function deleteMine(model: ModelData) {
|
|
|
- if (deletingId.value) return
|
|
|
- try {
|
|
|
- await ElMessageBox.confirm('是否删除该模特?', '提示', {
|
|
|
- confirmButtonText: '删除',
|
|
|
- cancelButtonText: '取消',
|
|
|
- type: 'warning',
|
|
|
- })
|
|
|
- } catch {
|
|
|
- return
|
|
|
- }
|
|
|
- deletingId.value = model.id
|
|
|
- try {
|
|
|
- await delShoesModelTemplate({ id: model.id })
|
|
|
- clearIfSelected(model)
|
|
|
- ElMessage.success('删除成功')
|
|
|
- await fetchModelList()
|
|
|
- } catch {
|
|
|
- // http 拦截器已提示
|
|
|
- } finally {
|
|
|
- deletingId.value = null
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-function selectModel(model: ModelData) {
|
|
|
- const next = {
|
|
|
- female: props.modelValue?.female || null,
|
|
|
- male: props.modelValue?.male || null,
|
|
|
- child: props.modelValue?.child || null,
|
|
|
- }
|
|
|
- const kind = modelKind(model)
|
|
|
- next[kind] = pickFields(model)
|
|
|
- emit('update:modelValue', next)
|
|
|
-}
|
|
|
-
|
|
|
-function scrollGrid(el: HTMLElement | null, dir: number) {
|
|
|
- if (!el) return
|
|
|
- const delta = Math.max(el.clientWidth * 0.9, 1) * dir
|
|
|
- el.scrollBy({ left: delta, behavior: 'smooth' })
|
|
|
-}
|
|
|
-
|
|
|
-function splitByKeywords(list: ModelData[]) {
|
|
|
- return {
|
|
|
- female: list.filter((model) => model.keywords === '女性'),
|
|
|
- male: list.filter((model) => model.keywords === '男性'),
|
|
|
- child: list.filter((model) => model.keywords === '儿童'),
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-async function fetchModelList() {
|
|
|
- try {
|
|
|
- const [recommendRes, myRes] = await Promise.all([
|
|
|
- getShoesModelTemplateApi({ status: 2 }),
|
|
|
- getShoesModelTemplateApi({ is_my: 1 }),
|
|
|
- ])
|
|
|
- const recommend = (recommendRes && recommendRes.data) || []
|
|
|
- const mine = (myRes && myRes.data) || []
|
|
|
- const grouped = splitByKeywords(recommend)
|
|
|
- femaleModels.value = grouped.female
|
|
|
- maleModels.value = grouped.male
|
|
|
- childModels.value = grouped.child
|
|
|
- myModels.value = Array.isArray(mine) ? mine : []
|
|
|
- nextTick(() => measureActive())
|
|
|
- } catch {
|
|
|
- ElMessage.error('获取模特列表失败')
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-function isOversize(dataUrl: string) {
|
|
|
- const b64 = dataUrl.split(',')[1] || ''
|
|
|
- return (b64.length * 3) / 4 / 1024 > imagesUpload.fileSize
|
|
|
-}
|
|
|
-
|
|
|
-function applyPickedImage(dataUrl: string) {
|
|
|
- if (!dataUrl) return
|
|
|
- if (isOversize(dataUrl)) {
|
|
|
- ElMessage.error('上传文件大小不能超过 20MB')
|
|
|
- return
|
|
|
- }
|
|
|
- pendingPreview.value = dataUrl
|
|
|
-}
|
|
|
-
|
|
|
-function readLocalFile(file: File) {
|
|
|
- if (!/\.(jpe?g|png)$/i.test(file.name)) {
|
|
|
- ElMessage.error('请上传 jpg/png 格式图片')
|
|
|
- return
|
|
|
- }
|
|
|
- if (file.size / 1024 > imagesUpload.fileSize) {
|
|
|
- ElMessage.error('上传文件大小不能超过 20MB')
|
|
|
- return
|
|
|
- }
|
|
|
- const reader = new FileReader()
|
|
|
- reader.onload = () => {
|
|
|
- applyPickedImage(String(reader.result || ''))
|
|
|
- }
|
|
|
- reader.readAsDataURL(file)
|
|
|
-}
|
|
|
-
|
|
|
-function pickImage() {
|
|
|
- if (uploading.value) return
|
|
|
- if (clientStore.isClient && clientStore.ipc) {
|
|
|
- clientStore.ipc.removeAllListeners(icpList.utils.openImage)
|
|
|
- clientStore.ipc.send(icpList.utils.openImage)
|
|
|
- clientStore.ipc.on(icpList.utils.openImage, (_event: unknown, result: { filePath?: string; base64Image?: string }) => {
|
|
|
- clientStore.ipc.removeAllListeners(icpList.utils.openImage)
|
|
|
- if (result?.base64Image) applyPickedImage(result.base64Image)
|
|
|
- else if (!result?.filePath) return
|
|
|
- else ElMessage.error('无法读取图片,请重试')
|
|
|
- })
|
|
|
- return
|
|
|
- }
|
|
|
- fileInput.value?.click()
|
|
|
-}
|
|
|
-
|
|
|
-function onFilePicked(e: Event) {
|
|
|
- const input = e.target as HTMLInputElement
|
|
|
- const file = input.files?.[0]
|
|
|
- input.value = ''
|
|
|
- if (file) readLocalFile(file)
|
|
|
-}
|
|
|
-
|
|
|
-function onDrop(e: DragEvent) {
|
|
|
- const file = e.dataTransfer?.files?.[0]
|
|
|
- if (file) readLocalFile(file)
|
|
|
-}
|
|
|
-
|
|
|
-function resetPending() {
|
|
|
- pendingPreview.value = ''
|
|
|
- pendingKeywords.value = ''
|
|
|
-}
|
|
|
-
|
|
|
-async function confirmUpload() {
|
|
|
- if (!pendingPreview.value) {
|
|
|
- ElMessage.error('请上传模特图')
|
|
|
- return
|
|
|
- }
|
|
|
- if (!pendingKeywords.value) {
|
|
|
- ElMessage.error('请选择类别')
|
|
|
- return
|
|
|
- }
|
|
|
- uploading.value = true
|
|
|
- const keywords = pendingKeywords.value
|
|
|
- try {
|
|
|
- const res: any = await uploadBaseImg({ image: pendingPreview.value })
|
|
|
- const imageUrl = res?.data?.url || res?.url
|
|
|
- if (!imageUrl) {
|
|
|
- ElMessage.error('图片上传失败')
|
|
|
- return
|
|
|
- }
|
|
|
- await addShoesModelTemplate({
|
|
|
- status: 0,
|
|
|
- image_url: imageUrl,
|
|
|
- keywords,
|
|
|
- })
|
|
|
- ElMessage.success('上传成功')
|
|
|
- resetPending()
|
|
|
- await fetchModelList()
|
|
|
- gender.value = keywords === '女性' ? 'female' : keywords === '儿童' ? 'child' : 'male'
|
|
|
- pane.value = 'upm'
|
|
|
- const found =
|
|
|
- myModels.value.find((model) => model.image_url === imageUrl) ||
|
|
|
- myModels.value.find((model) => model.keywords === keywords)
|
|
|
- if (found) selectModel(found)
|
|
|
- } catch {
|
|
|
- // http 拦截器已提示
|
|
|
- } finally {
|
|
|
- uploading.value = false
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-async function revealImages() {
|
|
|
- imagesReady.value = false
|
|
|
- await nextTick()
|
|
|
- requestAnimationFrame(() => {
|
|
|
- imagesReady.value = true
|
|
|
- measureActive()
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-watch(gender, () => {
|
|
|
- libScrollRef.value?.scrollTo({ left: 0 })
|
|
|
- mineScrollRef.value?.scrollTo({ left: 0 })
|
|
|
- nextTick(measureActive)
|
|
|
-})
|
|
|
-watch(pane, () => revealImages())
|
|
|
-watch(() => visibleLibModels.value.length, () => nextTick(measureLib))
|
|
|
-watch(() => visibleMineModels.value.length, () => nextTick(measureMine))
|
|
|
-watch(hasMine, () => nextTick(measureMine))
|
|
|
-
|
|
|
-onMounted(() => {
|
|
|
- revealImages()
|
|
|
- fetchModelList()
|
|
|
-})
|
|
|
-</script>
|
|
|
-
|
|
|
-<style scoped lang="scss">
|
|
|
-.model-card {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 12px;
|
|
|
- min-width: 0;
|
|
|
- min-height: 0;
|
|
|
- flex: 1 1 0;
|
|
|
- overflow: hidden;
|
|
|
-}
|
|
|
-
|
|
|
-.tabs-row {
|
|
|
- display: flex;
|
|
|
- align-items: flex-end;
|
|
|
- gap: 12px;
|
|
|
- border-bottom: 1.5px solid #e7ebf4;
|
|
|
- flex: none;
|
|
|
-}
|
|
|
-.tabs {
|
|
|
- display: flex;
|
|
|
- gap: 22px;
|
|
|
- flex: 1;
|
|
|
- min-width: 0;
|
|
|
- flex-wrap: wrap;
|
|
|
-}
|
|
|
-.tab {
|
|
|
- background: none;
|
|
|
- border: 0;
|
|
|
- padding: 2px 1px 10px;
|
|
|
- font-size: 15px;
|
|
|
- font-weight: 700;
|
|
|
- color: #46516a;
|
|
|
- position: relative;
|
|
|
- white-space: nowrap;
|
|
|
- cursor: pointer;
|
|
|
- outline: none;
|
|
|
- &:focus,
|
|
|
- &:focus-visible { outline: none; }
|
|
|
- &.on {
|
|
|
- color: #3d63f2;
|
|
|
- &::after {
|
|
|
- content: "";
|
|
|
- position: absolute;
|
|
|
- left: 0;
|
|
|
- right: 0;
|
|
|
- bottom: -1.5px;
|
|
|
- height: 3px;
|
|
|
- border-radius: 3px;
|
|
|
- background: linear-gradient(100deg, #3e7bfa, #7a5af8);
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-.lib-pane,
|
|
|
-.up-pane {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 10px;
|
|
|
- min-width: 0;
|
|
|
- min-height: 0;
|
|
|
- flex: 1 1 0;
|
|
|
- overflow: hidden;
|
|
|
-}
|
|
|
-
|
|
|
-.chips-row {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 10px;
|
|
|
- flex: none;
|
|
|
-}
|
|
|
-.chips {
|
|
|
- display: flex;
|
|
|
- gap: 8px;
|
|
|
- flex-wrap: wrap;
|
|
|
- flex: 1;
|
|
|
- min-width: 0;
|
|
|
-}
|
|
|
-.chip {
|
|
|
- appearance: none;
|
|
|
- padding: 5px 14px;
|
|
|
- border-radius: 999px;
|
|
|
- border: 1.5px solid #d9dfec;
|
|
|
- background: #fff;
|
|
|
- font-size: 13.5px;
|
|
|
- font-weight: 700;
|
|
|
- color: #46516a;
|
|
|
- cursor: pointer;
|
|
|
- outline: none;
|
|
|
- box-sizing: border-box;
|
|
|
- &:hover {
|
|
|
- border-color: #3d63f2;
|
|
|
- color: #3d63f2;
|
|
|
- }
|
|
|
- &:focus,
|
|
|
- &:focus-visible {
|
|
|
- outline: none;
|
|
|
- }
|
|
|
- &.on,
|
|
|
- &.on:hover,
|
|
|
- &.on:focus,
|
|
|
- &.on:active {
|
|
|
- background: #3d63f2;
|
|
|
- border-color: #3d63f2;
|
|
|
- color: #fff;
|
|
|
- box-shadow: 0 4px 12px rgba(61, 99, 242, 0.28);
|
|
|
- }
|
|
|
-}
|
|
|
-.hbtns { display: flex; gap: 6px; flex: none; }
|
|
|
-.hbtn {
|
|
|
- width: 27px;
|
|
|
- height: 27px;
|
|
|
- border-radius: 50%;
|
|
|
- border: 1.5px solid #d9dfec;
|
|
|
- background: #fff;
|
|
|
- color: #46516a;
|
|
|
- cursor: pointer;
|
|
|
- display: grid;
|
|
|
- place-items: center;
|
|
|
- padding: 0;
|
|
|
- svg { width: 13px; height: 13px; display: block; }
|
|
|
- &:hover {
|
|
|
- border-color: #3d63f2;
|
|
|
- color: #3d63f2;
|
|
|
- background: #f5f8ff;
|
|
|
- }
|
|
|
-}
|
|
|
-.hscroll {
|
|
|
- --rows: 2;
|
|
|
- --cols: 2;
|
|
|
- --row-h: 140px;
|
|
|
- display: grid;
|
|
|
- grid-template-rows: repeat(var(--rows), minmax(0, 1fr));
|
|
|
- gap: 11px;
|
|
|
- overflow-x: auto;
|
|
|
- overflow-y: hidden;
|
|
|
- overscroll-behavior-x: contain;
|
|
|
- scroll-snap-type: x mandatory;
|
|
|
- padding-bottom: 2px;
|
|
|
- scrollbar-width: none;
|
|
|
- flex: 1 1 0;
|
|
|
- min-height: 0;
|
|
|
- &::-webkit-scrollbar { display: none; }
|
|
|
- > * { scroll-snap-align: start; min-width: 0; min-height: 0; }
|
|
|
-
|
|
|
- &[data-flow="row"] {
|
|
|
- align-content: start;
|
|
|
- grid-auto-flow: row;
|
|
|
- grid-template-columns: repeat(var(--cols), minmax(0, 1fr));
|
|
|
- grid-template-rows: repeat(var(--rows), var(--row-h));
|
|
|
- }
|
|
|
- &[data-flow="column"],
|
|
|
- &:not([data-flow]) {
|
|
|
- align-content: start;
|
|
|
- grid-auto-flow: column;
|
|
|
- grid-auto-columns: calc(100% / var(--cols) - 20px);
|
|
|
- grid-template-rows: repeat(var(--rows), var(--row-h));
|
|
|
- }
|
|
|
-}
|
|
|
-.model {
|
|
|
- position: relative;
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- min-height: 0;
|
|
|
- border-radius: 11px;
|
|
|
- overflow: hidden;
|
|
|
- border: 2px solid transparent;
|
|
|
- cursor: pointer;
|
|
|
- background: #edf2fa;
|
|
|
- transition: all 0.15s;
|
|
|
- .model-img {
|
|
|
- display: block;
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- :deep(.el-image__inner),
|
|
|
- :deep(.el-image__placeholder),
|
|
|
- :deep(.el-image__error),
|
|
|
- :deep(.model-ph) {
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- }
|
|
|
- }
|
|
|
- .ck {
|
|
|
- position: absolute;
|
|
|
- top: 6px;
|
|
|
- right: 6px;
|
|
|
- width: 21px;
|
|
|
- height: 21px;
|
|
|
- border-radius: 50%;
|
|
|
- background: #3d63f2;
|
|
|
- color: #fff;
|
|
|
- font-size: 12px;
|
|
|
- display: none;
|
|
|
- place-items: center;
|
|
|
- box-shadow: 0 2px 6px rgba(23, 43, 99, 0.35);
|
|
|
- z-index: 2;
|
|
|
- }
|
|
|
- &:hover {
|
|
|
- transform: translateY(-2px);
|
|
|
- box-shadow: 0 8px 18px rgba(23, 43, 99, 0.14);
|
|
|
- }
|
|
|
- &.sel {
|
|
|
- border-color: #3d63f2;
|
|
|
- box-shadow: 0 6px 16px rgba(61, 99, 242, 0.24);
|
|
|
- }
|
|
|
- &.sel .ck { display: grid; }
|
|
|
- .del {
|
|
|
- position: absolute;
|
|
|
- right: 6px;
|
|
|
- bottom: 6px;
|
|
|
- z-index: 3;
|
|
|
- width: 22px;
|
|
|
- height: 22px;
|
|
|
- border: 0;
|
|
|
- border-radius: 50%;
|
|
|
- background: rgba(26, 34, 51, 0.62);
|
|
|
- color: #fff;
|
|
|
- font-size: 16px;
|
|
|
- line-height: 20px;
|
|
|
- cursor: pointer;
|
|
|
- display: grid;
|
|
|
- place-items: center;
|
|
|
- padding: 0;
|
|
|
- opacity: 0;
|
|
|
- &:hover { background: #f5303d; }
|
|
|
- &:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
|
- }
|
|
|
- &:hover .del { opacity: 1; }
|
|
|
-}
|
|
|
-.hscroll > .model:only-child {
|
|
|
- height: auto;
|
|
|
- aspect-ratio: 84 / 108;
|
|
|
- align-self: start;
|
|
|
-}
|
|
|
-.model-ph {
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- min-height: 100%;
|
|
|
- background: linear-gradient(90deg, #e7ebf4 0%, #f5f7fb 45%, #e7ebf4 100%);
|
|
|
- background-size: 200% 100%;
|
|
|
- animation: model-ph 1.2s ease infinite;
|
|
|
-}
|
|
|
-.model > .model-ph {
|
|
|
- position: absolute;
|
|
|
- inset: 0;
|
|
|
-}
|
|
|
-@keyframes model-ph {
|
|
|
- 0% { background-position: 100% 0; }
|
|
|
- 100% { background-position: -100% 0; }
|
|
|
-}
|
|
|
-.g-tag {
|
|
|
- position: absolute;
|
|
|
- left: 6px;
|
|
|
- top: 6px;
|
|
|
- z-index: 2;
|
|
|
- padding: 1px 6px;
|
|
|
- border-radius: 6px;
|
|
|
- background: rgba(26, 34, 51, 0.55);
|
|
|
- color: #fff;
|
|
|
- font-size: 11px;
|
|
|
- font-weight: 700;
|
|
|
- line-height: 16px;
|
|
|
-}
|
|
|
-.empty {
|
|
|
- grid-column: 1 / -1;
|
|
|
- color: #8b95ab;
|
|
|
- font-size: 13px;
|
|
|
- padding: 24px 0;
|
|
|
- text-align: center;
|
|
|
-}
|
|
|
-
|
|
|
-.up {
|
|
|
- border: 1.8px dashed #d9dfec;
|
|
|
- border-radius: 13px;
|
|
|
- background: #fafbfe;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- gap: 10px;
|
|
|
- padding: 14px 12px;
|
|
|
- text-align: center;
|
|
|
- cursor: pointer;
|
|
|
- transition: all 0.15s;
|
|
|
- width: 100%;
|
|
|
- box-sizing: border-box;
|
|
|
- &:hover {
|
|
|
- border-color: #3d63f2;
|
|
|
- background: #f5f8ff;
|
|
|
- }
|
|
|
- .uic {
|
|
|
- width: 40px;
|
|
|
- height: 40px;
|
|
|
- border-radius: 11px;
|
|
|
- display: grid;
|
|
|
- place-items: center;
|
|
|
- background: linear-gradient(135deg, #eef2ff, #e3e9ff);
|
|
|
- color: #3d63f2;
|
|
|
- flex: none;
|
|
|
- svg { width: 21px; height: 21px; display: block; }
|
|
|
- }
|
|
|
- .up-copy {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 2px;
|
|
|
- min-width: 0;
|
|
|
- }
|
|
|
- .t { font-size: 14px; font-weight: 700; color: #46516a; }
|
|
|
- .s { font-size: 12px; color: #8b95ab; }
|
|
|
- &.fill {
|
|
|
- flex: 1 1 0;
|
|
|
- min-height: 0;
|
|
|
- flex-direction: column;
|
|
|
- }
|
|
|
- &.bar {
|
|
|
- flex: none;
|
|
|
- min-height: 68px;
|
|
|
- padding: 12px 14px;
|
|
|
- flex-direction: row;
|
|
|
- justify-content: flex-start;
|
|
|
- text-align: left;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-.up-form {
|
|
|
- display: flex;
|
|
|
- gap: 10px;
|
|
|
- min-width: 0;
|
|
|
- min-height: 0;
|
|
|
- width: 100%;
|
|
|
- box-sizing: border-box;
|
|
|
- &.fill {
|
|
|
- flex: 1 1 0;
|
|
|
- flex-direction: column;
|
|
|
- .up-preview { flex: 1 1 0; min-height: 0; }
|
|
|
- .up-form-side { flex: none; }
|
|
|
- }
|
|
|
- &.bar {
|
|
|
- flex: none;
|
|
|
- align-items: stretch;
|
|
|
- .up-preview {
|
|
|
- width: 92px;
|
|
|
- height: 118px;
|
|
|
- flex: none;
|
|
|
- }
|
|
|
- .up-form-side { flex: 1; min-width: 0; }
|
|
|
- }
|
|
|
-}
|
|
|
-.up-preview {
|
|
|
- border-radius: 11px;
|
|
|
- overflow: hidden;
|
|
|
- background: #edf2fa;
|
|
|
- border: 1.5px solid #e7ebf4;
|
|
|
- min-width: 0;
|
|
|
-}
|
|
|
-.up-preview-img {
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- display: block;
|
|
|
-}
|
|
|
-.up-form-side {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 8px;
|
|
|
- min-width: 0;
|
|
|
-}
|
|
|
-.up-label {
|
|
|
- font-size: 12.5px;
|
|
|
- font-weight: 700;
|
|
|
- color: #46516a;
|
|
|
- text-align: left;
|
|
|
- line-height: 1.4;
|
|
|
-}
|
|
|
-.up-actions {
|
|
|
- display: flex;
|
|
|
- gap: 8px;
|
|
|
-}
|
|
|
-.gbtn {
|
|
|
- flex: 1;
|
|
|
- height: 36px;
|
|
|
- border-radius: 11px;
|
|
|
- font-size: 13.5px;
|
|
|
- font-weight: 800;
|
|
|
- cursor: pointer;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- &:disabled {
|
|
|
- opacity: 0.55;
|
|
|
- cursor: not-allowed;
|
|
|
- }
|
|
|
- &.ghost {
|
|
|
- border: 1.5px solid #3d63f2;
|
|
|
- color: #3d63f2;
|
|
|
- background: #fff;
|
|
|
- &:hover:not(:disabled) { background: #f3f6ff; }
|
|
|
- }
|
|
|
- &.solid {
|
|
|
- border: 0;
|
|
|
- color: #fff;
|
|
|
- background: linear-gradient(100deg, #3e7bfa, #7a5af8);
|
|
|
- &:hover:not(:disabled) { filter: brightness(1.05); }
|
|
|
- }
|
|
|
-}
|
|
|
-</style>
|