882 lines
22 KiB
Vue
882 lines
22 KiB
Vue
<template>
|
||
<view class="page">
|
||
<scroll-view scroll-y class="page-scroll">
|
||
<view class="hero-card">
|
||
<view class="hero-kicker">AI EVENT PLANNING OUTLINE</view>
|
||
<view class="hero-title">活动策划大纲生成</view>
|
||
<view class="hero-desc">
|
||
输入活动主题、受众、时间和资金等信息,系统会调用 `/ai/activityOutlineGen`
|
||
生成结构化活动策划大纲,适合方案初稿、汇报材料和执行拆解场景。
|
||
</view>
|
||
<view class="hero-actions">
|
||
<view class="hero-action" @tap="fillExample('volunteer')">志愿服务</view>
|
||
<view class="hero-action" @tap="fillExample('salon')">主题沙龙</view>
|
||
<view class="hero-action" @tap="fillExample('sports')">校园赛事</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="panel-card">
|
||
<view class="panel-title">活动信息</view>
|
||
<view class="field-list">
|
||
<view class="field-item">
|
||
<view class="field-label">活动主题</view>
|
||
<uv-input
|
||
v-model="form.event_theme"
|
||
class="field-input"
|
||
placeholder="例如:青春志愿行 绿美校园环保行动"
|
||
placeholder-style="color: #c0c4cc;"
|
||
:maxlength="-1"
|
||
/>
|
||
</view>
|
||
<view class="field-item">
|
||
<view class="field-label">面向受众</view>
|
||
<uv-input
|
||
v-model="form.people"
|
||
class="field-input"
|
||
placeholder="例如:全校团员青年、学生骨干、青年志愿者"
|
||
placeholder-style="color: #c0c4cc;"
|
||
:maxlength="-1"
|
||
/>
|
||
</view>
|
||
<view class="field-item">
|
||
<view class="field-label">活动时间</view>
|
||
<uv-input
|
||
v-model="form.event_time"
|
||
class="field-input"
|
||
placeholder="例如:2026年4月18日 14:30-17:30"
|
||
placeholder-style="color: #c0c4cc;"
|
||
:maxlength="-1"
|
||
/>
|
||
</view>
|
||
<view class="field-item">
|
||
<view class="field-label">活动资金</view>
|
||
<uv-input
|
||
v-model="form.event_funding"
|
||
class="field-input"
|
||
placeholder="例如:预算 5000 元,来源为校团委专项经费"
|
||
placeholder-style="color: #c0c4cc;"
|
||
:maxlength="-1"
|
||
/>
|
||
</view>
|
||
<view class="field-item">
|
||
<view class="field-label">活动形式</view>
|
||
<uv-input
|
||
v-model="form.event_type"
|
||
class="field-input"
|
||
placeholder="例如:专题讲座 + 分组互动 + 现场展示"
|
||
placeholder-style="color: #c0c4cc;"
|
||
:maxlength="-1"
|
||
/>
|
||
</view>
|
||
<view class="field-item">
|
||
<view class="field-label">活动地点</view>
|
||
<uv-input
|
||
v-model="form.event_location"
|
||
class="field-input"
|
||
placeholder="例如:大学生活动中心一楼报告厅"
|
||
placeholder-style="color: #c0c4cc;"
|
||
:maxlength="-1"
|
||
/>
|
||
</view>
|
||
<view class="field-item">
|
||
<view class="field-label">备注</view>
|
||
<textarea
|
||
v-model="form.event_meme"
|
||
class="field-textarea"
|
||
placeholder="补充活动背景、目标、关键限制、预期成果或希望体现的亮点"
|
||
placeholder-style="color: #c0c4cc;"
|
||
maxlength="-1"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="submit-bar">
|
||
<view class="ghost-btn" @tap="resetForm">重置</view>
|
||
<view class="primary-btn" :class="{ 'primary-btn--disabled': !canSubmit || loading }" @tap="handleGenerate">
|
||
{{ loading ? '生成中' : '生成大纲' }}
|
||
</view>
|
||
</view>
|
||
|
||
<view class="hint-block">
|
||
<view class="hint-title">提交说明</view>
|
||
<view class="hint-text">
|
||
必填字段会直接作为 `inputs` 透传给 AI,后端固定触发“活动策划大纲生成”指令,并返回整理后的正文结果。
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="panel-card result-card">
|
||
<view class="result-head">
|
||
<view class="panel-title">生成结果</view>
|
||
<view class="result-status" :class="loading ? 'result-status--loading' : hasOutlineContent ? 'result-status--done' : ''">
|
||
{{ loading ? '生成中' : hasOutlineContent ? '已生成' : '待生成' }}
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="loading" class="loading-panel">
|
||
<view class="loading-kicker">OUTLINE GENERATING</view>
|
||
<view class="loading-title">AI 正在整理活动策划结构</view>
|
||
<view class="loading-desc">
|
||
正在综合活动主题、受众、时间与经费信息,生成可直接二次编辑的策划大纲。
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="showThinkingIndicator" class="outline-thinking">
|
||
<view class="thinking-dot"></view>
|
||
<text>AI 正在生成正文内容,请稍候...</text>
|
||
</view>
|
||
|
||
<view v-if="resultSummary" class="result-alert">
|
||
{{ resultSummary }}
|
||
</view>
|
||
|
||
<view v-if="hasOutlineContent || loading" class="field-item">
|
||
<view class="field-label">策划大纲</view>
|
||
<textarea
|
||
v-model="editableOutlineMarkdown"
|
||
class="result-textarea"
|
||
placeholder="填写左侧信息后开始生成活动策划大纲"
|
||
placeholder-style="color: #c0c4cc;"
|
||
maxlength="-1"
|
||
:disabled="loading"
|
||
/>
|
||
</view>
|
||
|
||
<view v-else class="result-empty">
|
||
填写左侧信息后开始生成活动策划大纲
|
||
</view>
|
||
|
||
<view class="result-actions">
|
||
<view class="result-btn" @tap="copyOutline">复制内容</view>
|
||
<view class="result-btn" @tap="clearResult">清空结果</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import {
|
||
activityOutlineGen,
|
||
buildWebSocketUrl,
|
||
getCurrentUser
|
||
} from './service'
|
||
|
||
const STREAM_END_TEXT = '__END__'
|
||
const SOCKET_CONNECTED_TEXT = '连接成功'
|
||
const THINK_OPEN_TAG = '<think>'
|
||
const THINK_CLOSE_TAG = '</think>'
|
||
|
||
const createDefaultForm = () => ({
|
||
event_theme: '',
|
||
people: '',
|
||
event_time: '',
|
||
event_funding: '',
|
||
event_type: '',
|
||
event_location: '',
|
||
event_meme: ''
|
||
})
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
form: createDefaultForm(),
|
||
loading: false,
|
||
result: null,
|
||
outlineMarkdown: '',
|
||
editableOutlineMarkdown: '',
|
||
resultSummary: '',
|
||
activeConversationId: '',
|
||
activeMessageId: '',
|
||
socketTask: null,
|
||
socketConnectPromise: null,
|
||
streamStarted: false,
|
||
streamResolver: null,
|
||
currentUser: {
|
||
userId: null
|
||
}
|
||
}
|
||
},
|
||
computed: {
|
||
canSubmit() {
|
||
return !!(
|
||
String(this.form.event_theme || '').trim() &&
|
||
String(this.form.people || '').trim() &&
|
||
String(this.form.event_time || '').trim() &&
|
||
String(this.form.event_funding || '').trim()
|
||
)
|
||
},
|
||
parsedOutlineContent() {
|
||
return this.parseAssistantContent(this.outlineMarkdown)
|
||
},
|
||
visibleOutlineMarkdown() {
|
||
return this.parsedOutlineContent.content
|
||
},
|
||
hasOutlineContent() {
|
||
return !!String(this.editableOutlineMarkdown || '').trim()
|
||
},
|
||
showThinkingIndicator() {
|
||
return this.loading && this.parsedOutlineContent.thinking
|
||
}
|
||
},
|
||
onUnload() {
|
||
this.clearStreamResolver()
|
||
this.disconnectWebSocket()
|
||
},
|
||
methods: {
|
||
showToast(title) {
|
||
uni.showToast({
|
||
title,
|
||
icon: 'none'
|
||
})
|
||
},
|
||
parseAssistantContent(value) {
|
||
if (!value) {
|
||
return {
|
||
content: '',
|
||
thinking: false
|
||
}
|
||
}
|
||
|
||
let cursor = 0
|
||
let content = ''
|
||
let thinking = false
|
||
|
||
while (cursor < value.length) {
|
||
if (!thinking) {
|
||
const openIndex = value.indexOf(THINK_OPEN_TAG, cursor)
|
||
const closeIndex = value.indexOf(THINK_CLOSE_TAG, cursor)
|
||
|
||
if (closeIndex !== -1 && (openIndex === -1 || closeIndex < openIndex)) {
|
||
cursor = closeIndex + THINK_CLOSE_TAG.length
|
||
continue
|
||
}
|
||
if (openIndex === -1) {
|
||
content += value.slice(cursor)
|
||
break
|
||
}
|
||
content += value.slice(cursor, openIndex)
|
||
cursor = openIndex + THINK_OPEN_TAG.length
|
||
thinking = true
|
||
continue
|
||
}
|
||
|
||
const closeIndex = value.indexOf(THINK_CLOSE_TAG, cursor)
|
||
if (closeIndex === -1) {
|
||
break
|
||
}
|
||
cursor = closeIndex + THINK_CLOSE_TAG.length
|
||
thinking = false
|
||
}
|
||
|
||
return {
|
||
content,
|
||
thinking
|
||
}
|
||
},
|
||
syncEditableOutline() {
|
||
this.editableOutlineMarkdown = this.visibleOutlineMarkdown
|
||
},
|
||
buildPayload() {
|
||
return {
|
||
event_theme: String(this.form.event_theme || '').trim(),
|
||
people: String(this.form.people || '').trim(),
|
||
event_time: String(this.form.event_time || '').trim(),
|
||
event_funding: String(this.form.event_funding || '').trim(),
|
||
event_type: String(this.form.event_type || '').trim(),
|
||
event_location: String(this.form.event_location || '').trim(),
|
||
event_meme: String(this.form.event_meme || '').trim()
|
||
}
|
||
},
|
||
pickNonEmptyString() {
|
||
for (let index = 0; index < arguments.length; index += 1) {
|
||
const value = arguments[index]
|
||
if (typeof value === 'string' && value.length > 0) {
|
||
return value
|
||
}
|
||
}
|
||
return ''
|
||
},
|
||
pickRawString() {
|
||
for (let index = 0; index < arguments.length; index += 1) {
|
||
const value = arguments[index]
|
||
if (typeof value === 'string') {
|
||
return value
|
||
}
|
||
}
|
||
return undefined
|
||
},
|
||
stringifyUnknown(value) {
|
||
try {
|
||
return JSON.stringify(value, null, 2)
|
||
} catch (error) {
|
||
return String(value || '')
|
||
}
|
||
},
|
||
parseOutline(payload) {
|
||
if (!payload) {
|
||
return ''
|
||
}
|
||
const answer = this.pickNonEmptyString(
|
||
payload.answer,
|
||
payload.raw && payload.raw.answer,
|
||
payload.raw && payload.raw.data && payload.raw.data.answer,
|
||
payload.raw && payload.raw.output,
|
||
payload.raw && payload.raw.result,
|
||
payload.raw && payload.raw.content,
|
||
payload.raw && payload.raw.message
|
||
)
|
||
if (answer) {
|
||
return answer
|
||
}
|
||
return this.stringifyUnknown((payload && payload.raw) || payload)
|
||
},
|
||
buildSummary() {
|
||
const parts = []
|
||
if (this.activeConversationId) {
|
||
parts.push(`conversationId: ${this.activeConversationId}`)
|
||
}
|
||
if (this.activeMessageId) {
|
||
parts.push(`messageId: ${this.activeMessageId}`)
|
||
}
|
||
return parts.join(' | ')
|
||
},
|
||
clearStreamResolver() {
|
||
this.streamResolver = null
|
||
},
|
||
resolveStream() {
|
||
if (this.streamResolver && this.streamResolver.resolve) {
|
||
this.streamResolver.resolve()
|
||
}
|
||
this.clearStreamResolver()
|
||
},
|
||
rejectStream(error) {
|
||
if (this.streamResolver && this.streamResolver.reject) {
|
||
this.streamResolver.reject(error)
|
||
}
|
||
this.clearStreamResolver()
|
||
},
|
||
createStreamWaiter() {
|
||
this.streamStarted = false
|
||
return new Promise((resolve, reject) => {
|
||
this.streamResolver = {
|
||
resolve,
|
||
reject
|
||
}
|
||
})
|
||
},
|
||
disconnectWebSocket() {
|
||
this.socketConnectPromise = null
|
||
const socketTask = this.socketTask
|
||
this.socketTask = null
|
||
if (socketTask && typeof socketTask.close === 'function') {
|
||
try {
|
||
socketTask.close({})
|
||
} catch (error) {
|
||
console.warn('socket close failed', error)
|
||
}
|
||
}
|
||
},
|
||
appendOutlineChunk(chunk, conversationId, messageId) {
|
||
if (!chunk) {
|
||
return
|
||
}
|
||
if (conversationId && conversationId !== '0') {
|
||
this.activeConversationId = conversationId
|
||
}
|
||
if (messageId && messageId !== '0') {
|
||
this.activeMessageId = messageId
|
||
}
|
||
if (!this.streamStarted) {
|
||
this.streamStarted = true
|
||
this.outlineMarkdown = ''
|
||
}
|
||
this.outlineMarkdown += chunk
|
||
this.syncEditableOutline()
|
||
this.resultSummary = this.buildSummary() || '正在通过 WebSocket 接收流式内容'
|
||
},
|
||
finishStream(conversationId, messageId) {
|
||
if (conversationId && conversationId !== '0') {
|
||
this.activeConversationId = conversationId
|
||
}
|
||
if (messageId && messageId !== '0') {
|
||
this.activeMessageId = messageId
|
||
}
|
||
this.resultSummary = this.buildSummary() || '已通过 WebSocket 接收生成结果'
|
||
this.loading = false
|
||
this.resolveStream()
|
||
this.disconnectWebSocket()
|
||
},
|
||
handleSocketMessage(raw) {
|
||
if (!raw) {
|
||
return
|
||
}
|
||
try {
|
||
const data = JSON.parse(raw)
|
||
if (data && data.answer === STREAM_END_TEXT) {
|
||
this.finishStream(data.conversationId, data.messageId)
|
||
return
|
||
}
|
||
if (data && (data.loading === 'true' || data.loading === true)) {
|
||
this.resultSummary = 'AI 正在组织活动策划大纲结构...'
|
||
return
|
||
}
|
||
const chunk = this.pickRawString(data && data.notice, data && data.answer)
|
||
if (typeof chunk === 'string') {
|
||
this.appendOutlineChunk(chunk, data && data.conversationId, data && data.messageId)
|
||
}
|
||
} catch (error) {
|
||
this.appendOutlineChunk(raw)
|
||
}
|
||
},
|
||
refreshUser() {
|
||
this.currentUser = getCurrentUser()
|
||
return this.currentUser
|
||
},
|
||
connectWebSocket() {
|
||
const user = this.refreshUser()
|
||
if (!user.userId) {
|
||
return Promise.reject(new Error('未获取到当前登录用户'))
|
||
}
|
||
const currentSocket = this.socketTask
|
||
if (currentSocket && this.socketConnectPromise) {
|
||
return this.socketConnectPromise
|
||
}
|
||
if (currentSocket) {
|
||
this.disconnectWebSocket()
|
||
}
|
||
|
||
let cleanupPending = () => undefined
|
||
const connectPromise = new Promise((resolve, reject) => {
|
||
let settled = false
|
||
const socketTask = uni.connectSocket({
|
||
url: buildWebSocketUrl(user.userId),
|
||
complete: () => {}
|
||
})
|
||
|
||
cleanupPending = () => {
|
||
if (this.socketConnectPromise === connectPromise) {
|
||
this.socketConnectPromise = null
|
||
}
|
||
}
|
||
|
||
socketTask.onMessage((event) => {
|
||
const raw = String((event && event.data) || '')
|
||
if (raw === SOCKET_CONNECTED_TEXT) {
|
||
if (!settled) {
|
||
settled = true
|
||
cleanupPending()
|
||
resolve()
|
||
}
|
||
return
|
||
}
|
||
this.handleSocketMessage(raw)
|
||
})
|
||
socketTask.onClose(() => {
|
||
if (this.socketTask === socketTask) {
|
||
this.socketTask = null
|
||
}
|
||
if (!settled) {
|
||
settled = true
|
||
cleanupPending()
|
||
reject(new Error('活动策划大纲连接已关闭'))
|
||
return
|
||
}
|
||
if (this.loading) {
|
||
this.rejectStream(new Error('活动策划大纲连接已关闭'))
|
||
this.loading = false
|
||
}
|
||
cleanupPending()
|
||
})
|
||
socketTask.onError(() => {
|
||
if (!settled) {
|
||
settled = true
|
||
cleanupPending()
|
||
reject(new Error('活动策划大纲连接失败'))
|
||
return
|
||
}
|
||
if (this.loading) {
|
||
this.rejectStream(new Error('活动策划大纲连接失败'))
|
||
this.loading = false
|
||
}
|
||
})
|
||
this.socketTask = socketTask
|
||
})
|
||
this.socketConnectPromise = connectPromise
|
||
return connectPromise
|
||
},
|
||
clearResult() {
|
||
this.result = null
|
||
this.outlineMarkdown = ''
|
||
this.editableOutlineMarkdown = ''
|
||
this.resultSummary = ''
|
||
this.activeConversationId = ''
|
||
this.activeMessageId = ''
|
||
},
|
||
resetForm() {
|
||
this.form = createDefaultForm()
|
||
this.clearResult()
|
||
},
|
||
async handleGenerate() {
|
||
if (this.loading) {
|
||
return
|
||
}
|
||
if (!this.canSubmit) {
|
||
this.showToast('请先填写全部必填项')
|
||
return
|
||
}
|
||
this.loading = true
|
||
this.clearResult()
|
||
this.resultSummary = '正在建立 WebSocket 连接...'
|
||
try {
|
||
await this.connectWebSocket()
|
||
this.resultSummary = '连接成功,已提交生成任务,等待流式返回...'
|
||
const waitStream = this.createStreamWaiter()
|
||
const data = await activityOutlineGen(this.buildPayload())
|
||
this.result = data || null
|
||
if (!this.streamStarted && data && this.parseOutline(data)) {
|
||
this.outlineMarkdown = this.parseOutline(data)
|
||
this.syncEditableOutline()
|
||
this.resultSummary = this.buildSummary() || '接口直接返回生成结果'
|
||
this.loading = false
|
||
this.resolveStream()
|
||
this.disconnectWebSocket()
|
||
} else {
|
||
await waitStream
|
||
}
|
||
if (!this.visibleOutlineMarkdown) {
|
||
this.showToast('接口已返回,但未解析到正文内容')
|
||
return
|
||
}
|
||
this.showToast('活动策划大纲生成完成')
|
||
} catch (error) {
|
||
this.disconnectWebSocket()
|
||
this.clearStreamResolver()
|
||
this.loading = false
|
||
this.showToast((error && error.message) || '生成失败')
|
||
}
|
||
},
|
||
copyOutline() {
|
||
if (!this.editableOutlineMarkdown) {
|
||
this.showToast('暂无可复制内容')
|
||
return
|
||
}
|
||
uni.setClipboardData({
|
||
data: this.editableOutlineMarkdown,
|
||
success: () => {
|
||
this.showToast('内容已复制')
|
||
}
|
||
})
|
||
},
|
||
fillExample(type) {
|
||
if (type === 'volunteer') {
|
||
this.form.event_theme = '青春志愿行 绿美校园环保行动'
|
||
this.form.people = '全校团员青年、学生骨干、青年志愿者'
|
||
this.form.event_time = '2026年4月18日 14:30-17:30'
|
||
this.form.event_funding = '预算 5000 元,来源为校团委专项经费'
|
||
this.form.event_type = '志愿服务 + 分组清洁 + 环保倡议'
|
||
this.form.event_location = '校园主干道、教学楼周边和青年林'
|
||
this.form.event_meme = '突出劳动教育、志愿服务精神和绿色校园建设,生成含活动背景、流程、分工、宣传和风险预案的大纲。'
|
||
return
|
||
}
|
||
if (type === 'salon') {
|
||
this.form.event_theme = '青春思享汇 新时代青年成长主题沙龙'
|
||
this.form.people = '学生干部、团支部书记、青年学生代表'
|
||
this.form.event_time = '2026年5月10日 19:00-21:00'
|
||
this.form.event_funding = '预算 3000 元,包含物料、海报和场地布置'
|
||
this.form.event_type = '主题分享 + 圆桌讨论 + 互动提问'
|
||
this.form.event_location = '大学生活动中心多功能厅'
|
||
this.form.event_meme = '希望突出思想引领、青年交流和成果展示,生成适合汇报审批的结构化活动方案。'
|
||
return
|
||
}
|
||
this.form.event_theme = '青春杯校园三人篮球赛'
|
||
this.form.people = '各学院学生代表队、体育类社团、观赛学生'
|
||
this.form.event_time = '2026年4月下旬周末全天'
|
||
this.form.event_funding = '预算 8000 元,含裁判、奖品、宣传和后勤保障'
|
||
this.form.event_type = '校园赛事 + 开幕仪式 + 分组淘汰赛'
|
||
this.form.event_location = '学校室外篮球场'
|
||
this.form.event_meme = '需要体现赛事组织、赛程安排、安全保障、医疗预案和宣传报道计划。'
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.page {
|
||
min-height: 100vh;
|
||
background:
|
||
radial-gradient(circle at top right, rgba(51, 97, 164, 0.14), transparent 26%),
|
||
linear-gradient(180deg, #f7f9fc 0%, #f2f4f8 100%);
|
||
}
|
||
|
||
.page-scroll {
|
||
height: 100vh;
|
||
padding: 28rpx 24rpx 40rpx;
|
||
}
|
||
|
||
.hero-card {
|
||
padding: 34rpx 30rpx;
|
||
border-radius: 32rpx;
|
||
background: linear-gradient(135deg, #16315f 0%, #26508e 52%, #4b79bb 100%);
|
||
box-shadow: 0 18rpx 44rpx rgba(33, 67, 121, 0.18);
|
||
color: #eef5ff;
|
||
}
|
||
|
||
.hero-kicker {
|
||
font-size: 22rpx;
|
||
letter-spacing: 4rpx;
|
||
color: rgba(238, 245, 255, 0.72);
|
||
}
|
||
|
||
.hero-title {
|
||
margin-top: 18rpx;
|
||
font-size: 42rpx;
|
||
line-height: 1.35;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.hero-desc {
|
||
margin-top: 16rpx;
|
||
font-size: 24rpx;
|
||
line-height: 1.8;
|
||
color: rgba(238, 245, 255, 0.88);
|
||
}
|
||
|
||
.hero-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 14rpx;
|
||
margin-top: 26rpx;
|
||
}
|
||
|
||
.hero-action {
|
||
padding: 12rpx 20rpx;
|
||
border-radius: 999rpx;
|
||
background: rgba(255, 255, 255, 0.12);
|
||
font-size: 22rpx;
|
||
color: #f4f8ff;
|
||
}
|
||
|
||
.panel-card {
|
||
margin-top: 24rpx;
|
||
padding: 26rpx;
|
||
border-radius: 30rpx;
|
||
background: rgba(255, 255, 255, 0.96);
|
||
border: 1rpx solid rgba(51, 97, 164, 0.08);
|
||
box-shadow: 0 10rpx 28rpx rgba(61, 73, 95, 0.08);
|
||
}
|
||
|
||
.panel-title {
|
||
font-size: 30rpx;
|
||
font-weight: 700;
|
||
color: #1f2329;
|
||
}
|
||
|
||
.field-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 22rpx;
|
||
margin-top: 20rpx;
|
||
}
|
||
|
||
.field-item {
|
||
width: 100%;
|
||
}
|
||
|
||
.field-label {
|
||
margin-bottom: 12rpx;
|
||
font-size: 25rpx;
|
||
font-weight: 600;
|
||
color: #2b3037;
|
||
}
|
||
|
||
.field-textarea {
|
||
width: 100%;
|
||
min-height: 220rpx;
|
||
padding: 22rpx 24rpx;
|
||
border-radius: 22rpx;
|
||
background: #f6f8fb;
|
||
border: 1rpx solid #dde4ef;
|
||
font-size: 24rpx;
|
||
line-height: 1.8;
|
||
color: #2a2e35;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
:deep(.field-input.uv-input) {
|
||
width: 100%;
|
||
min-height: 88rpx;
|
||
padding: 0 24rpx;
|
||
border-radius: 12rpx;
|
||
border: 1px solid #dcdfe6;
|
||
background: #ffffff;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
:deep(.field-input.uv-input .uv-input__content__field-wrapper__field) {
|
||
height: 88rpx;
|
||
min-height: 88rpx;
|
||
font-size: 28rpx;
|
||
line-height: 88rpx;
|
||
color: #303133;
|
||
}
|
||
|
||
.submit-bar,
|
||
.result-actions {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 18rpx;
|
||
margin-top: 24rpx;
|
||
}
|
||
|
||
.ghost-btn,
|
||
.primary-btn,
|
||
.result-btn {
|
||
height: 88rpx;
|
||
line-height: 88rpx;
|
||
border-radius: 22rpx;
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
text-align: center;
|
||
}
|
||
|
||
.ghost-btn {
|
||
background: #f6f7fa;
|
||
color: #50617a;
|
||
border: 1rpx solid #d8dfeb;
|
||
}
|
||
|
||
.primary-btn {
|
||
background: linear-gradient(135deg, #1f4f8f 0%, #3f70b5 100%);
|
||
color: #fff;
|
||
}
|
||
|
||
.primary-btn--disabled {
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.result-btn {
|
||
background: #eef4fb;
|
||
color: #24508f;
|
||
}
|
||
|
||
.hint-block,
|
||
.result-alert {
|
||
margin-top: 22rpx;
|
||
padding: 22rpx 24rpx;
|
||
border-radius: 24rpx;
|
||
background: #f5f8fd;
|
||
border: 1rpx solid rgba(51, 97, 164, 0.08);
|
||
font-size: 22rpx;
|
||
line-height: 1.75;
|
||
color: #6d7d93;
|
||
}
|
||
|
||
.hint-title {
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
color: #35527d;
|
||
}
|
||
|
||
.hint-text {
|
||
margin-top: 10rpx;
|
||
}
|
||
|
||
.result-card {
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.result-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.result-status {
|
||
padding: 10rpx 16rpx;
|
||
border-radius: 999rpx;
|
||
background: #eef1f5;
|
||
color: #7e8792;
|
||
font-size: 20rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.result-status--loading {
|
||
background: #edf5ff;
|
||
color: #1554ad;
|
||
}
|
||
|
||
.result-status--done {
|
||
background: #eef9f1;
|
||
color: #1f8f49;
|
||
}
|
||
|
||
.loading-panel {
|
||
margin-top: 22rpx;
|
||
padding: 26rpx;
|
||
border-radius: 26rpx;
|
||
background: linear-gradient(135deg, #f5f9ff 0%, #eef4fb 100%);
|
||
}
|
||
|
||
.loading-kicker {
|
||
font-size: 20rpx;
|
||
letter-spacing: 3rpx;
|
||
color: #6682aa;
|
||
}
|
||
|
||
.loading-title {
|
||
margin-top: 10rpx;
|
||
font-size: 30rpx;
|
||
font-weight: 700;
|
||
color: #23446f;
|
||
}
|
||
|
||
.loading-desc {
|
||
margin-top: 12rpx;
|
||
font-size: 22rpx;
|
||
line-height: 1.75;
|
||
color: #6d7d93;
|
||
}
|
||
|
||
.outline-thinking {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
margin-top: 20rpx;
|
||
font-size: 22rpx;
|
||
color: #35527d;
|
||
}
|
||
|
||
.thinking-dot {
|
||
width: 16rpx;
|
||
height: 16rpx;
|
||
border-radius: 50%;
|
||
background: #4b79bb;
|
||
}
|
||
|
||
.result-textarea {
|
||
width: 100%;
|
||
min-height: 560rpx;
|
||
padding: 24rpx;
|
||
border-radius: 24rpx;
|
||
background: #f8fafc;
|
||
border: 1rpx solid #e1e7f0;
|
||
font-size: 24rpx;
|
||
line-height: 1.85;
|
||
color: #2b3037;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.result-empty {
|
||
margin-top: 22rpx;
|
||
padding: 32rpx 24rpx;
|
||
border-radius: 24rpx;
|
||
background: #f8fafc;
|
||
font-size: 24rpx;
|
||
line-height: 1.75;
|
||
color: #8a94a0;
|
||
text-align: center;
|
||
}
|
||
</style>
|