Files
happy-life-star/mini-program/src/pages/main/ScriptView.vue
T
peanut ee5a6aba5d feat: 小程序脚本首页重构 + 社交数据导入 + TTS 播放优化
- 后端:新增社交数据导入/审批/洞察生成 API(SocialContent/SocialInsight)
- 后端:优化脚本上下文服务,TTS 服务增强
- 小程序:重构脚本首页布局,新增社交导入页面
- 小程序:新增 useTtsPlayer composable,移除旧 ScriptAudioPlayer 组件
- 小程序:新增社交导入服务,优化请求服务
- SQL:新增社交数据导入建表脚本
- 文档:补充设计文档和实施计划

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 07:18:02 +08:00

938 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="script-view">
<view v-if="viewState === 'home'" class="wish-home">
<view class="home-head">
<view class="history-button" @click="openScriptLibrary">
<view class="history-lines">
<view></view>
<view></view>
<view></view>
</view>
<text>历史</text>
</view>
<view class="head-action-row">
<view class="social-import-btn" @click="openSocialImport">
<text>导入社交数据</text>
</view>
<view class="script-list-btn" @click="openScriptLibrary">
<text>我的剧本</text>
</view>
</view>
</view>
<view class="hero-copy">
<text class="hero-title">今天有什么</text>
<text class="hero-title"><text class="hero-highlight">心愿</text>想实现</text>
</view>
<view class="orb-wrap">
<view
class="mic-orb"
:class="{ pressing: voiceState === 'pressing', recognizing: voiceState === 'recognizing' }"
@touchstart.prevent="startVoicePress"
@touchend.prevent="endVoicePress"
@touchcancel.prevent="cancelVoicePress"
>
<view class="mic-symbol">
<view class="mic-head"></view>
<view class="mic-stem"></view>
<view class="mic-base"></view>
</view>
</view>
</view>
<text class="voice-copy">{{ voiceCopy }}</text>
<view class="wish-input-wrap">
<input
class="wish-input"
v-model="wishText"
confirm-type="send"
placeholder="写下你的心愿,AI帮你重写人生"
placeholder-class="placeholder"
@confirm="submitWish('text')"
/>
<view class="send-button" :class="{ disabled: !wishText.trim() }" @click="submitWish('text')">发送</view>
</view>
<view class="profile-boost">
<view class="boost-main" @click="openSocialInsights">
<text class="boost-title">人生素材画像</text>
<text class="boost-copy">{{ socialInsightCopy }}</text>
</view>
<switch
class="boost-switch"
:checked="useSocialInsights"
color="#a855f7"
@change="useSocialInsights = $event.detail.value"
/>
</view>
<view class="inspiration-section">
<view class="section-line">
<text class="section-title">灵感一下</text>
<text class="refresh" @click="shuffleInspirations">换一换</text>
</view>
<view class="recommend-grid">
<view
v-for="item in recommendations"
:key="item.text"
class="recommend-card"
@click="useRecommendation(item.text)"
>
<text class="recommend-text">{{ item.text }}</text>
<text class="recommend-tag">{{ item.tag || item.category || '灵感' }}</text>
</view>
</view>
</view>
</view>
<view v-else-if="viewState === 'generating'" class="generation-view">
<view class="conversation">
<view class="chat-bubble user">
<text>{{ wishText }}</text>
<text class="bubble-time">{{ currentMessageTime }}</text>
</view>
<view class="chat-bubble system">
<text>心愿实现中</text>
<text class="bubble-time">{{ currentMessageTime }}</text>
</view>
</view>
<view class="loading-orbit">
<view class="orbit-core"></view>
</view>
<text class="loading-copy">正在把你的心愿写成故事</text>
</view>
<view v-else class="result-view">
<view class="conversation compact">
<view class="chat-bubble user">
<text>{{ wishText }}</text>
<text class="bubble-time">{{ currentMessageTime }}</text>
</view>
<view class="chat-bubble system done">
<text>心愿已实现故事已为你展开</text>
<text class="bubble-time">{{ currentResultTime }}</text>
</view>
</view>
<view class="story-card">
<view class="story-head">
<view class="story-title-wrap">
<text class="story-title">{{ currentResult?.title || '我的人生剧本' }}</text>
<view class="tag-row">
<text v-for="tag in resultTags" :key="tag" class="tag">{{ tag }}</text>
</view>
</view>
<button class="close-icon" @click="closeResult">×</button>
</view>
<text class="story-body">{{ resultContent }}</text>
<view class="audio-section" @click="trackTtsClick">
<text class="audio-icon"></text>
<text class="audio-unavailable">{{ ttsButtonText }}</text>
</view>
<view class="result-actions">
<button class="action-btn" @click="changeDirection">换个方向</button>
<button class="action-btn" @click="notLikeMe">不像我</button>
<button class="action-btn primary" @click="trackTtsClick">{{ ttsButtonText }}</button>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { useAppStore } from '../../stores/app.js'
import analytics from '../../services/analytics.js'
import * as socialImport from '../../services/socialImport.js'
import { useTtsPlayer } from '../../composables/useTtsPlayer.js'
const store = useAppStore()
const pagePath = '/pages/main/ScriptView'
const viewState = ref('home')
const wishText = ref('')
const voiceState = ref('idle')
const generationStartedAt = ref(0)
const currentResult = ref(null)
const currentMessageTime = ref('')
const currentResultTime = ref('')
const generating = ref(false)
const remainingCount = ref(3)
const style = ref('career')
const randomRecommendations = ref([])
const useSocialInsights = ref(true)
const confirmedInsights = ref([])
const ttsPlayer = useTtsPlayer({ pagePath })
const fallbackRecommendations = [
{ text: '如果老板今天突然夸我,我的人生会怎样展开?', tag: '职场逆袭' },
{ text: '我不再内耗,专注搞钱,逆袭成行业顶尖', tag: '成长' },
{ text: '重生回18岁,这次我要活成自己喜欢的样子', tag: '重生' },
{ text: '我终于被所有人看见,也被自己认可', tag: '被认可' }
]
const recommendations = computed(() => {
const source = randomRecommendations.value.length ? randomRecommendations.value : (store.inspirationRecommendations || [])
return source.length ? source.slice(0, 4) : fallbackRecommendations
})
const socialInsightCopy = computed(() => {
if (!confirmedInsights.value.length) return '未确认画像,点这里导入社交内容'
return `已确认 ${confirmedInsights.value.length} 个,将辅助生成更像你的剧本`
})
const voiceCopy = computed(() => {
if (voiceState.value === 'pressing') return '松开后开始实现心愿'
if (voiceState.value === 'recognizing') return '正在识别你的心愿……'
if (voiceState.value === 'error') return '语音暂不可用,可以先输入文字'
return '按住说话,即刻如愿'
})
const resultTags = computed(() => {
const tags = currentResult.value?.tags
if (Array.isArray(tags) && tags.length) return tags.slice(0, 3)
const styleText = currentResult.value?.style || '爽文'
return [styleText, '成长', '被看见']
})
const resultContent = computed(() => {
return currentResult.value?.content || currentResult.value?.summary || '故事正在生成,请稍后查看。'
})
const ttsButtonText = computed(() => {
if (!currentResult.value?.id) return '生成保存后可语音播放'
return ttsPlayer.buttonText.value
})
const formatMessageTime = () => {
const date = new Date()
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${hours}:${minutes}`
}
const normalizeGeneratedScript = (data) => {
const script = data?.script || data || {}
const latestScript = Array.isArray(store.scripts) && store.scripts.length ? store.scripts[0] : null
const merged = script?.id ? script : { ...latestScript, ...script }
const content = merged?.content || merged?.plotJson?.fullContent || merged?.summary || ''
return {
id: merged?.id || '',
title: merged?.title || wishText.value || '我的人生剧本',
theme: merged?.theme || wishText.value,
style: merged?.style || '爽文',
length: merged?.length || 'medium',
tags: merged?.tags || [merged?.style || '爽文', '成长', '被看见'],
summary: merged?.summary || content.slice(0, 90),
content
}
}
const openScriptLibrary = () => {
analytics.track('script_my_scripts_click', {}, { eventType: 'script', pagePath })
uni.$emit('switchTab', 'mine')
}
const openSocialInsights = () => {
analytics.track('script_social_insights_click', {
confirmed_count: confirmedInsights.value.length
}, { eventType: 'social_import', pagePath })
uni.navigateTo({
url: confirmedInsights.value.length ? '/pages/social-import/insights' : '/pages/social-import/index'
})
}
const openSocialImport = () => {
analytics.track('script_social_import_entry_click', {
source: 'home_head'
}, { eventType: 'social_import', pagePath })
uni.navigateTo({ url: '/pages/social-import/index' })
}
const useRecommendation = (text) => {
analytics.track('script_inspiration_select', {
source: 'recommendation',
prompt_length: text.length
}, { eventType: 'script', pagePath })
wishText.value = text
}
const shuffleInspirations = async () => {
analytics.track('script_inspiration_refresh', {
source: 'home'
}, { eventType: 'script', pagePath })
const list = await store.fetchRandomInspirations(4)
randomRecommendations.value = list.length ? list : fallbackRecommendations
}
const startVoicePress = () => {
if (generating.value) return
voiceState.value = 'pressing'
analytics.track('script_voice_press_start', {}, { eventType: 'script', pagePath })
}
const cancelVoicePress = () => {
voiceState.value = 'idle'
}
const endVoicePress = async () => {
if (voiceState.value !== 'pressing') return
analytics.track('script_voice_press_end', {}, { eventType: 'script', pagePath })
voiceState.value = 'recognizing'
setTimeout(() => {
voiceState.value = 'error'
analytics.track('script_voice_recognize_fail', {
reason: 'speech_recognition_not_configured'
}, { eventType: 'script', pagePath })
uni.showToast({ title: '语音识别暂未配置,请先输入文字', icon: 'none' })
setTimeout(() => {
if (voiceState.value === 'error') voiceState.value = 'idle'
}, 1800)
}, 300)
}
const submitWish = async (source = 'text') => {
const text = wishText.value.trim()
if (!text || generating.value) return
analytics.track('script_wish_submit', {
source,
prompt_length: text.length
}, { eventType: 'script', pagePath })
currentMessageTime.value = formatMessageTime()
generationStartedAt.value = Date.now()
generating.value = true
ttsPlayer.reset()
viewState.value = 'generating'
analytics.track('script_generation_progress_view', {
source,
prompt_length: text.length
}, { eventType: 'script', pagePath })
const res = await store.generateScriptFromInspiration({
prompt: text,
style: style.value,
length: 'medium',
useSocialInsights: useSocialInsights.value
})
generating.value = false
if (!res.success) {
analytics.track('script_generate_fail', {
source,
error: res.error || 'unknown',
duration_ms: Date.now() - generationStartedAt.value
}, { eventType: 'script', pagePath })
viewState.value = 'home'
uni.showToast({ title: res.error || '生成失败', icon: 'none' })
return
}
currentResult.value = normalizeGeneratedScript(res.data)
currentResultTime.value = formatMessageTime()
if (typeof res.data?.remainingCount === 'number') remainingCount.value = res.data.remainingCount
analytics.track('script_generate_success', {
source,
style: currentResult.value.style || '',
length: currentResult.value.length || '',
use_social_insights: useSocialInsights.value,
duration_ms: Date.now() - generationStartedAt.value
}, { eventType: 'script', pagePath })
analytics.track('script_result_view', {
script_id: currentResult.value.id || '',
style: currentResult.value.style || '',
length: currentResult.value.length || ''
}, { eventType: 'script', pagePath })
viewState.value = 'result'
}
const closeResult = () => {
viewState.value = 'home'
currentResult.value = null
ttsPlayer.reset()
}
const changeDirection = () => {
analytics.track('script_result_change_direction_click', {
script_id: currentResult.value?.id || ''
}, { eventType: 'script', pagePath })
wishText.value = `${wishText.value},换一个方向重新展开`
currentResult.value = null
ttsPlayer.reset()
viewState.value = 'home'
}
const notLikeMe = () => {
analytics.track('script_result_not_like_me_click', {
script_id: currentResult.value?.id || ''
}, { eventType: 'script', pagePath })
uni.showToast({ title: '已记录反馈,可以调整心愿后再试', icon: 'none' })
}
const trackTtsClick = () => {
analytics.track('script_result_tts_click', {
script_id: currentResult.value?.id || ''
}, { eventType: 'tts', pagePath })
ttsPlayer.playSource(currentResult.value?.id || '')
}
const loadConfirmedInsights = async () => {
try {
const res = await socialImport.listInsights('confirmed')
confirmedInsights.value = res.data || []
} catch (error) {
confirmedInsights.value = []
}
}
onMounted(() => {
analytics.track('script_home_view', {}, { eventType: 'script', pagePath })
loadConfirmedInsights()
})
</script>
<style scoped>
.script-view {
min-height: 100%;
color: #fff;
font-family: "PingFang SC", "Noto Sans SC", sans-serif;
}
.wish-home,
.generation-view,
.result-view {
min-height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
padding: 4rpx 0 24rpx;
}
.wish-home {
gap: 32rpx;
}
.home-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14rpx;
}
.history-button,
.script-list-btn {
height: 64rpx;
display: flex;
align-items: center;
gap: 12rpx;
padding: 0 22rpx;
border-radius: 999rpx;
color: #e8ccff;
font-size: 28rpx;
background: rgba(43, 19, 83, 0.72);
border: 1rpx solid rgba(168, 85, 247, 0.28);
box-shadow: 0 0 22rpx rgba(116, 52, 202, 0.12);
}
.head-action-row {
display: flex;
align-items: center;
gap: 12rpx;
min-width: 0;
}
.social-import-btn {
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 0 22rpx;
border-radius: 999rpx;
color: #fff;
font-size: 25rpx;
font-weight: 800;
white-space: nowrap;
background:
radial-gradient(circle at 80% 10%, rgba(255, 255, 255, 0.26), transparent 26%),
linear-gradient(135deg, #b045ff, #612eff);
box-shadow: 0 14rpx 34rpx rgba(129, 66, 255, 0.34);
}
.history-lines {
width: 28rpx;
display: flex;
flex-direction: column;
gap: 5rpx;
}
.history-lines view {
height: 3rpx;
border-radius: 999rpx;
background: currentColor;
}
.hero-copy {
margin-top: 12rpx;
}
.hero-title {
display: block;
font-size: 76rpx;
font-weight: 800;
line-height: 1.26;
letter-spacing: 0;
}
.hero-highlight {
color: #d18aff;
text-shadow: 0 0 28rpx rgba(209, 138, 255, 0.52);
}
.orb-wrap {
position: relative;
height: 330rpx;
display: flex;
align-items: center;
justify-content: center;
}
.orb-wrap::before {
content: '';
position: absolute;
width: 420rpx;
height: 420rpx;
border-radius: 50%;
background: radial-gradient(circle, rgba(116, 41, 210, 0.42), transparent 64%);
filter: blur(6rpx);
}
.mic-orb {
position: relative;
width: 260rpx;
height: 260rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(145deg, #f1a0ff 0%, #934dff 48%, #4d1ccb 100%);
box-shadow:
0 0 72rpx rgba(169, 85, 247, 0.75),
0 0 180rpx rgba(102, 41, 201, 0.55);
transition: transform 0.18s ease, box-shadow 0.18s ease;
}
.mic-orb.pressing {
transform: scale(1.06);
box-shadow:
0 0 86rpx rgba(241, 160, 255, 0.82),
0 0 220rpx rgba(102, 41, 201, 0.68);
}
.mic-orb.recognizing {
opacity: 0.86;
}
.mic-symbol {
position: relative;
width: 88rpx;
height: 118rpx;
}
.mic-head {
width: 58rpx;
height: 78rpx;
margin: 0 auto;
border-radius: 30rpx;
border: 8rpx solid rgba(255, 255, 255, 0.92);
box-sizing: border-box;
}
.mic-stem {
width: 8rpx;
height: 34rpx;
margin: -2rpx auto 0;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.92);
}
.mic-base {
width: 58rpx;
height: 8rpx;
margin: 0 auto;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.92);
}
.voice-copy {
text-align: center;
font-size: 36rpx;
font-weight: 500;
line-height: 56rpx;
color: rgba(255, 255, 255, 0.92);
}
.wish-input-wrap {
min-height: 96rpx;
display: flex;
align-items: center;
gap: 14rpx;
padding: 12rpx 14rpx 12rpx 28rpx;
border-radius: 52rpx;
background: linear-gradient(180deg, rgba(43, 19, 83, 0.72), rgba(32, 14, 61, 0.66));
border: 1rpx solid rgba(168, 85, 247, 0.42);
box-shadow: 0 0 22rpx rgba(116, 52, 202, 0.12);
}
.wish-input {
flex: 1;
height: 72rpx;
color: #fff;
font-size: 34rpx;
}
.placeholder {
color: rgba(216, 180, 254, 0.48);
}
.send-button {
min-width: 104rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 999rpx;
color: #fff;
font-size: 28rpx;
font-weight: 700;
background: linear-gradient(145deg, #934dff, #4d1ccb);
}
.send-button.disabled {
opacity: 0.45;
}
.profile-boost {
min-height: 88rpx;
display: flex;
align-items: center;
gap: 18rpx;
padding: 18rpx 22rpx;
border-radius: 28rpx;
border: 1rpx solid rgba(168, 85, 247, 0.26);
background: rgba(43, 19, 83, 0.44);
}
.boost-main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 6rpx;
}
.boost-title {
color: #fff;
font-size: 28rpx;
font-weight: 800;
}
.boost-copy {
color: rgba(232, 204, 255, 0.72);
font-size: 23rpx;
line-height: 1.35;
}
.boost-switch {
transform: scale(0.78);
transform-origin: right center;
}
.inspiration-section {
display: flex;
flex-direction: column;
gap: 18rpx;
}
.section-line {
display: flex;
align-items: center;
justify-content: space-between;
}
.section-title {
font-size: 44rpx;
font-weight: 700;
}
.refresh {
font-size: 30rpx;
color: #e8ccff;
}
.recommend-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18rpx;
}
.recommend-card {
min-height: 142rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 16rpx;
padding: 22rpx;
border-radius: 36rpx;
background: linear-gradient(180deg, rgba(48, 24, 89, 0.78), rgba(32, 14, 62, 0.76));
border: 1rpx solid rgba(168, 85, 247, 0.22);
box-sizing: border-box;
}
.recommend-text {
font-size: 28rpx;
line-height: 40rpx;
color: rgba(255, 255, 255, 0.92);
}
.recommend-tag {
align-self: flex-start;
padding: 5rpx 14rpx;
border-radius: 999rpx;
font-size: 22rpx;
color: #d18aff;
background: rgba(168, 85, 247, 0.22);
}
.generation-view {
justify-content: center;
gap: 52rpx;
}
.conversation {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.conversation.compact {
margin-bottom: 26rpx;
}
.chat-bubble {
max-width: 86%;
display: flex;
flex-direction: column;
gap: 8rpx;
padding: 24rpx 28rpx;
border-radius: 36rpx;
font-size: 34rpx;
line-height: 52rpx;
}
.chat-bubble.user {
align-self: flex-end;
background: linear-gradient(145deg, rgba(140, 68, 242, 0.86), rgba(95, 29, 184, 0.9));
color: #fff;
}
.chat-bubble.system {
align-self: flex-start;
background: rgba(255, 255, 255, 0.07);
border: 1rpx solid rgba(192, 132, 252, 0.22);
color: rgba(255, 255, 255, 0.92);
}
.chat-bubble.done {
border-color: rgba(192, 132, 252, 0.42);
}
.bubble-time {
font-size: 24rpx;
line-height: 32rpx;
color: rgba(255, 255, 255, 0.65);
}
.loading-orbit {
position: relative;
width: 180rpx;
height: 180rpx;
align-self: center;
border-radius: 50%;
border: 3rpx solid rgba(192, 132, 252, 0.28);
box-shadow: 0 0 44rpx rgba(168, 85, 247, 0.55);
}
.loading-orbit::after {
content: '';
position: absolute;
right: 18rpx;
top: 20rpx;
width: 24rpx;
height: 24rpx;
border-radius: 50%;
background: #ffd86b;
box-shadow: 0 0 24rpx rgba(255, 216, 107, 0.72);
}
.orbit-core {
position: absolute;
inset: 42rpx;
border-radius: 50%;
background: radial-gradient(circle, #c084fc, #5c1bb0);
}
.loading-copy {
text-align: center;
font-size: 30rpx;
color: rgba(255, 255, 255, 0.75);
}
.story-card {
border-radius: 52rpx;
padding: 34rpx;
background: rgba(16, 8, 34, 0.72);
border: 1rpx solid rgba(192, 132, 252, 0.55);
box-shadow: 0 0 60rpx rgba(125, 55, 205, 0.18);
}
.story-head {
display: flex;
align-items: flex-start;
gap: 20rpx;
}
.story-title-wrap {
flex: 1;
min-width: 0;
}
.story-title {
display: block;
font-size: 52rpx;
font-weight: 700;
line-height: 1.35;
}
.close-icon {
width: 64rpx;
height: 64rpx;
line-height: 60rpx;
padding: 0;
border-radius: 50%;
color: rgba(255, 255, 255, 0.9);
background: rgba(255, 255, 255, 0.07);
border: 1rpx solid rgba(192, 132, 252, 0.32);
font-size: 42rpx;
}
.close-icon::after {
border: 0;
}
.tag-row {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 18rpx;
}
.tag {
padding: 6rpx 16rpx;
border-radius: 999rpx;
color: #d18aff;
font-size: 24rpx;
background: rgba(168, 85, 247, 0.22);
}
.story-body {
display: block;
margin-top: 28rpx;
font-size: 32rpx;
font-weight: 400;
line-height: 1.78;
color: rgba(255, 255, 255, 0.92);
white-space: pre-wrap;
}
.audio-section {
margin-top: 28rpx;
min-height: 76rpx;
padding: 0 24rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 14rpx;
color: #fff;
background: linear-gradient(135deg, rgba(36, 198, 220, 0.82), rgba(127, 90, 240, 0.88));
box-shadow: 0 12rpx 30rpx rgba(36, 198, 220, 0.18);
}
.audio-icon {
width: 34rpx;
height: 34rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: rgba(5, 6, 21, 0.86);
font-size: 18rpx;
font-weight: 900;
background: rgba(255, 255, 255, 0.86);
}
.audio-unavailable {
color: rgba(255, 255, 255, 0.92);
font-size: 25rpx;
font-weight: 800;
line-height: 1.2;
text-align: center;
}
.result-actions {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14rpx;
margin-top: 26rpx;
}
.action-btn {
height: 72rpx;
min-height: 72rpx;
padding: 0 8rpx;
border-radius: 28rpx;
display: flex;
align-items: center;
justify-content: center;
color: #e8ccff;
font-size: 26rpx;
font-weight: 700;
line-height: 1.15;
text-align: center;
white-space: normal;
box-sizing: border-box;
background: rgba(88, 28, 135, 0.18);
border: 1rpx solid rgba(192, 132, 252, 0.35);
}
.action-btn.primary {
color: #fff;
background: linear-gradient(145deg, #8c44f2, #5f1db8);
}
.action-btn::after {
border: 0;
}
</style>