forked from natuka/web.puabadge.com
新增:京东下单页面
This commit is contained in:
1 parent
8e8cde9ef5
commit
007c675603
25 files changed
+3264
-25
No files matched your search
@@ -0,0 +1,347 @@
|
||||
<template>
|
||||
<div class="composer">
|
||||
<div v-if="editing" class="composer__edit-bar">
|
||||
<span class="composer__edit-left">
|
||||
<el-icon :size="14"><EditPen /></el-icon>
|
||||
<span>{{ editingHint }}</span>
|
||||
</span>
|
||||
<button class="composer__exit" type="button" @click="emit('exit-edit')">
|
||||
<el-icon :size="12"><Close /></el-icon>
|
||||
<span>{{ exitEditLabel }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div ref="boxRef" class="composer__box">
|
||||
<div v-if="previewUrl" ref="previewRef" class="composer__preview">
|
||||
<img class="composer__preview-img" :src="previewUrl" alt="已选图片" />
|
||||
<button class="composer__preview-close" type="button" aria-label="移除图片" @click="emit('clear-preview')">
|
||||
<el-icon :size="10" color="#fff"><Close /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
ref="textareaRef"
|
||||
class="composer__input"
|
||||
rows="2"
|
||||
:value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
@input="onInput"
|
||||
/>
|
||||
|
||||
<div class="composer__toolbar">
|
||||
<button
|
||||
class="composer__upload"
|
||||
type="button"
|
||||
aria-label="上传图片"
|
||||
:disabled="disabled || editing || inspecting"
|
||||
@click="emit('upload')"
|
||||
>
|
||||
<el-icon v-if="inspecting" class="is-loading" :size="20"><Loading /></el-icon>
|
||||
<el-icon v-else :size="22"><Picture /></el-icon>
|
||||
</button>
|
||||
|
||||
<div class="composer__tags">
|
||||
<span v-for="tag in tagItems" :key="tag.key" class="composer__tag" :title="tag.label">
|
||||
<el-icon class="composer__tag-icon" :size="12"><component :is="tag.icon" /></el-icon>
|
||||
<span class="composer__tag-text">{{ tag.label }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="composer__send"
|
||||
type="button"
|
||||
aria-label="发送"
|
||||
:disabled="disabled || !canSend"
|
||||
@click="emit('send')"
|
||||
>
|
||||
<el-icon :size="18"><Top /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { Close, EditPen, Loading, Picture, Present, Star, Top, User } from '@element-plus/icons-vue'
|
||||
|
||||
export type ComposerTag = {
|
||||
key: string
|
||||
label: string
|
||||
icon?: 'user' | 'star' | 'present'
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: string
|
||||
placeholder?: string
|
||||
previewUrl?: string
|
||||
tags?: ComposerTag[]
|
||||
editing?: boolean
|
||||
editingHint?: string
|
||||
exitEditLabel?: string
|
||||
disabled?: boolean
|
||||
canSend?: boolean
|
||||
/** 图片识别 / 主体识别进行中 */
|
||||
inspecting?: boolean
|
||||
}>(),
|
||||
{
|
||||
modelValue: '',
|
||||
placeholder: '',
|
||||
previewUrl: '',
|
||||
tags: () => [],
|
||||
editing: false,
|
||||
editingHint: '',
|
||||
exitEditLabel: '退出编辑',
|
||||
disabled: false,
|
||||
canSend: false,
|
||||
inspecting: false,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void
|
||||
(e: 'upload'): void
|
||||
(e: 'clear-preview'): void
|
||||
(e: 'send'): void
|
||||
(e: 'exit-edit'): void
|
||||
}>()
|
||||
|
||||
const iconMap = {
|
||||
user: User,
|
||||
star: Star,
|
||||
present: Present,
|
||||
}
|
||||
|
||||
const tagItems = computed(() =>
|
||||
(props.tags || [])
|
||||
.filter((tag) => tag && String(tag.label || '').trim())
|
||||
.map((tag) => ({
|
||||
key: tag.key || tag.label,
|
||||
label: String(tag.label).trim(),
|
||||
icon: iconMap[tag.icon || 'present'] || Present,
|
||||
})),
|
||||
)
|
||||
|
||||
const textareaRef = ref<HTMLTextAreaElement | null>(null)
|
||||
const boxRef = ref<HTMLElement | null>(null)
|
||||
const previewRef = ref<HTMLElement | null>(null)
|
||||
|
||||
function onInput(e: Event) {
|
||||
const target = e.target as HTMLTextAreaElement
|
||||
emit('update:modelValue', target.value)
|
||||
}
|
||||
|
||||
function focus() {
|
||||
textareaRef.value?.focus()
|
||||
}
|
||||
|
||||
function getPreviewEl() {
|
||||
return previewRef.value
|
||||
}
|
||||
|
||||
function getFlyTargetEl() {
|
||||
return previewRef.value || boxRef.value
|
||||
}
|
||||
|
||||
defineExpose({ focus, getPreviewEl, getFlyTargetEl })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.composer {
|
||||
flex-shrink: 0;
|
||||
padding: 8px 12px calc(10px + env(safe-area-inset-bottom));
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.composer__edit-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px;
|
||||
background: #eaf6ec;
|
||||
color: #3a8e4d;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.composer__edit-left {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.composer__exit {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
flex-shrink: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #3a8e4d;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.composer__box {
|
||||
padding: 12px 12px 10px;
|
||||
border-radius: 18px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #ebebeb;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.composer__preview {
|
||||
position: relative;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: #f0f0f0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.composer__preview-close {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.composer__input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
max-height: 96px;
|
||||
margin: 0 0 10px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
resize: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #1a1a1a;
|
||||
font-family: inherit;
|
||||
|
||||
&::placeholder {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.composer__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.composer__upload {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.is-loading {
|
||||
animation: jd-spin 0.9s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes jd-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.composer__tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.composer__tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
max-width: 88px;
|
||||
height: 26px;
|
||||
padding: 0 8px;
|
||||
border-radius: 999px;
|
||||
background: #f2f2f2;
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.composer__tag-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.composer__tag-text {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.composer__send {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
flex-shrink: 0;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
background: #3a8e4d;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user