260720
This commit is contained in:
881
pages/activity-outline/index.vue
Normal file
881
pages/activity-outline/index.vue
Normal file
@@ -0,0 +1,881 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user