|
|
@@ -0,0 +1,620 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
|
+import {
|
|
|
+ createDemandFeedback,
|
|
|
+ fetchDemandFeedback,
|
|
|
+} from '../api/videoDiscovery'
|
|
|
+import type {
|
|
|
+ DemandFeedbackItem,
|
|
|
+ DemandFeedbackTarget,
|
|
|
+ FeedbackAction,
|
|
|
+ FeedbackSummary,
|
|
|
+} from '../types/videoDiscovery'
|
|
|
+
|
|
|
+const props = defineProps<{
|
|
|
+ open: boolean
|
|
|
+ target: DemandFeedbackTarget | null
|
|
|
+}>()
|
|
|
+
|
|
|
+const emit = defineEmits<{
|
|
|
+ close: []
|
|
|
+ submitted: [summary: FeedbackSummary]
|
|
|
+}>()
|
|
|
+
|
|
|
+const ACTIONS: Array<{ value: FeedbackAction; label: string }> = [
|
|
|
+ { value: 'support', label: '认可' },
|
|
|
+ { value: 'oppose', label: '不认可' },
|
|
|
+ { value: 'correct', label: '纠错' },
|
|
|
+ { value: 'supplement', label: '补充' },
|
|
|
+]
|
|
|
+
|
|
|
+const REASONS: Record<string, Array<{ value: string; label: string }>> = {
|
|
|
+ demand: [
|
|
|
+ { value: 'not_a_demand', label: '需求不成立' },
|
|
|
+ { value: 'wrong_grade', label: '等级不合适' },
|
|
|
+ { value: 'wrong_category', label: '分类不准确' },
|
|
|
+ { value: 'unclear_wording', label: '需求表述不清' },
|
|
|
+ { value: 'wrong_reason', label: '判断原因不准确' },
|
|
|
+ { value: 'missing_demand', label: '缺少需求' },
|
|
|
+ { value: 'other', label: '其他' },
|
|
|
+ ],
|
|
|
+ video: [
|
|
|
+ { value: 'irrelevant', label: '与需求不相关' },
|
|
|
+ { value: 'weak_relevance', label: '相关性弱' },
|
|
|
+ { value: 'unavailable', label: '视频失效' },
|
|
|
+ { value: 'duplicate', label: '视频重复' },
|
|
|
+ { value: 'wrong_info', label: '标题或信息错误' },
|
|
|
+ { value: 'missing_video', label: '缺少相关视频' },
|
|
|
+ { value: 'other', label: '其他' },
|
|
|
+ ],
|
|
|
+ hit_content: [
|
|
|
+ { value: 'not_hit', label: '内容未命中' },
|
|
|
+ { value: 'wrong_point_type', label: '点位类型错误' },
|
|
|
+ { value: 'inaccurate_wording', label: '表述不准确' },
|
|
|
+ { value: 'wrong_reason', label: '命中原因不准确' },
|
|
|
+ { value: 'duplicate', label: '内容重复' },
|
|
|
+ { value: 'missing_content', label: '缺少命中内容' },
|
|
|
+ { value: 'other', label: '其他' },
|
|
|
+ ],
|
|
|
+}
|
|
|
+
|
|
|
+const action = ref<FeedbackAction | null>(null)
|
|
|
+const reasonCode = ref('')
|
|
|
+const content = ref('')
|
|
|
+const submitting = ref(false)
|
|
|
+const submitError = ref<string | null>(null)
|
|
|
+const history = ref<DemandFeedbackItem[]>([])
|
|
|
+const historyTotal = ref(0)
|
|
|
+const historyLoading = ref(false)
|
|
|
+const historyError = ref<string | null>(null)
|
|
|
+
|
|
|
+const targetLabel = computed(() => {
|
|
|
+ if (props.target?.target_type === 'demand') return '需求'
|
|
|
+ if (props.target?.target_type === 'video') return '视频'
|
|
|
+ return '命中内容'
|
|
|
+})
|
|
|
+
|
|
|
+const reasonOptions = computed(() => REASONS[props.target?.target_type ?? 'demand'])
|
|
|
+const dirty = computed(() => Boolean(action.value || reasonCode.value || content.value.trim()))
|
|
|
+const canSubmit = computed(() => {
|
|
|
+ if (!action.value || !props.target || submitting.value) return false
|
|
|
+ if (action.value === 'oppose' && !reasonCode.value) return false
|
|
|
+ if (
|
|
|
+ (action.value === 'correct' || action.value === 'supplement')
|
|
|
+ && !content.value.trim()
|
|
|
+ ) return false
|
|
|
+ return true
|
|
|
+})
|
|
|
+
|
|
|
+watch(
|
|
|
+ [() => props.open, () => props.target],
|
|
|
+ ([open]) => {
|
|
|
+ if (!open || !props.target) return
|
|
|
+ action.value = null
|
|
|
+ reasonCode.value = ''
|
|
|
+ content.value = ''
|
|
|
+ submitError.value = null
|
|
|
+ void loadHistory()
|
|
|
+ },
|
|
|
+)
|
|
|
+
|
|
|
+watch(action, (value) => {
|
|
|
+ if (value === 'support') reasonCode.value = ''
|
|
|
+})
|
|
|
+
|
|
|
+async function loadHistory() {
|
|
|
+ if (!props.target) return
|
|
|
+ historyLoading.value = true
|
|
|
+ historyError.value = null
|
|
|
+ try {
|
|
|
+ const response = await fetchDemandFeedback(props.target)
|
|
|
+ history.value = response.items ?? []
|
|
|
+ historyTotal.value = response.total ?? 0
|
|
|
+ } catch (error) {
|
|
|
+ historyError.value = error instanceof Error ? error.message : String(error)
|
|
|
+ } finally {
|
|
|
+ historyLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function requestId(): string {
|
|
|
+ if (typeof crypto.randomUUID === 'function') return crypto.randomUUID()
|
|
|
+ return `${Date.now()}-${Math.random().toString(16).slice(2)}`
|
|
|
+}
|
|
|
+
|
|
|
+async function submit() {
|
|
|
+ if (!canSubmit.value || !props.target || !action.value) return
|
|
|
+ submitting.value = true
|
|
|
+ submitError.value = null
|
|
|
+ try {
|
|
|
+ const response = await createDemandFeedback({
|
|
|
+ client_request_id: requestId(),
|
|
|
+ target_type: props.target.target_type,
|
|
|
+ demand_grade_id: props.target.demand_grade_id,
|
|
|
+ video_id: props.target.video_id,
|
|
|
+ demand_video_expansion_id: props.target.demand_video_expansion_id,
|
|
|
+ feedback_action: action.value,
|
|
|
+ reason_code: reasonCode.value || undefined,
|
|
|
+ content: content.value.trim() || undefined,
|
|
|
+ })
|
|
|
+ emit('submitted', response.feedback_summary)
|
|
|
+ emit('close')
|
|
|
+ } catch (error) {
|
|
|
+ submitError.value = error instanceof Error ? error.message : String(error)
|
|
|
+ } finally {
|
|
|
+ submitting.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function close() {
|
|
|
+ if (submitting.value) return
|
|
|
+ if (dirty.value && !window.confirm('反馈尚未提交,确定关闭吗?')) return
|
|
|
+ emit('close')
|
|
|
+}
|
|
|
+
|
|
|
+function onKeydown(event: KeyboardEvent) {
|
|
|
+ if (event.key === 'Escape' && props.open) close()
|
|
|
+}
|
|
|
+
|
|
|
+function actionLabel(value: FeedbackAction): string {
|
|
|
+ return ACTIONS.find((item) => item.value === value)?.label ?? value
|
|
|
+}
|
|
|
+
|
|
|
+function reasonLabel(value: string | null): string {
|
|
|
+ if (!value) return ''
|
|
|
+ return reasonOptions.value.find((item) => item.value === value)?.label ?? value
|
|
|
+}
|
|
|
+
|
|
|
+function formatTime(value: string | null): string {
|
|
|
+ if (!value) return '—'
|
|
|
+ const date = new Date(value)
|
|
|
+ if (Number.isNaN(date.getTime())) return value
|
|
|
+ return new Intl.DateTimeFormat('zh-CN', {
|
|
|
+ month: '2-digit',
|
|
|
+ day: '2-digit',
|
|
|
+ hour: '2-digit',
|
|
|
+ minute: '2-digit',
|
|
|
+ }).format(date)
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => window.addEventListener('keydown', onKeydown))
|
|
|
+onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown))
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <Teleport to="body">
|
|
|
+ <Transition name="feedback-fade">
|
|
|
+ <div v-if="open && target" class="feedback-overlay" @click.self="close">
|
|
|
+ <aside class="feedback-drawer" role="dialog" aria-modal="true" :aria-label="`${targetLabel}反馈`">
|
|
|
+ <header class="drawer-head">
|
|
|
+ <div>
|
|
|
+ <span>反馈 · {{ targetLabel }}</span>
|
|
|
+ <h2>{{ target.title }}</h2>
|
|
|
+ </div>
|
|
|
+ <button type="button" aria-label="关闭反馈" @click="close">×</button>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <div class="drawer-body">
|
|
|
+ <section class="target-context" aria-label="反馈对象">
|
|
|
+ <div v-for="item in target.context" :key="item.label">
|
|
|
+ <span>{{ item.label }}</span>
|
|
|
+ <strong>{{ item.value }}</strong>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="form-section">
|
|
|
+ <div class="section-title">
|
|
|
+ <strong>你的判断</strong>
|
|
|
+ <span>必填</span>
|
|
|
+ </div>
|
|
|
+ <div class="action-options">
|
|
|
+ <button
|
|
|
+ v-for="item in ACTIONS"
|
|
|
+ :key="item.value"
|
|
|
+ type="button"
|
|
|
+ :class="{ active: action === item.value }"
|
|
|
+ @click="action = item.value"
|
|
|
+ >
|
|
|
+ {{ item.label }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section v-if="action && action !== 'support'" class="form-section">
|
|
|
+ <div class="section-title">
|
|
|
+ <strong>问题原因</strong>
|
|
|
+ <span>{{ action === 'oppose' ? '必填' : '选填' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="reason-options">
|
|
|
+ <button
|
|
|
+ v-for="item in reasonOptions"
|
|
|
+ :key="item.value"
|
|
|
+ type="button"
|
|
|
+ :class="{ active: reasonCode === item.value }"
|
|
|
+ @click="reasonCode = reasonCode === item.value ? '' : item.value"
|
|
|
+ >
|
|
|
+ {{ item.label }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="form-section">
|
|
|
+ <div class="section-title">
|
|
|
+ <strong>补充说明</strong>
|
|
|
+ <span>{{ action === 'correct' || action === 'supplement' ? '必填' : '选填' }}</span>
|
|
|
+ </div>
|
|
|
+ <textarea
|
|
|
+ v-model="content"
|
|
|
+ maxlength="1000"
|
|
|
+ rows="4"
|
|
|
+ placeholder="请说明判断依据或建议的正确内容"
|
|
|
+ />
|
|
|
+ <div class="text-count">{{ content.length }} / 1000</div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <p v-if="submitError" class="feedback-error">{{ submitError }}</p>
|
|
|
+
|
|
|
+ <section class="history-section">
|
|
|
+ <div class="section-title">
|
|
|
+ <strong>反馈记录</strong>
|
|
|
+ <span>{{ historyTotal }} 条</span>
|
|
|
+ </div>
|
|
|
+ <div v-if="historyLoading" class="history-state">正在加载反馈记录…</div>
|
|
|
+ <div v-else-if="historyError" class="history-state error">{{ historyError }}</div>
|
|
|
+ <div v-else-if="history.length" class="history-list">
|
|
|
+ <article v-for="item in history" :key="item.id">
|
|
|
+ <header>
|
|
|
+ <strong>
|
|
|
+ {{ item.feedback_user.display_name }}
|
|
|
+ <small>@{{ item.feedback_user.username }}</small>
|
|
|
+ </strong>
|
|
|
+ <time>{{ formatTime(item.created_at) }}</time>
|
|
|
+ </header>
|
|
|
+ <div class="history-tags">
|
|
|
+ <span>{{ actionLabel(item.feedback_action) }}</span>
|
|
|
+ <span v-if="item.reason_code">{{ reasonLabel(item.reason_code) }}</span>
|
|
|
+ </div>
|
|
|
+ <p v-if="item.content">{{ item.content }}</p>
|
|
|
+ </article>
|
|
|
+ </div>
|
|
|
+ <div v-else class="history-state">暂无反馈记录</div>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <footer class="drawer-actions">
|
|
|
+ <button type="button" class="cancel" @click="close">取消</button>
|
|
|
+ <button type="button" class="submit" :disabled="!canSubmit" @click="submit">
|
|
|
+ {{ submitting ? '提交中…' : '提交反馈' }}
|
|
|
+ </button>
|
|
|
+ </footer>
|
|
|
+ </aside>
|
|
|
+ </div>
|
|
|
+ </Transition>
|
|
|
+ </Teleport>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.feedback-overlay {
|
|
|
+ position: fixed;
|
|
|
+ z-index: 1000;
|
|
|
+ inset: 0;
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ background: rgba(18, 28, 22, 0.34);
|
|
|
+ backdrop-filter: blur(2px);
|
|
|
+}
|
|
|
+
|
|
|
+.feedback-drawer {
|
|
|
+ width: min(470px, 100vw);
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ background: #fff;
|
|
|
+ color: #17211b;
|
|
|
+ box-shadow: -18px 0 48px rgba(18, 38, 25, 0.16);
|
|
|
+}
|
|
|
+
|
|
|
+.drawer-head {
|
|
|
+ min-height: 82px;
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 16px;
|
|
|
+ padding: 18px 20px 15px;
|
|
|
+ border-bottom: 1px solid #e5eae6;
|
|
|
+}
|
|
|
+
|
|
|
+.drawer-head span {
|
|
|
+ color: #397354;
|
|
|
+ font-size: 10px;
|
|
|
+ font-weight: 800;
|
|
|
+ letter-spacing: 0.12em;
|
|
|
+}
|
|
|
+
|
|
|
+.drawer-head h2 {
|
|
|
+ margin: 5px 0 0;
|
|
|
+ font-family: Georgia, 'Songti SC', serif;
|
|
|
+ font-size: 18px;
|
|
|
+ line-height: 1.35;
|
|
|
+}
|
|
|
+
|
|
|
+.drawer-head button {
|
|
|
+ width: 30px;
|
|
|
+ height: 30px;
|
|
|
+ flex: 0 0 auto;
|
|
|
+ border: 1px solid #dce3dd;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #f8faf8;
|
|
|
+ color: #68736b;
|
|
|
+ font-size: 19px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.drawer-body {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 16px 20px 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.target-context {
|
|
|
+ display: grid;
|
|
|
+ gap: 7px;
|
|
|
+ padding: 12px 13px;
|
|
|
+ border: 1px solid #dce6de;
|
|
|
+ border-radius: 10px;
|
|
|
+ background: #f4f8f5;
|
|
|
+}
|
|
|
+
|
|
|
+.target-context div {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 64px minmax(0, 1fr);
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.target-context span {
|
|
|
+ color: #829087;
|
|
|
+ font-size: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.target-context strong {
|
|
|
+ overflow-wrap: anywhere;
|
|
|
+ color: #334239;
|
|
|
+ font-size: 11px;
|
|
|
+ line-height: 1.45;
|
|
|
+}
|
|
|
+
|
|
|
+.form-section,
|
|
|
+.history-section {
|
|
|
+ margin-top: 18px;
|
|
|
+}
|
|
|
+
|
|
|
+.section-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 9px;
|
|
|
+}
|
|
|
+
|
|
|
+.section-title strong {
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.section-title span {
|
|
|
+ color: #8a958d;
|
|
|
+ font-size: 9px;
|
|
|
+}
|
|
|
+
|
|
|
+.action-options {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
+ gap: 7px;
|
|
|
+}
|
|
|
+
|
|
|
+.action-options button,
|
|
|
+.reason-options button {
|
|
|
+ border: 1px solid #d8dfd9;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #fff;
|
|
|
+ color: #58645c;
|
|
|
+ font-size: 11px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.action-options button {
|
|
|
+ height: 36px;
|
|
|
+ font-weight: 700;
|
|
|
+}
|
|
|
+
|
|
|
+.reason-options {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 7px;
|
|
|
+}
|
|
|
+
|
|
|
+.reason-options button {
|
|
|
+ min-height: 30px;
|
|
|
+ padding: 5px 9px;
|
|
|
+}
|
|
|
+
|
|
|
+.action-options button:hover,
|
|
|
+.reason-options button:hover,
|
|
|
+.action-options button.active,
|
|
|
+.reason-options button.active {
|
|
|
+ border-color: #3d7656;
|
|
|
+ background: #e9f2ec;
|
|
|
+ color: #275b40;
|
|
|
+}
|
|
|
+
|
|
|
+textarea {
|
|
|
+ width: 100%;
|
|
|
+ box-sizing: border-box;
|
|
|
+ resize: vertical;
|
|
|
+ padding: 10px 11px;
|
|
|
+ border: 1px solid #d8dfd9;
|
|
|
+ border-radius: 9px;
|
|
|
+ outline: none;
|
|
|
+ color: #26332a;
|
|
|
+ font: inherit;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.55;
|
|
|
+}
|
|
|
+
|
|
|
+textarea:focus {
|
|
|
+ border-color: #6b957c;
|
|
|
+ box-shadow: 0 0 0 3px rgba(53, 110, 78, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.text-count {
|
|
|
+ margin-top: 4px;
|
|
|
+ color: #98a19a;
|
|
|
+ font-size: 9px;
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+
|
|
|
+.feedback-error {
|
|
|
+ margin: 12px 0 0;
|
|
|
+ padding: 9px 10px;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #fff0ec;
|
|
|
+ color: #a8462c;
|
|
|
+ font-size: 11px;
|
|
|
+}
|
|
|
+
|
|
|
+.history-section {
|
|
|
+ padding-top: 17px;
|
|
|
+ border-top: 1px solid #e8ece9;
|
|
|
+}
|
|
|
+
|
|
|
+.history-list {
|
|
|
+ display: grid;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.history-list article {
|
|
|
+ padding: 10px 11px;
|
|
|
+ border: 1px solid #e1e6e2;
|
|
|
+ border-radius: 9px;
|
|
|
+ background: #fbfcfb;
|
|
|
+}
|
|
|
+
|
|
|
+.history-list header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.history-list header strong {
|
|
|
+ font-size: 11px;
|
|
|
+}
|
|
|
+
|
|
|
+.history-list header small {
|
|
|
+ margin-left: 5px;
|
|
|
+ color: #8b958e;
|
|
|
+ font-size: 9px;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.history-list time {
|
|
|
+ color: #919a93;
|
|
|
+ font-size: 9px;
|
|
|
+}
|
|
|
+
|
|
|
+.history-tags {
|
|
|
+ display: flex;
|
|
|
+ gap: 5px;
|
|
|
+ margin-top: 7px;
|
|
|
+}
|
|
|
+
|
|
|
+.history-tags span {
|
|
|
+ padding: 2px 6px;
|
|
|
+ border-radius: 999px;
|
|
|
+ background: #e7efe9;
|
|
|
+ color: #39604a;
|
|
|
+ font-size: 9px;
|
|
|
+ font-weight: 700;
|
|
|
+}
|
|
|
+
|
|
|
+.history-list p {
|
|
|
+ margin: 7px 0 0;
|
|
|
+ color: #5f6b63;
|
|
|
+ font-size: 11px;
|
|
|
+ line-height: 1.5;
|
|
|
+ white-space: pre-wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.history-state {
|
|
|
+ padding: 12px;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #f7f9f7;
|
|
|
+ color: #89928b;
|
|
|
+ font-size: 10px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.history-state.error {
|
|
|
+ color: #a8462c;
|
|
|
+}
|
|
|
+
|
|
|
+.drawer-actions {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 9px;
|
|
|
+ padding: 13px 20px;
|
|
|
+ border-top: 1px solid #e3e8e4;
|
|
|
+ background: #fbfcfb;
|
|
|
+}
|
|
|
+
|
|
|
+.drawer-actions button {
|
|
|
+ height: 36px;
|
|
|
+ padding: 0 16px;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 800;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.drawer-actions .cancel {
|
|
|
+ border: 1px solid #d4dcd6;
|
|
|
+ background: #fff;
|
|
|
+ color: #68736b;
|
|
|
+}
|
|
|
+
|
|
|
+.drawer-actions .submit {
|
|
|
+ border: 1px solid #315f47;
|
|
|
+ background: #315f47;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.drawer-actions .submit:disabled {
|
|
|
+ border-color: #cdd5cf;
|
|
|
+ background: #cdd5cf;
|
|
|
+ cursor: not-allowed;
|
|
|
+}
|
|
|
+
|
|
|
+.feedback-fade-enter-active,
|
|
|
+.feedback-fade-leave-active {
|
|
|
+ transition: opacity 0.18s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.feedback-fade-enter-active .feedback-drawer,
|
|
|
+.feedback-fade-leave-active .feedback-drawer {
|
|
|
+ transition: transform 0.18s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.feedback-fade-enter-from,
|
|
|
+.feedback-fade-leave-to {
|
|
|
+ opacity: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.feedback-fade-enter-from .feedback-drawer,
|
|
|
+.feedback-fade-leave-to .feedback-drawer {
|
|
|
+ transform: translateX(24px);
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 640px) {
|
|
|
+ .feedback-drawer {
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|