将组件级 i18n 对象重构为统一由全局 locales/messages.js 管理,确保中英文语言切换时正确显示对应文本。 - 新增 utils/i18n.js 国际化工具函数 - 更新 store/$t.mixin.js 翻译混入逻辑 - 迁移 pkgService、pkgBooking、pkgAccount、pkgExplore、pages 等共 58 个组件的中英文文本至全局消息表
1257 lines
44 KiB
Vue
1257 lines
44 KiB
Vue
<template>
|
|
<view class="submit-request-page tn-safe-area-inset-bottom">
|
|
<tn-nav-bar
|
|
:isBack="true"
|
|
:bottomShadow="false"
|
|
:backgroundColor="navBarBackgroundColor"
|
|
:customBackStyle="navBarBackStyle"
|
|
:fixed="true"
|
|
class="glass-nav-bar"
|
|
>
|
|
<view class="tn-text-bold tn-text-xl tn-color-black" :style="{ opacity: navTitleOpacity }">{{ pageTitle }}</view>
|
|
</tn-nav-bar>
|
|
|
|
<scroll-view class="page-scroll" scroll-y @scroll="handleScroll" :show-scrollbar="false">
|
|
<view class="page-content tn-padding" :style="{ paddingTop: vuex_custom_bar_height + 'px', paddingBottom: pageBottomPadding }">
|
|
<view class="hero-header tn-margin-bottom-xl">
|
|
<view class="hero-badge">
|
|
<text class="badge-dot"></text>
|
|
<text>APPLICATION</text>
|
|
</view>
|
|
<view class="hero-title">{{ pageTitle }}</view>
|
|
<view class="hero-desc">{{ pageDesc }}</view>
|
|
</view>
|
|
|
|
<view class="form-card tn-margin-top-xl">
|
|
<view class="block-title">{{ formBlockTitle }}</view>
|
|
<view class="block-subtitle">{{ formBlockDesc }}</view>
|
|
|
|
<view v-if="showRoomTypeSelector" class="form-item tn-margin-top">
|
|
<view class="form-label">{{ roomTypeLabel }}</view>
|
|
<room-type-selector
|
|
v-model="form.intendedRoomTypeCode"
|
|
:initial-area-code="form.intendedAreaCode"
|
|
@change="handleRoomTypeChange"
|
|
/>
|
|
</view>
|
|
|
|
<view v-if="showFloorSelector" class="form-item tn-margin-top">
|
|
<view class="form-label">{{ floorLabel }}</view>
|
|
<floor-selector
|
|
v-model="form.intendedFloorLevel"
|
|
@change="handleFloorChange"
|
|
/>
|
|
</view>
|
|
|
|
<view v-if="showPreferenceSelectors" class="form-item tn-margin-top">
|
|
<view class="form-label">{{ $tr('兴趣爱好') }}</view>
|
|
<tag-selector
|
|
v-model="form.hobbiesAndInterestsTagKeys"
|
|
:options="hobbyOptions"
|
|
:loading="dictLoading"
|
|
:emptyText="$tr('暂无兴趣爱好标签')"
|
|
/>
|
|
</view>
|
|
|
|
<view v-if="showPreferenceSelectors" class="form-item tn-margin-top">
|
|
<view class="form-label">{{ $tr('生活习惯') }}</view>
|
|
<tag-selector
|
|
v-model="form.lifestyleHabitsTagKeys"
|
|
:options="lifestyleOptions"
|
|
:loading="dictLoading"
|
|
:emptyText="$tr('暂无生活习惯标签')"
|
|
/>
|
|
</view>
|
|
|
|
<view v-if="showDateRange" class="form-item tn-margin-top">
|
|
<view class="form-label">{{ dateRangeLabel }}</view>
|
|
<date-range-selector
|
|
:start.sync="form.start"
|
|
:end.sync="form.end"
|
|
:start-label="dateRangeStartLabel"
|
|
:end-label="dateRangeEndLabel"
|
|
:start-placeholder="dateRangeStartPlaceholder"
|
|
:end-placeholder="dateRangeEndPlaceholder"
|
|
/>
|
|
</view>
|
|
|
|
<view class="form-item tn-margin-top">
|
|
<view class="form-label">{{ $tr('补充说明') }}</view>
|
|
<textarea class="form-textarea" :placeholder="remarkPlaceholder" v-model="form.remark"></textarea>
|
|
</view>
|
|
|
|
<view v-if="!showFloatingSubmitBtn" class="submit-btn btn-primary tn-margin-top-xl" :class="{ 'submit-btn--disabled': isSubmitting }" @click="submitRequest">
|
|
<text class="tn-text-bold">{{ isSubmitting ? $tr('提交中...') : primaryActionText }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
</scroll-view>
|
|
|
|
<view v-if="showFloatingSubmitBtn" class="floating-submit-bar">
|
|
<view class="floating-submit-bar__inner">
|
|
<view class="submit-btn btn-primary" :class="{ 'submit-btn--disabled': isSubmitting }" @click="submitRequest">
|
|
<text class="tn-text-bold">{{ isSubmitting ? $tr('提交中...') : primaryActionText }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
import { submitApplicationForm, sysdictPage } from '@/api/index.js'
|
|
import { saveActiveApplicationCode, saveHomePageTabId } from '@/utils/app-state'
|
|
import { compactEnglishText, localizePayMethodText } from '../../utils/select-room-normalizers'
|
|
import { getStoredUser } from '../../utils/flow'
|
|
|
|
import TagSelector from './components/TagSelector.vue'
|
|
import DateRangeSelector from './components/DateRangeSelector.vue'
|
|
import RoomTypeSelector from './components/RoomTypeSelector.vue'
|
|
import FloorSelector from './components/FloorSelector.vue'
|
|
|
|
function createDefaultForm() {
|
|
const now = new Date()
|
|
const pad = n => String(n).padStart(2, '0')
|
|
const defaultStart = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`
|
|
|
|
return {
|
|
intendedAreaCode: '',
|
|
intendedRoomTypeCode: '',
|
|
intendedFloorLevel: '',
|
|
roomSlotHeight: 1,
|
|
hobbiesAndInterestsTagKeys: [],
|
|
lifestyleHabitsTagKeys: [],
|
|
remark: '',
|
|
start: defaultStart,
|
|
end: ''
|
|
}
|
|
}
|
|
|
|
const DICT_TEXT_FALLBACK_MAP = {
|
|
hobbies: {
|
|
'打篮球': 'Play Basketball',
|
|
'打羽毛球': 'Play Badminton',
|
|
'唱歌': 'Sing',
|
|
'电竞': 'Esports Games',
|
|
'电竞游戏': 'Esports Games',
|
|
'旅游': 'Travel'
|
|
},
|
|
lifestyle: {
|
|
'晚睡': 'Stay Up Late',
|
|
'早睡': 'Sleep Early',
|
|
'早起': 'Wake Up Early',
|
|
'安静': 'Quiet',
|
|
'整洁': 'Tidy',
|
|
'爱干净': 'Neat'
|
|
}
|
|
}
|
|
|
|
function getOptionTextCandidates(item = {}) {
|
|
return [
|
|
item.value,
|
|
item.label,
|
|
item.description,
|
|
item.name,
|
|
item.cname,
|
|
item.nameZh,
|
|
item.labelZh,
|
|
item.valueZh,
|
|
item.descriptionZh,
|
|
item.ename,
|
|
item.nameEn,
|
|
item.labelEn,
|
|
item.valueEn,
|
|
item.descriptionEn,
|
|
item.code
|
|
].map(value => String(value || '').trim()).filter(Boolean)
|
|
}
|
|
|
|
function getDictFallbackText(item = {}, language = 'zh', type = '') {
|
|
if (language !== 'en') {
|
|
return ''
|
|
}
|
|
const fallbackMap = DICT_TEXT_FALLBACK_MAP[type] || {}
|
|
const baseText = [
|
|
item.description,
|
|
item.value,
|
|
item.label,
|
|
item.name,
|
|
item.cname,
|
|
item.nameZh,
|
|
item.labelZh,
|
|
item.valueZh,
|
|
item.descriptionZh
|
|
].map(value => String(value || '').trim()).find(Boolean)
|
|
return baseText ? (fallbackMap[baseText] || '') : ''
|
|
}
|
|
|
|
function getLocalizedOptionText(item = {}, language = 'zh', type = '') {
|
|
const isEn = language === 'en'
|
|
const preferred = isEn
|
|
? [item.descriptionEn, item.ename, item.nameEn, item.labelEn, item.valueEn, item.description, item.value, item.label, item.name, item.cname, item.code]
|
|
: [item.description, item.cname, item.nameZh, item.labelZh, item.valueZh, item.value, item.label, item.name, item.code]
|
|
const matched = preferred.map(value => String(value || '').trim()).find(Boolean)
|
|
if (!matched) {
|
|
return getDictFallbackText(item, language, type)
|
|
}
|
|
if (isEn) {
|
|
const fallbackMap = DICT_TEXT_FALLBACK_MAP[type] || {}
|
|
if (fallbackMap[matched]) {
|
|
return fallbackMap[matched]
|
|
}
|
|
const zhText = [item.label, item.name, item.cname, item.nameZh, item.labelZh, item.description, item.value, item.valueZh, item.descriptionZh]
|
|
.map(value => String(value || '').trim())
|
|
.find(Boolean)
|
|
if (zhText && fallbackMap[zhText]) {
|
|
return fallbackMap[zhText]
|
|
}
|
|
if (zhText && matched === zhText) {
|
|
return getDictFallbackText(item, language, type) || matched
|
|
}
|
|
}
|
|
return matched
|
|
}
|
|
|
|
function normalizeDictList(response, language = 'zh', type = '') {
|
|
const raw = response && response.data ? (response.data.data || response.data) : response
|
|
const list = Array.isArray(raw) ? raw : []
|
|
return list
|
|
.map(item => ({
|
|
key: item && (item.key || item.code || item.value || ''),
|
|
value: getLocalizedOptionText(item, language, type) || getOptionTextCandidates(item)[0] || ''
|
|
}))
|
|
.filter(item => item.key && item.value)
|
|
}
|
|
|
|
function normalizeDateTime(date, endOfDay = false) {
|
|
if (!date) {
|
|
return ''
|
|
}
|
|
return `${date}T${endOfDay ? '23:59:59' : '00:00:00'}`
|
|
}
|
|
|
|
function buildRoomLabel(payload = {}) {
|
|
return payload.roomLabel || payload.currentRoomLabel || payload.roomName || ''
|
|
}
|
|
|
|
function isPlainObject(value) {
|
|
return !!value && typeof value === 'object' && !Array.isArray(value)
|
|
}
|
|
|
|
function normalizeCodeValue(value) {
|
|
if (value === undefined || value === null) {
|
|
return ''
|
|
}
|
|
if (typeof value === 'string' || typeof value === 'number') {
|
|
return String(value).trim()
|
|
}
|
|
return ''
|
|
}
|
|
|
|
function extractSubmitApplicationCode(response) {
|
|
const payload = response && response.data !== undefined ? response.data : response
|
|
const directValues = [
|
|
payload,
|
|
payload && payload.data,
|
|
payload && payload.result,
|
|
payload && payload.record,
|
|
payload && payload.application
|
|
]
|
|
|
|
for (let i = 0; i < directValues.length; i += 1) {
|
|
const directCode = normalizeCodeValue(directValues[i])
|
|
if (directCode) {
|
|
return directCode
|
|
}
|
|
}
|
|
|
|
const sources = directValues.filter(isPlainObject)
|
|
|
|
const codeFields = ['detailCode', 'code', 'applicationCode', 'formCode', 'referenceTypeNo', 'no']
|
|
for (let i = 0; i < sources.length; i += 1) {
|
|
const current = sources[i]
|
|
for (let j = 0; j < codeFields.length; j += 1) {
|
|
const nextCode = normalizeCodeValue(current[codeFields[j]])
|
|
if (nextCode) {
|
|
return nextCode
|
|
}
|
|
}
|
|
}
|
|
return ''
|
|
}
|
|
|
|
const BUSINESS_TYPE_CONFIG = {
|
|
'change-room': {
|
|
title: '换房申请',
|
|
titleEn: 'Change Room Request',
|
|
titleEnShort: 'Room Change',
|
|
desc: '请填写期望房型、计划换房时间和补充说明,提交后由管家继续跟进。',
|
|
descEn: 'Fill in the expected room type, move date, and notes. Your butler will follow up after submission.',
|
|
formBlockTitle: '换房申请信息',
|
|
formBlockTitleEn: 'Change Room Details',
|
|
formBlockDesc: '将通过真实申请接口提交换房意向。',
|
|
formBlockDescEn: 'Your change room request will be submitted through the live application API.',
|
|
primaryActionText: '提交换房申请',
|
|
primaryActionTextEn: 'Submit Change Room Request',
|
|
remarkPlaceholder: '请输入换房原因、房间偏好和其他补充说明',
|
|
remarkPlaceholderEn: 'Enter the reason, room preference, and additional notes',
|
|
roomTypeLabel: '期望房型',
|
|
roomTypeLabelEn: 'Expected Room Type',
|
|
dateRangeLabel: '时间安排',
|
|
dateRangeLabelEn: 'Schedule',
|
|
dateRangeStartLabel: '期望换房',
|
|
dateRangeStartLabelEn: 'Move Date',
|
|
dateRangeEndLabel: '预计入住',
|
|
dateRangeEndLabelEn: 'Expected Check-in',
|
|
dateRangeStartPlaceholder: '请选择换房日期',
|
|
dateRangeStartPlaceholderEn: 'Select move date',
|
|
dateRangeEndPlaceholder: '请选择入住日期',
|
|
dateRangeEndPlaceholderEn: 'Select check-in date',
|
|
showRoomTypeSelector: true,
|
|
showPreferenceSelectors: false,
|
|
showDateRange: true,
|
|
buildDefaultRemark(payload = {}) {
|
|
const roomLabel = buildRoomLabel(payload)
|
|
const language = payload.language || 'zh'
|
|
if (language === 'en') {
|
|
return roomLabel
|
|
? `I am currently staying in "${roomLabel}" and would like to request a room change. Please help confirm available room types and processing time.`
|
|
: 'I would like to request a room change. Please help confirm available room types and processing time.'
|
|
}
|
|
return roomLabel
|
|
? `我当前居住房间为“${roomLabel}”,现申请换房,请协助确认可调整房型和办理时间。`
|
|
: '我想申请换房,请协助确认可调整房型和办理时间。'
|
|
}
|
|
},
|
|
'renew-stay': {
|
|
title: '续住申请',
|
|
titleEn: 'Renew Stay Request',
|
|
titleEnShort: 'Renew Stay',
|
|
desc: '请填写计划续住时间和补充说明,提交后由管家继续跟进。',
|
|
descEn: 'Fill in the renewal period and notes. Your butler will follow up after submission.',
|
|
formBlockTitle: '续住申请信息',
|
|
formBlockTitleEn: 'Renewal Details',
|
|
formBlockDesc: '将通过真实申请接口提交续住意向。',
|
|
formBlockDescEn: 'Your renewal request will be submitted through the live application API.',
|
|
primaryActionText: '提交续住申请',
|
|
primaryActionTextEn: 'Submit Renewal Request',
|
|
remarkPlaceholder: '请输入续住原因、计划续住时长和其他说明',
|
|
remarkPlaceholderEn: 'Enter the renewal reason, duration, and notes',
|
|
dateRangeLabel: '续住时间',
|
|
dateRangeLabelEn: 'Renewal Period',
|
|
dateRangeStartLabel: '当前到期',
|
|
dateRangeStartLabelEn: 'Current End Date',
|
|
dateRangeEndLabel: '计划续住至',
|
|
dateRangeEndLabelEn: 'Renew Until',
|
|
dateRangeStartPlaceholder: '请选择当前到期日',
|
|
dateRangeStartPlaceholderEn: 'Select current end date',
|
|
dateRangeEndPlaceholder: '请选择续住到期日',
|
|
dateRangeEndPlaceholderEn: 'Select renewed end date',
|
|
showRoomTypeSelector: false,
|
|
showPreferenceSelectors: false,
|
|
showDateRange: true,
|
|
buildDefaultRemark(payload = {}) {
|
|
const roomLabel = buildRoomLabel(payload)
|
|
const language = payload.language || 'zh'
|
|
if (language === 'en') {
|
|
return roomLabel
|
|
? `I am currently staying in "${roomLabel}" and would like to renew my stay. Please help confirm the renewal process.`
|
|
: 'I would like to renew my stay. Please help confirm the renewal process.'
|
|
}
|
|
return roomLabel
|
|
? `我当前居住房间为“${roomLabel}”,现申请续住,请协助确认续住办理流程。`
|
|
: '我想申请续住,请协助确认续住办理流程。'
|
|
}
|
|
},
|
|
checkout: {
|
|
title: '退房申请',
|
|
titleEn: 'Checkout Request',
|
|
titleEnShort: 'Check Out',
|
|
desc: '请填写计划退房时间和离场说明,提交后由管家安排后续验房流程。',
|
|
descEn: 'Fill in the checkout schedule and departure notes. Your butler will arrange the inspection process.',
|
|
formBlockTitle: '退房申请信息',
|
|
formBlockTitleEn: 'Checkout Details',
|
|
formBlockDesc: '将通过真实申请接口提交退房意向。',
|
|
formBlockDescEn: 'Your checkout request will be submitted through the live application API.',
|
|
primaryActionText: '提交退房申请',
|
|
primaryActionTextEn: 'Submit Checkout Request',
|
|
remarkPlaceholder: '请输入退房原因、物品处理说明和其他补充内容',
|
|
remarkPlaceholderEn: 'Enter the checkout reason, belongings arrangement, and notes',
|
|
dateRangeLabel: '退房安排',
|
|
dateRangeLabelEn: 'Checkout Schedule',
|
|
dateRangeStartLabel: '计划退房',
|
|
dateRangeStartLabelEn: 'Planned Checkout',
|
|
dateRangeEndLabel: '最晚离场',
|
|
dateRangeEndLabelEn: 'Latest Departure',
|
|
dateRangeStartPlaceholder: '请选择退房日期',
|
|
dateRangeStartPlaceholderEn: 'Select checkout date',
|
|
dateRangeEndPlaceholder: '请选择离场日期',
|
|
dateRangeEndPlaceholderEn: 'Select departure date',
|
|
showRoomTypeSelector: false,
|
|
showPreferenceSelectors: false,
|
|
showDateRange: true,
|
|
buildDefaultRemark(payload = {}) {
|
|
const roomLabel = buildRoomLabel(payload)
|
|
const language = payload.language || 'zh'
|
|
if (language === 'en') {
|
|
return roomLabel
|
|
? `I am currently staying in "${roomLabel}" and would like to check out. Please help arrange the inspection and departure process.`
|
|
: 'I would like to check out. Please help arrange the inspection and departure process.'
|
|
}
|
|
return roomLabel
|
|
? `我当前居住房间为“${roomLabel}”,现申请退房,请协助安排后续验房和离场流程。`
|
|
: '我想申请退房,请协助安排后续验房和离场流程。'
|
|
}
|
|
},
|
|
repair: {
|
|
title: '设备报修',
|
|
titleEn: 'Repair Request',
|
|
titleEnShort: 'Repair',
|
|
desc: '请填写故障位置、问题描述和期望上门时间,提交后由管家安排处理。',
|
|
descEn: 'Fill in the issue location, problem details, and preferred visit time. Your butler will arrange service.',
|
|
formBlockTitle: '报修信息',
|
|
formBlockTitleEn: 'Repair Details',
|
|
formBlockDesc: '将通过真实申请接口提交报修需求。',
|
|
formBlockDescEn: 'Your repair request will be submitted through the live application API.',
|
|
primaryActionText: '提交报修申请',
|
|
primaryActionTextEn: 'Submit Repair Request',
|
|
remarkPlaceholder: '请输入故障位置、问题现象和期望处理时间',
|
|
remarkPlaceholderEn: 'Enter the issue location, symptoms, and preferred handling time',
|
|
dateRangeLabel: '上门时间',
|
|
dateRangeLabelEn: 'Visit Time',
|
|
dateRangeStartLabel: '期望上门',
|
|
dateRangeStartLabelEn: 'Preferred Visit',
|
|
dateRangeEndLabel: '可接受至',
|
|
dateRangeEndLabelEn: 'Available Until',
|
|
dateRangeStartPlaceholder: '请选择期望时间',
|
|
dateRangeStartPlaceholderEn: 'Select preferred time',
|
|
dateRangeEndPlaceholder: '请选择最晚时间',
|
|
dateRangeEndPlaceholderEn: 'Select latest time',
|
|
showRoomTypeSelector: false,
|
|
showPreferenceSelectors: false,
|
|
showDateRange: true,
|
|
buildDefaultRemark(payload = {}) {
|
|
const roomLabel = buildRoomLabel(payload)
|
|
const language = payload.language || 'zh'
|
|
if (language === 'en') {
|
|
return roomLabel
|
|
? `I am currently staying in "${roomLabel}" and need help with a device repair. Please arrange service support.`
|
|
: 'I need help with a device repair. Please arrange service support.'
|
|
}
|
|
return roomLabel
|
|
? `我当前居住房间为“${roomLabel}”,有设备需要报修,请协助安排人员处理。`
|
|
: '我有设备需要报修,请协助安排人员处理。'
|
|
}
|
|
},
|
|
laundry: {
|
|
title: '洗衣房预约',
|
|
titleEn: 'Laundry Room Booking',
|
|
titleEnShort: 'Laundry',
|
|
desc: '请填写预约时间和补充说明,提交后由管家确认场地安排。',
|
|
descEn: 'Fill in the booking time and notes. Your butler will confirm the venue arrangement.',
|
|
formBlockTitle: '预约信息',
|
|
formBlockTitleEn: 'Booking Details',
|
|
formBlockDesc: '将通过真实申请接口提交洗衣房预约需求。',
|
|
formBlockDescEn: 'Your laundry room booking will be submitted through the live application API.',
|
|
primaryActionText: '提交洗衣房预约',
|
|
primaryActionTextEn: 'Submit Laundry Booking',
|
|
remarkPlaceholder: '请输入预约需求、设备偏好和其他说明',
|
|
remarkPlaceholderEn: 'Enter the booking request, equipment preference, and notes',
|
|
dateRangeLabel: '预约时间',
|
|
dateRangeLabelEn: 'Booking Time',
|
|
dateRangeStartLabel: '预约开始',
|
|
dateRangeStartLabelEn: 'Start Time',
|
|
dateRangeEndLabel: '预约结束',
|
|
dateRangeEndLabelEn: 'End Time',
|
|
dateRangeStartPlaceholder: '请选择开始时间',
|
|
dateRangeStartPlaceholderEn: 'Select start time',
|
|
dateRangeEndPlaceholder: '请选择结束时间',
|
|
dateRangeEndPlaceholderEn: 'Select end time',
|
|
showRoomTypeSelector: false,
|
|
showPreferenceSelectors: false,
|
|
showDateRange: true,
|
|
buildDefaultRemark(payload = {}) {
|
|
if ((payload.language || 'zh') === 'en') {
|
|
return 'I would like to book the laundry room. Please help confirm available time slots and equipment arrangements.'
|
|
}
|
|
return '我想预约洗衣房,请协助确认可用时段和设备安排。'
|
|
}
|
|
},
|
|
gym: {
|
|
title: '健身房预约',
|
|
titleEn: 'Gym Booking',
|
|
titleEnShort: 'Gym',
|
|
desc: '请填写预约时间和补充说明,提交后由管家确认场地安排。',
|
|
descEn: 'Fill in the booking time and notes. Your butler will confirm the venue arrangement.',
|
|
formBlockTitle: '预约信息',
|
|
formBlockTitleEn: 'Booking Details',
|
|
formBlockDesc: '将通过真实申请接口提交健身房预约需求。',
|
|
formBlockDescEn: 'Your gym booking will be submitted through the live application API.',
|
|
primaryActionText: '提交健身房预约',
|
|
primaryActionTextEn: 'Submit Gym Booking',
|
|
remarkPlaceholder: '请输入预约需求、人数和其他说明',
|
|
remarkPlaceholderEn: 'Enter the booking request, attendee count, and notes',
|
|
dateRangeLabel: '预约时间',
|
|
dateRangeLabelEn: 'Booking Time',
|
|
dateRangeStartLabel: '预约开始',
|
|
dateRangeStartLabelEn: 'Start Time',
|
|
dateRangeEndLabel: '预约结束',
|
|
dateRangeEndLabelEn: 'End Time',
|
|
dateRangeStartPlaceholder: '请选择开始时间',
|
|
dateRangeStartPlaceholderEn: 'Select start time',
|
|
dateRangeEndPlaceholder: '请选择结束时间',
|
|
dateRangeEndPlaceholderEn: 'Select end time',
|
|
showRoomTypeSelector: false,
|
|
showPreferenceSelectors: false,
|
|
showDateRange: true,
|
|
buildDefaultRemark(payload = {}) {
|
|
if ((payload.language || 'zh') === 'en') {
|
|
return 'I would like to book the gym. Please help confirm available time slots.'
|
|
}
|
|
return '我想预约健身房,请协助确认可用时段。'
|
|
}
|
|
},
|
|
kitchen: {
|
|
title: '公共厨房预约',
|
|
titleEn: 'Shared Kitchen Booking',
|
|
titleEnShort: 'Kitchen',
|
|
desc: '请填写预约时间和补充说明,提交后由管家确认场地安排。',
|
|
descEn: 'Fill in the booking time and notes. Your butler will confirm the venue arrangement.',
|
|
formBlockTitle: '预约信息',
|
|
formBlockTitleEn: 'Booking Details',
|
|
formBlockDesc: '将通过真实申请接口提交公共厨房预约需求。',
|
|
formBlockDescEn: 'Your shared kitchen booking will be submitted through the live application API.',
|
|
primaryActionText: '提交公共厨房预约',
|
|
primaryActionTextEn: 'Submit Kitchen Booking',
|
|
remarkPlaceholder: '请输入预约需求、人数和其他说明',
|
|
remarkPlaceholderEn: 'Enter the booking request, attendee count, and notes',
|
|
dateRangeLabel: '预约时间',
|
|
dateRangeLabelEn: 'Booking Time',
|
|
dateRangeStartLabel: '预约开始',
|
|
dateRangeStartLabelEn: 'Start Time',
|
|
dateRangeEndLabel: '预约结束',
|
|
dateRangeEndLabelEn: 'End Time',
|
|
dateRangeStartPlaceholder: '请选择开始时间',
|
|
dateRangeStartPlaceholderEn: 'Select start time',
|
|
dateRangeEndPlaceholder: '请选择结束时间',
|
|
dateRangeEndPlaceholderEn: 'Select end time',
|
|
showRoomTypeSelector: false,
|
|
showPreferenceSelectors: false,
|
|
showDateRange: true,
|
|
buildDefaultRemark(payload = {}) {
|
|
if ((payload.language || 'zh') === 'en') {
|
|
return 'I would like to book the shared kitchen. Please help confirm available time slots.'
|
|
}
|
|
return '我想预约公共厨房,请协助确认可用时段。'
|
|
}
|
|
},
|
|
'butler-contact': {
|
|
title: '联系管家',
|
|
titleEn: 'Contact Butler',
|
|
titleEnShort: 'Butler',
|
|
desc: '请填写需要协助的事项和补充说明,提交后由管家跟进处理。',
|
|
descEn: 'Fill in the request details and notes. Your butler will follow up after submission.',
|
|
formBlockTitle: '沟通信息',
|
|
formBlockTitleEn: 'Request Details',
|
|
formBlockDesc: '将通过真实申请接口提交沟通需求。',
|
|
formBlockDescEn: 'Your request will be submitted through the live application API.',
|
|
primaryActionText: '提交联系事项',
|
|
primaryActionTextEn: 'Submit Request',
|
|
remarkPlaceholder: '请输入希望联系管家的事项内容',
|
|
remarkPlaceholderEn: 'Enter what you would like help from your butler with',
|
|
showRoomTypeSelector: false,
|
|
showPreferenceSelectors: false,
|
|
showDateRange: false,
|
|
buildDefaultRemark(payload = {}) {
|
|
if ((payload.language || 'zh') === 'en') {
|
|
return 'I need to contact my butler for an in-stay request. Please assist as soon as possible.'
|
|
}
|
|
return '我需要联系管家处理住中事项,请尽快协助。'
|
|
}
|
|
}
|
|
}
|
|
|
|
export default {
|
|
components: {
|
|
TagSelector,
|
|
DateRangeSelector,
|
|
RoomTypeSelector,
|
|
FloorSelector
|
|
},
|
|
data() {
|
|
return {
|
|
sourceTitleZh: '',
|
|
sourceTitleEn: '',
|
|
sourceDescZh: '',
|
|
sourceDescEn: '',
|
|
rawSourceTitle: '',
|
|
rawSourceDesc: '',
|
|
sourcePayload: {},
|
|
isSubmitting: false,
|
|
dictLoading: false,
|
|
hobbyOptions: [],
|
|
lifestyleOptions: [],
|
|
form: createDefaultForm(),
|
|
scrollTop: 0,
|
|
lastAutoRemark: ''
|
|
}
|
|
},
|
|
computed: {
|
|
language() {
|
|
return this.$getLanguage()
|
|
},
|
|
navBarBackgroundColor() {
|
|
if (this.scrollTop < 40) return 'rgba(255, 255, 255, 0)';
|
|
return 'rgba(255, 255, 255, 0.96)';
|
|
},
|
|
navBarBackStyle() {
|
|
const isDarkBg = this.scrollTop < 40;
|
|
return {
|
|
transition: 'all 0.2s ease',
|
|
background: isDarkBg ? 'rgba(0, 0, 0, 0.2)' : 'transparent',
|
|
borderRadius: '50%',
|
|
width: '64rpx',
|
|
height: '64rpx',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginLeft: '12rpx',
|
|
color: isDarkBg ? '#FFFFFF' : '#000000'
|
|
}
|
|
},
|
|
navTitleOpacity() {
|
|
return Math.min(Math.max((this.scrollTop - 40) / 50, 0), 1)
|
|
},
|
|
businessType() {
|
|
return this.sourcePayload && this.sourcePayload.businessType ? this.sourcePayload.businessType : ''
|
|
},
|
|
businessConfig() {
|
|
return BUSINESS_TYPE_CONFIG[this.businessType] || null
|
|
},
|
|
isRoomTypeApply() {
|
|
return this.sourcePayload && this.sourcePayload.sourceType === 'area-room-type'
|
|
},
|
|
pageTitle() {
|
|
if (this.isRoomTypeApply) {
|
|
return this.language === 'en'
|
|
? compactEnglishText(this.$tr('房型申请'), 22)
|
|
: this.$tr('房型申请')
|
|
}
|
|
return this.getBusinessText('titleShort')
|
|
|| this.getBusinessText('title')
|
|
|| this.getLocalizedSourceText('title')
|
|
|| this.$tr('入住申请')
|
|
},
|
|
pageDesc() {
|
|
if (this.isRoomTypeApply) {
|
|
return this.language === 'en'
|
|
? compactEnglishText(this.$tr('请确认房型、区域、偏好标签和租期信息后提交申请。'), 96)
|
|
: this.$tr('请确认房型、区域、偏好标签和租期信息后提交申请。')
|
|
}
|
|
const text = this.getBusinessText('desc') || this.getLocalizedSourceText('desc') || this.$tr('请填写您的入住意向、生活偏好与预计租期后提交申请单。')
|
|
return this.language === 'en' ? compactEnglishText(text, 96) : text
|
|
},
|
|
formBlockTitle() {
|
|
if (this.isRoomTypeApply) {
|
|
return this.$tr('房型申请信息')
|
|
}
|
|
return this.getBusinessText('formBlockTitle') || this.$tr('申请信息')
|
|
},
|
|
formBlockDesc() {
|
|
if (this.isRoomTypeApply) {
|
|
return this.$tr('已接入真实申请单接口,支持带出房型与区域编码后直接提交。')
|
|
}
|
|
return this.getBusinessText('formBlockDesc') || this.$tr('请填写您的入住意向、生活偏好与预计租期后提交申请单。')
|
|
},
|
|
remarkPlaceholder() {
|
|
if (this.isRoomTypeApply) {
|
|
return this.$tr('请输入您的申请补充说明')
|
|
}
|
|
return this.getBusinessText('remarkPlaceholder') || this.$tr('请输入补充说明')
|
|
},
|
|
primaryActionText() {
|
|
if (this.isRoomTypeApply) {
|
|
return this.language === 'en'
|
|
? compactEnglishText(this.$tr('提交房型申请'), 24)
|
|
: this.$tr('提交房型申请')
|
|
}
|
|
const text = this.getBusinessText('primaryActionText') || this.$tr('提交申请')
|
|
return this.language === 'en' ? compactEnglishText(text, 24) : text
|
|
},
|
|
roomTypeLabel() {
|
|
return this.getBusinessText('roomTypeLabel') || this.$tr('申请房型')
|
|
},
|
|
pageBottomPadding() {
|
|
return this.showFloatingSubmitBtn ? '220rpx' : '60rpx'
|
|
},
|
|
showFloatingSubmitBtn() {
|
|
return this.isRoomTypeApply
|
|
},
|
|
floorLabel() {
|
|
return this.getBusinessText('floorLabel') || this.$tr('楼层')
|
|
},
|
|
showRoomTypeSelector() {
|
|
if (this.isRoomTypeApply) {
|
|
return true
|
|
}
|
|
return this.businessConfig ? this.businessConfig.showRoomTypeSelector !== false : true
|
|
},
|
|
showFloorSelector() {
|
|
if (this.isRoomTypeApply) {
|
|
return true
|
|
}
|
|
return this.businessConfig ? this.businessConfig.showFloorSelector !== false : true
|
|
},
|
|
showPreferenceSelectors() {
|
|
if (this.isRoomTypeApply) {
|
|
return true
|
|
}
|
|
return this.businessConfig ? !!this.businessConfig.showPreferenceSelectors : true
|
|
},
|
|
showDateRange() {
|
|
if (this.isRoomTypeApply) {
|
|
return true
|
|
}
|
|
return this.businessConfig ? this.businessConfig.showDateRange !== false : true
|
|
},
|
|
dateRangeLabel() {
|
|
return this.getBusinessText('dateRangeLabel') || this.$tr('租期时间')
|
|
},
|
|
dateRangeStartLabel() {
|
|
return this.getBusinessText('dateRangeStartLabel') || this.$tr('预计入住')
|
|
},
|
|
dateRangeEndLabel() {
|
|
return this.getBusinessText('dateRangeEndLabel') || this.$tr('预计离开')
|
|
},
|
|
dateRangeStartPlaceholder() {
|
|
return this.getBusinessText('dateRangeStartPlaceholder') || this.$tr('请选择入住日期')
|
|
},
|
|
dateRangeEndPlaceholder() {
|
|
return this.getBusinessText('dateRangeEndPlaceholder') || this.$tr('请选择离开日期')
|
|
}
|
|
},
|
|
watch: {
|
|
language() {
|
|
this.syncLocalizedDraftContent()
|
|
this.loadDictOptions()
|
|
}
|
|
},
|
|
onLoad(options) {
|
|
this.sourceTitleZh = this.decodeRouteText(options && (options.titleZh || options.title), '')
|
|
this.sourceTitleEn = this.decodeRouteText(options && options.titleEn, '')
|
|
this.sourceDescZh = this.decodeRouteText(options && (options.descZh || options.desc), '')
|
|
this.sourceDescEn = this.decodeRouteText(options && options.descEn, '')
|
|
this.rawSourceTitle = this.decodeRouteText(options && options.title, '')
|
|
this.rawSourceDesc = this.decodeRouteText(options && options.desc, '')
|
|
if (options.payload) {
|
|
Promise.resolve(options.payload)
|
|
.then(decodeURIComponent)
|
|
.then(JSON.parse)
|
|
.then((payload) => {
|
|
this.sourcePayload = payload && typeof payload === 'object' ? payload : {}
|
|
})
|
|
.catch((error) => {
|
|
console.error('parse submit payload error', error)
|
|
this.sourcePayload = {}
|
|
})
|
|
.finally(() => {
|
|
this.initFormFromSource()
|
|
this.loadDictOptions()
|
|
})
|
|
return
|
|
}
|
|
this.initFormFromSource()
|
|
this.loadDictOptions()
|
|
},
|
|
methods: {
|
|
decodeRouteText(value, fallback = '') {
|
|
if (value === undefined || value === null || value === '') {
|
|
return fallback
|
|
}
|
|
try {
|
|
return decodeURIComponent(String(value))
|
|
} catch (error) {
|
|
return String(value)
|
|
}
|
|
},
|
|
getLocalizedSourceText(type) {
|
|
if (type === 'title') {
|
|
if (this.language === 'en') {
|
|
return this.sourceTitleEn || this.sourceTitleZh || this.rawSourceTitle || ''
|
|
}
|
|
return this.sourceTitleZh || this.sourceTitleEn || this.rawSourceTitle || ''
|
|
}
|
|
if (this.language === 'en') {
|
|
return this.sourceDescEn || this.sourceDescZh || this.rawSourceDesc || ''
|
|
}
|
|
return this.sourceDescZh || this.sourceDescEn || this.rawSourceDesc || ''
|
|
},
|
|
getBusinessText(field) {
|
|
if (!this.businessConfig) {
|
|
return ''
|
|
}
|
|
const fieldKey = this.language === 'en'
|
|
? `${field}En${field.endsWith('Short') ? '' : ''}`
|
|
: field
|
|
if (this.language === 'en' && field === 'titleShort') {
|
|
return this.businessConfig.titleEnShort || this.businessConfig.titleEn || this.businessConfig.title || ''
|
|
}
|
|
return this.businessConfig[fieldKey] || this.businessConfig[field] || ''
|
|
},
|
|
handleScroll(e) {
|
|
this.scrollTop = e.detail.scrollTop
|
|
},
|
|
buildDefaultRemarkFromSource() {
|
|
const roomTypeName = this.language === 'en'
|
|
? (this.sourcePayload.roomTypeNameEn || this.sourcePayload.roomTypeName || '')
|
|
: (this.sourcePayload.roomTypeName || '')
|
|
const roomName = this.language === 'en'
|
|
? (this.sourcePayload.roomNameEn || this.sourcePayload.roomName || '')
|
|
: (this.sourcePayload.roomName || '')
|
|
const bannerTitle = this.language === 'en'
|
|
? (this.sourcePayload.bannerTitleEn || this.sourcePayload.bannerTitle || '')
|
|
: (this.sourcePayload.bannerTitle || '')
|
|
|
|
if (roomTypeName) {
|
|
return this.language === 'en'
|
|
? `I plan to apply for the room type "${roomTypeName}". Please help confirm the application requirements, pricing, and next steps.`
|
|
: `我计划申请房型“${roomTypeName}”,请协助确认申请条件、价格和后续安排。`
|
|
}
|
|
|
|
if (roomName) {
|
|
return this.language === 'en'
|
|
? `I plan to apply for the room "${roomName}". Please help confirm the move-in process and review requirements.`
|
|
: `我计划申请房源“${roomName}”,请协助确认入住流程和审核要求。`
|
|
}
|
|
|
|
if (bannerTitle) {
|
|
return this.language === 'en'
|
|
? `I would like to submit a stay application related to "${bannerTitle}".`
|
|
: `我想提交“${bannerTitle}”相关房源的入住申请。`
|
|
}
|
|
|
|
if (this.businessConfig && typeof this.businessConfig.buildDefaultRemark === 'function') {
|
|
return this.businessConfig.buildDefaultRemark({
|
|
...this.sourcePayload,
|
|
language: this.language
|
|
})
|
|
}
|
|
|
|
return ''
|
|
},
|
|
syncLocalizedDraftContent() {
|
|
const nextAutoRemark = this.buildDefaultRemarkFromSource()
|
|
if (!this.form || typeof this.form !== 'object') {
|
|
this.lastAutoRemark = nextAutoRemark
|
|
return
|
|
}
|
|
const currentRemark = String(this.form.remark || '')
|
|
const shouldSyncRemark = !currentRemark || currentRemark === this.lastAutoRemark
|
|
if (shouldSyncRemark) {
|
|
this.form.remark = nextAutoRemark
|
|
}
|
|
this.lastAutoRemark = nextAutoRemark
|
|
},
|
|
initFormFromSource() {
|
|
const defaultForm = createDefaultForm()
|
|
this.form = {
|
|
...defaultForm,
|
|
intendedAreaCode: this.sourcePayload.areaCode || '',
|
|
intendedRoomTypeCode: this.sourcePayload.roomTypeCode || '',
|
|
intendedFloorLevel: this.sourcePayload.intendedFloorLevel || this.sourcePayload.IntendedFloorLevel || '',
|
|
remark: '',
|
|
start: this.sourcePayload.start || defaultForm.start,
|
|
end: this.sourcePayload.end || ''
|
|
}
|
|
|
|
// If we don't have a roomTypeCode but we have an areaCode,
|
|
// we still pass it down so RoomTypeSelector can select the current store/area
|
|
if (this.sourcePayload.areaCode && !this.sourcePayload.roomTypeCode) {
|
|
this.form.intendedAreaCode = this.sourcePayload.areaCode
|
|
}
|
|
this.form.remark = this.buildDefaultRemarkFromSource()
|
|
this.lastAutoRemark = this.form.remark
|
|
},
|
|
handleRoomTypeChange({ area, roomType }) {
|
|
if (area && area.code) {
|
|
this.form.intendedAreaCode = area.code
|
|
}
|
|
if (roomType) {
|
|
// 如果用户主动切换了房型,可以动态更新页面上的信息
|
|
if (!this.sourcePayload) this.sourcePayload = {}
|
|
|
|
this.sourcePayload.roomTypeCode = roomType.code
|
|
this.sourcePayload.roomTypeName = roomType.name
|
|
this.sourcePayload.roomTypeNameEn = roomType.nameEn || ''
|
|
this.sourcePayload.selectedRoomTypeBuildingCode = roomType.buildingCode || ''
|
|
this.sourcePayload.selectedRoomTypeBuildingSectionCode = roomType.buildingSectionCode || ''
|
|
this.sourcePayload.selectedRoomTypeBuildingName = roomType.buildingName || ''
|
|
this.sourcePayload.selectedRoomTypeBuildingSectionName = roomType.buildingSectionName || ''
|
|
|
|
if (area) {
|
|
this.sourcePayload.areaCode = area.code
|
|
this.sourcePayload.areaName = area.name
|
|
this.sourcePayload.areaNameEn = area.nameEn || ''
|
|
}
|
|
|
|
if (roomType.price) {
|
|
this.sourcePayload.price = roomType.price
|
|
}
|
|
}
|
|
},
|
|
handleFloorChange({ floor }) {
|
|
if (!this.sourcePayload) {
|
|
this.sourcePayload = {}
|
|
}
|
|
this.sourcePayload.intendedFloorLevel = floor && floor.value ? floor.value : ''
|
|
this.sourcePayload.intendedFloorName = floor
|
|
? (this.language === 'en' ? (floor.nameEn || floor.name || '') : (floor.name || floor.nameEn || ''))
|
|
: ''
|
|
},
|
|
loadDictOptions() {
|
|
this.dictLoading = true
|
|
return Promise.all([
|
|
sysdictPage({ Type: 'UserHobbiesAndInterestsTag', PageIndex: 1, PageSize: 100 }, this.language),
|
|
sysdictPage({ Type: 'UserLifestyleHabitsTag', PageIndex: 1, PageSize: 100 }, this.language)
|
|
])
|
|
.then(([hobbyRes, lifestyleRes]) => {
|
|
this.hobbyOptions = normalizeDictList(hobbyRes, this.language, 'hobbies')
|
|
this.lifestyleOptions = normalizeDictList(lifestyleRes, this.language, 'lifestyle')
|
|
this.initFormTagsFromUser()
|
|
})
|
|
.catch((error) => {
|
|
console.error('load application dict options failed', error)
|
|
uni.showToast({
|
|
title: this.$tr('偏好标签加载失败'),
|
|
icon: 'none'
|
|
})
|
|
})
|
|
.finally(() => {
|
|
this.dictLoading = false
|
|
})
|
|
},
|
|
initFormTagsFromUser() {
|
|
const user = getStoredUser()
|
|
const authProfile = user && user.authProfile ? user.authProfile : {}
|
|
|
|
if (!this.form.hobbiesAndInterestsTagKeys.length) {
|
|
this.form.hobbiesAndInterestsTagKeys = this.matchTagKeys(authProfile.hobbyTags, this.hobbyOptions)
|
|
}
|
|
|
|
if (!this.form.lifestyleHabitsTagKeys.length) {
|
|
this.form.lifestyleHabitsTagKeys = this.matchTagKeys(authProfile.dietaryHabitTags, this.lifestyleOptions)
|
|
}
|
|
},
|
|
matchTagKeys(tagList = [], options = []) {
|
|
if (!Array.isArray(tagList) || !Array.isArray(options) || !options.length) {
|
|
return []
|
|
}
|
|
|
|
return tagList
|
|
.map(item => {
|
|
const rawKey = item && (item.key || item.code || item.value || '')
|
|
const matched = options.find(option => {
|
|
return String(option.key) === String(rawKey) || option.value === rawKey
|
|
})
|
|
return matched ? matched.key : ''
|
|
})
|
|
.filter(Boolean)
|
|
},
|
|
validateForm() {
|
|
if (this.showDateRange) {
|
|
if (!this.form.start) {
|
|
return this.$tr('请选择{label}', { label: this.dateRangeStartLabel })
|
|
}
|
|
if (!this.form.end) {
|
|
return this.$tr('请选择{label}', { label: this.dateRangeEndLabel })
|
|
}
|
|
if (new Date(this.form.end).getTime() < new Date(this.form.start).getTime()) {
|
|
return this.$tr('{endLabel}不能早于{startLabel}', {
|
|
endLabel: this.dateRangeEndLabel,
|
|
startLabel: this.dateRangeStartLabel
|
|
})
|
|
}
|
|
}
|
|
if (!this.form.remark.trim()) {
|
|
return this.$tr('请输入补充说明')
|
|
}
|
|
return ''
|
|
},
|
|
buildSubmitPayload() {
|
|
return {
|
|
intendedAreaCode: this.form.intendedAreaCode || null,
|
|
intendedRoomTypeCode: this.form.intendedRoomTypeCode || null,
|
|
IntendedFloorLevel: this.form.intendedFloorLevel || null,
|
|
roomSlotHeight: Number(this.form.roomSlotHeight),
|
|
hobbiesAndInterestsTagKeys: this.showPreferenceSelectors && this.form.hobbiesAndInterestsTagKeys.length ? this.form.hobbiesAndInterestsTagKeys : null,
|
|
lifestyleHabitsTagKeys: this.showPreferenceSelectors && this.form.lifestyleHabitsTagKeys.length ? this.form.lifestyleHabitsTagKeys : null,
|
|
remark: this.form.remark.trim() || null,
|
|
start: this.showDateRange ? normalizeDateTime(this.form.start, false) : null,
|
|
end: this.showDateRange ? normalizeDateTime(this.form.end, true) : null
|
|
}
|
|
},
|
|
submitRequest() {
|
|
if (this.isSubmitting) {
|
|
return
|
|
}
|
|
|
|
const errorMessage = this.validateForm()
|
|
if (errorMessage) {
|
|
uni.showToast({
|
|
title: errorMessage,
|
|
icon: 'none'
|
|
})
|
|
return
|
|
}
|
|
|
|
this.isSubmitting = true
|
|
uni.showLoading({
|
|
title: this.$tr('提交中...')
|
|
})
|
|
|
|
const payload = this.buildSubmitPayload()
|
|
return submitApplicationForm(payload)
|
|
.then((response) => {
|
|
const applicationCode = extractSubmitApplicationCode(response)
|
|
if (applicationCode) {
|
|
saveActiveApplicationCode(applicationCode)
|
|
}
|
|
uni.hideLoading()
|
|
uni.showToast({
|
|
title: this.$tr('申请单提交成功'),
|
|
icon: 'success'
|
|
})
|
|
|
|
setTimeout(() => {
|
|
saveHomePageTabId('my-application')
|
|
uni.reLaunch({
|
|
url: '/pages/index/index?pageId=my-application'
|
|
})
|
|
}, 800)
|
|
})
|
|
.catch((error) => {
|
|
console.error('submit application form failed', error)
|
|
uni.hideLoading()
|
|
uni.showToast({
|
|
title: (error && (error.message || error.msg)) || this.$tr('申请单提交失败'),
|
|
icon: 'none'
|
|
})
|
|
})
|
|
.finally(() => {
|
|
this.isSubmitting = false
|
|
})
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.submit-request-page {
|
|
min-height: 100vh;
|
|
background: linear-gradient(180deg, #F0F6FF 0%, #F8FAFF 100%);
|
|
position: relative;
|
|
}
|
|
|
|
.page-scroll {
|
|
width: 100%;
|
|
height: 100vh;
|
|
}
|
|
|
|
.page-content {
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* 毛玻璃导航栏覆盖样式 */
|
|
.glass-nav-bar ::v-deep .tn-nav-bar {
|
|
backdrop-filter: blur(20rpx);
|
|
-webkit-backdrop-filter: blur(20rpx);
|
|
}
|
|
|
|
.form-card {
|
|
background: #FFFFFF;
|
|
border-radius: 24rpx;
|
|
padding: 36rpx;
|
|
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
|
|
border: 1px solid rgba($brand-primary, 0.05);
|
|
}
|
|
|
|
.hero-header {
|
|
padding: 20rpx 10rpx;
|
|
}
|
|
|
|
.hero-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 8rpx 20rpx 8rpx 14rpx;
|
|
background: rgba($brand-primary, 0.08);
|
|
border-radius: 100rpx;
|
|
margin-bottom: 24rpx;
|
|
}
|
|
|
|
.badge-dot {
|
|
width: 10rpx;
|
|
height: 10rpx;
|
|
border-radius: 50%;
|
|
background: $brand-primary;
|
|
margin-right: 12rpx;
|
|
box-shadow: 0 0 8rpx rgba($brand-primary, 0.4);
|
|
}
|
|
|
|
.hero-badge text:not(.badge-dot) {
|
|
font-size: 20rpx;
|
|
color: $brand-primary;
|
|
font-weight: 700;
|
|
letter-spacing: 2rpx;
|
|
}
|
|
|
|
.hero-title {
|
|
font-size: 44rpx;
|
|
font-weight: 700;
|
|
color: #1A2233;
|
|
letter-spacing: 2rpx;
|
|
margin-bottom: 16rpx;
|
|
line-height: 1.3;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.block-title {
|
|
font-size: 32rpx;
|
|
font-weight: 700;
|
|
color: #1A2233;
|
|
letter-spacing: 1rpx;
|
|
}
|
|
|
|
.block-subtitle {
|
|
margin-top: 12rpx;
|
|
color: #6A7A99;
|
|
font-size: 24rpx;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.hero-desc,
|
|
.form-label,
|
|
.empty-text {
|
|
color: #6A7A99;
|
|
}
|
|
|
|
.hero-desc {
|
|
font-size: 28rpx;
|
|
line-height: 1.6;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 3;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.form-label {
|
|
font-size: 26rpx;
|
|
font-weight: 600;
|
|
margin-bottom: 20rpx;
|
|
color: #1A2233;
|
|
}
|
|
|
|
.form-input,
|
|
.form-textarea {
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
background: #FFFFFF;
|
|
border-radius: 20rpx;
|
|
padding: 26rpx 32rpx;
|
|
border: 1rpx solid rgba($brand-primary, 0.15);
|
|
font-size: 28rpx;
|
|
color: #1A2233;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
|
|
}
|
|
|
|
.form-textarea:focus {
|
|
border: 1rpx solid $brand-primary;
|
|
background: #FFFFFF;
|
|
box-shadow: 0 0 0 6rpx rgba($brand-primary, 0.1);
|
|
}
|
|
|
|
.form-textarea {
|
|
height: 200rpx;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.submit-btn {
|
|
width: 100%;
|
|
height: 96rpx;
|
|
border-radius: 999rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: linear-gradient(135deg, rgba($brand-primary, 0.9) 0%, $brand-primary 100%);
|
|
color: #FFFFFF;
|
|
box-shadow: 0 12rpx 32rpx rgba($brand-primary, 0.25);
|
|
font-size: 30rpx;
|
|
letter-spacing: 2rpx;
|
|
}
|
|
|
|
.btn-primary:active {
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
.submit-btn--disabled {
|
|
opacity: 0.6;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.floating-submit-bar {
|
|
position: fixed;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
z-index: 30;
|
|
padding: 20rpx 24rpx calc(20rpx + env(safe-area-inset-bottom));
|
|
background: linear-gradient(180deg, rgba(248, 250, 255, 0) 0%, rgba(248, 250, 255, 0.92) 24%, #F8FAFF 100%);
|
|
backdrop-filter: blur(16rpx);
|
|
-webkit-backdrop-filter: blur(16rpx);
|
|
}
|
|
|
|
.floating-submit-bar__inner {
|
|
max-width: 100%;
|
|
}
|
|
|
|
.source-row {
|
|
padding: 24rpx 0;
|
|
border-bottom: 1px dashed rgba($brand-primary, 0.15);
|
|
}
|
|
|
|
.source-row:last-child {
|
|
border-bottom: none;
|
|
padding-bottom: 8rpx;
|
|
}
|
|
|
|
.source-row--interactive:active {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.source-row__label {
|
|
color: #6A7A99;
|
|
font-size: 26rpx;
|
|
}
|
|
|
|
.source-row__value {
|
|
color: #1A2233;
|
|
font-size: 26rpx;
|
|
font-weight: 500;
|
|
text-align: right;
|
|
flex: 1;
|
|
min-width: 0;
|
|
margin-left: 24rpx;
|
|
word-break: break-word;
|
|
}
|
|
|
|
</style>
|