文件
fenleixinxi/uniapp/pages/post/create.vue

2144 行
52 KiB
Vue

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<template>
<view class="create fc-page">
<!-- 统一顶部深绿质感导航栏,右侧提供独立草稿箱入口 -->
<fc-navbar title="发布信息" theme="gradient" :show-back="true">
<template #right>
<view class="create__draft-entry" @click="goDraftsPage">
<image class="create__draft-icon" :src="iconUrl('mine-draft')" mode="aspectFit" />
<text class="create__draft-text">草稿箱</text>
<text v-if="draftCount > 0" class="create__draft-badge">{{ draftCount }}</text>
</view>
</template>
</fc-navbar>
<!-- 1. 分类选择单元格(唯一入口,点击唤起全部分类抽屉,避免同一分类重复展示) -->
<view class="fc-card create__section">
<view class="create__cell" @click="openCatDrawer">
<view class="create__cell-left">
<text class="create__label">所属分类</text>
<text class="create__required">*</text>
</view>
<view class="create__cell-right">
<view v-if="currentCat" class="create__selected-cat">
<image class="create__selected-cat-icon" :src="catIcon(currentCat)" mode="aspectFit" />
<text class="create__selected-cat-name">{{ currentCat.name }}</text>
</view>
<text v-else class="create__cell-ph">请选择信息所属分类</text>
<text class="create__arrow">›</text>
</view>
</view>
</view>
<!-- 2. 发布身份选择(个人 / 商家二选一,未认证商家同样展示并支持跳转认证) -->
<view class="fc-card create__section">
<view class="create__label-row">
<text class="create__label">发布身份</text>
<text v-if="!isMerchantUser" class="create__label-tip">认证商家享专属标识与引流</text>
</view>
<view class="create__identity-group">
<view
class="create__identity-item"
:class="{ 'create__identity-item--active': form.identityType === 'person' }"
@click="switchIdentity('person')"
>
<text class="create__identity-title">个人</text>
<text class="create__identity-desc">发布便民闲置信息</text>
</view>
<view
class="create__identity-item"
:class="{
'create__identity-item--active': form.identityType === 'merchant',
'create__identity-item--unauth': !isMerchantUser
}"
@click="switchIdentity('merchant')"
>
<view class="create__identity-title-row">
<text class="create__identity-title">商家</text>
<text class="create__merchant-badge" :class="'create__merchant-badge--' + merchantBadgeClass">
{{ merchantBadgeText }}
</text>
</view>
<text class="create__identity-desc fc-ellipsis">{{ merchantDescText }}</text>
</view>
</view>
</view>
<!-- 3. 基本信息表单(标题、详细内容、媒体上传) -->
<view class="fc-card create__section">
<view class="create__label">标题<text class="create__required">*</text></view>
<input
v-model="form.title"
class="create__input"
placeholder="一句话清晰概括(如:诚聘配电工 待遇从优 / 急售九成新电动车)"
maxlength="40"
placeholder-class="create__ph"
@input="onFieldInput"
/>
<view class="create__label create__label--mt">详细内容<text class="create__required">*</text></view>
<textarea
v-model="form.content"
class="create__textarea"
placeholder="补充价格、服务时间、详细要求等,信息越详细真实越容易成交..."
maxlength="500"
placeholder-class="create__ph"
@input="onFieldInput"
/>
<text class="create__count">{{ (form.content || '').length }}/500</text>
<view class="create__label create__label--mt">图片 / 视频(实拍更具可信度)</view>
<view class="create__media">
<view v-for="(m, i) in form.media" :key="m + i" class="create__media-item fc-thumb">
<image v-if="!isVideo(m)" class="create__media-img" :src="assetUrl(m)" mode="aspectFill" />
<view v-else class="create__media-video">
<image class="fc-icon fc-icon--md" :src="iconUrl('plus-ink')" mode="aspectFit" />
<text class="create__media-video-txt">视频</text>
</view>
<view class="create__media-del" @click.stop="removeMedia(i)">
<text>×</text>
</view>
</view>
<!-- 上传图片按钮 -->
<view v-if="!uploading && imageCount < 6" class="create__upload" @click="chooseImage">
<image class="fc-icon fc-icon--md" :src="iconUrl('image')" mode="aspectFit" />
<text class="create__upload-txt">图片 {{ imageCount }}/6</text>
</view>
<!-- 上传视频按钮 -->
<view v-if="!uploading && videoCount < 1" class="create__upload" @click="chooseVideo">
<image class="fc-icon fc-icon--md" :src="iconUrl('plus-ink')" mode="aspectFit" />
<text class="create__upload-txt">短视频 {{ videoCount }}/1</text>
</view>
</view>
<text v-if="uploading" class="create__uploading">媒体上传中,请稍候…</text>
</view>
<!-- 4. 价格与特色标签(按分类推荐单位与亮点) -->
<view class="fc-card create__section">
<view class="create__label-row">
<text class="create__label">期望价格 / 薪资</text>
<view class="create__negotiable" @click="toggleNegotiable">
<view class="create__checkbox" :class="{ 'create__checkbox--checked': form.isNegotiable }">
<text v-if="form.isNegotiable" class="create__checkbox-tick">✓</text>
</view>
<text class="create__negotiable-label">面议 / 无标价</text>
</view>
</view>
<!-- 标价输入 -->
<view v-if="!form.isNegotiable" class="create__price-box">
<view class="create__price-input-row">
<text class="create__price-symbol">¥</text>
<input
v-model="form.price"
class="create__price-input"
type="digit"
placeholder="0.00"
placeholder-class="create__ph"
@input="onFieldInput"
/>
<text class="create__price-unit-text">{{ form.priceUnit || '元' }}</text>
</view>
<!-- 计价单位快捷选择胶囊 -->
<view class="create__unit-chips">
<view
v-for="u in currentUnits"
:key="u"
class="create__unit-chip"
:class="{ 'create__unit-chip--active': form.priceUnit === u }"
@click="selectPriceUnit(u)"
>
{{ u }}
</view>
</view>
</view>
<!-- 特色亮点标签(最多5个) -->
<view class="create__label create__label--mt">特色亮点(最多5个)</view>
<!-- 已选标签胶囊 -->
<view v-if="form.tags && form.tags.length" class="create__tags-wrapper">
<view
v-for="(t, i) in form.tags"
:key="'tag-' + t + i"
class="create__tag-badge"
@click="removeTag(i)"
>
<text class="create__tag-badge-text">{{ t }}</text>
<text class="create__tag-badge-del">×</text>
</view>
</view>
<!-- 推荐标签胶囊 -->
<view v-if="recommendTags.length > 0" class="create__recommend-tags">
<text class="create__recommend-title">推荐亮点:</text>
<view class="create__recommend-chips">
<view
v-for="rt in recommendTags"
:key="rt"
class="create__recommend-chip"
:class="{ 'create__recommend-chip--disabled': form.tags.includes(rt) }"
@click="addRecommendTag(rt)"
>
+ {{ rt }}
</view>
</view>
</view>
<!-- 手动添加自定义标签 -->
<view v-if="form.tags.length < 5" class="create__tag-custom-row">
<input
v-model="customTagInput"
class="create__tag-custom-input"
placeholder="自定义亮点(如:随时看 / 包过户)"
maxlength="8"
placeholder-class="create__ph"
confirm-type="done"
@confirm="addCustomTag"
/>
<view class="create__tag-custom-btn" @click="addCustomTag">
<text class="create__tag-custom-btn-txt">添加</text>
</view>
</view>
</view>
<!-- 5. 联系方式与地址定位 -->
<view class="fc-card create__section">
<view class="create__row">
<text class="create__row-label">联系人</text>
<input
v-model="form.contact"
class="create__input create__input--row"
placeholder="请输入您的称呼(如:李先生)"
placeholder-class="create__ph"
@input="onFieldInput"
/>
</view>
<view class="create__row create__row--line">
<text class="create__row-label">联系电话</text>
<input
v-model="form.phone"
class="create__input create__input--row"
type="number"
maxlength="11"
placeholder="请输入11位联系手机号"
placeholder-class="create__ph"
@input="onFieldInput"
/>
</view>
<view class="create__row create__row--line">
<text class="create__row-label">微信号</text>
<input
v-model="form.wechat"
class="create__input create__input--row"
placeholder="选填,方便意向买家直接微信联系"
placeholder-class="create__ph"
maxlength="40"
@input="onFieldInput"
/>
</view>
<view class="create__row create__row--line" @click="chooseLocation">
<text class="create__row-label">所在位置</text>
<view class="create__location">
<image class="fc-icon fc-icon--sm" :src="iconUrl('location')" mode="aspectFit" />
<text class="create__location-text fc-ellipsis">{{ form.address || '点击定位/选择商铺' }}</text>
<text class="create__location-arrow">›</text>
</view>
</view>
<view class="create__row create__row--line">
<text class="create__row-label">所属区域</text>
<fc-area-picker
class="create__area-picker"
:areas="areaRows"
:value="form.areaId"
variant="plain"
show-full
placeholder="请选择省 / 市 / 区"
@change="onRegionPicked"
/>
</view>
</view>
<!-- 6. 发布方式与计费(由后台「发布与费用配置」下发) -->
<view class="fc-card create__section">
<view class="create__label-row">
<text class="create__label">发布方式</text>
<text v-if="publish.mode === 'paid'" class="create__quota">
套餐额度:剩余 {{ publish.quotaLeft }} 条
</text>
</view>
<view v-if="publish.mode === 'free'" class="create__free-tip">
<text>当前处于限时免费模式,所有用户均可直接免费发布!</text>
</view>
<view v-else class="create__plans">
<view
v-for="p in plans"
:key="p.key"
class="create__plan"
:class="{ 'create__plan--active': form.plan === p.key }"
@click="selectPlan(p)"
>
<text class="create__plan-name">{{ p.name }}</text>
<text class="create__plan-price">{{ p.price }}</text>
<text class="create__plan-desc">{{ p.desc }}</text>
<text v-if="p.tag" class="create__plan-tag">{{ p.tag }}</text>
</view>
</view>
<!-- 支付方式选择(微信 / 余额) -->
<view v-if="needPay" class="create__pay">
<text class="create__pay-label">支付方式</text>
<view class="create__pay-types">
<view
class="create__pay-type"
:class="{ 'create__pay-type--active': form.payType === 'wxpay' }"
@click="form.payType = 'wxpay'"
>
<image class="fc-icon fc-icon--sm" :src="iconUrl('paywechat')" mode="aspectFit" />
<text class="create__pay-text">微信支付</text>
</view>
<view
class="create__pay-type"
:class="{ 'create__pay-type--active': form.payType === 'balance' }"
@click="form.payType = 'balance'"
>
<image class="fc-icon fc-icon--sm" :src="iconUrl('wallet')" mode="aspectFit" />
<text class="create__pay-text">余额支付(¥{{ balance }})</text>
</view>
</view>
</view>
</view>
<!-- 底部固定主控操作栏 -->
<view class="create__bar">
<view class="create__bar-draft" @click="saveDraftManual">
<image class="create__bar-draft-icon" :src="iconUrl('mine-draft')" mode="aspectFit" />
<text class="create__bar-draft-txt">存草稿</text>
</view>
<view
class="create__bar-submit"
:class="{ 'create__bar-submit--disabled': submitting || uploading }"
@click="submit"
>
<text class="create__bar-submit-txt">
{{ submitting ? '提交中…' : (needPay ? '确认支付并发布' : '立即公开发布') }}
</text>
</view>
</view>
<view class="fc-safe-bottom"></view>
<!-- ================= 全部分类弹窗选择器抽屉(海量分类由接口返回决定,支持流畅自适应滚动) ================= -->
<view
v-if="catDrawerVisible"
class="cat-drawer-mask"
:class="{ 'cat-drawer-mask--visible': catDrawerAnim }"
@click="closeCatDrawer"
>
<view
class="cat-drawer"
:class="{ 'cat-drawer--visible': catDrawerAnim }"
@click.stop
>
<view class="cat-drawer__header">
<text class="cat-drawer__title">选择信息分类</text>
<view class="cat-drawer__close" @click="closeCatDrawer">
<text class="cat-drawer__close-txt">✕</text>
</view>
</view>
<scroll-view class="cat-drawer__body" scroll-y :show-scrollbar="false">
<view class="cat-drawer__grid">
<view
v-for="c in categories"
:key="c.id"
class="cat-drawer__item"
:class="{ 'cat-drawer__item--active': Number(form.categoryId) === Number(c.id) }"
@click="onSelectDrawerCat(c)"
>
<view class="cat-drawer__icon-box">
<image class="cat-drawer__icon" :src="catIcon(c)" mode="aspectFit" />
</view>
<text class="cat-drawer__name">{{ c.name }}</text>
<view v-if="Number(form.categoryId) === Number(c.id)" class="cat-drawer__check">✓</view>
</view>
</view>
<view class="cat-drawer__space"></view>
</scroll-view>
</view>
</view>
</view>
</template>
<script>
/**
* 信息发布页面(重构版)
* 核心优化:
* 1. 分类选择由接口动态返回决定,统一收敛为「所属分类单元格 + 全部分类抽屉」单一入口,
* 避免同一分类在页面内重复出现(已选值只展示一次);
* 2. 彻底剥离草稿箱列表到独立页面 /pages/post/drafts,发布页右上方提供直通入口与存草稿闭环;
* 3. 支持个人 / 商家身份二选一(已认证商家可切换为商家身份发布并附带门店信息);
* 4. 严格遵循春绿规范,大厂级布局与无障碍高对比度。
*/
import FcNavbar from '@/components/fc-navbar/fc-navbar.vue'
import FcAreaPicker from '@/components/fc-area-picker/fc-area-picker.vue'
import homeApi from '@/common/api/home.js'
import postApi from '@/common/api/post.js'
import { useUserStore } from '@/common/store/index.js'
import { iconUrl, categoryIconUrl, loadUiConfig, requestSubscribe } from '@/common/store/ui.js'
import { pay } from '@/common/utils/pay.js'
import { assetUrl } from '@/common/utils/format.js'
import config from '@/common/config/index.js'
export default {
components: { FcNavbar, FcAreaPicker },
data() {
return {
categories: [],
draftCount: 0,
currentDraftId: 0, // 当前正在编辑的草稿ID(编辑自某草稿)
// 平铺服务区域(每条自带省/市/区县),交给级联选择器分组
areaRows: [],
submitting: false,
uploading: false,
draftTimer: null,
catDrawerVisible: false,
catDrawerAnim: false,
customTagInput: '',
form: {
categoryId: 0,
identityType: 'person', // 'person' | 'merchant'
title: '',
content: '',
media: [],
price: '',
priceUnit: '元',
isNegotiable: false,
wechat: '',
tags: [],
contact: '',
phone: '',
address: '',
areaId: 0,
area: '',
lat: null,
lng: null,
plan: 'free',
payType: config.defaultPayType || 'wxpay',
},
publish: {
mode: 'paid',
singlePrice: 5,
discountRate: 1,
discountText: '',
monthPrice: 19.9,
monthQuota: 30,
yearPrice: 168,
yearQuota: 500,
quotaLeft: 0,
freeQuotaLeft: 0,
freeQuotaCycle: 'daily',
freeUnlimited: false,
},
}
},
computed: {
/**
* 当前用户是否已认证为商家
* @returns {boolean}
*/
isMerchantUser() {
try {
const u = useUserStore().state.userInfo
return !!(u && (u.isMerchant || Number(u.merchantStatus) === 1))
} catch (e) {
return false
}
},
/**
* 商家资质审核状态
* @returns {number} -1未申请, 0审核中, 1已通过, 2未通过
*/
merchantAuditStatus() {
try {
const u = useUserStore().state.userInfo
if (u && u.merchantStatus != null) {
return Number(u.merchantStatus)
}
if (u && u.isMerchant) return 1
return -1
} catch (e) {
return -1
}
},
/**
* 商家店铺名称
* @returns {string}
*/
merchantShopName() {
try {
const u = useUserStore().state.userInfo
return (u && u.merchant && (u.merchant.shop_name || u.merchant.name)) || ''
} catch (e) {
return ''
}
},
/**
* 商家身份徽章展示文案
* @returns {string}
*/
merchantBadgeText() {
if (this.isMerchantUser) return '已认证'
const s = this.merchantAuditStatus
if (s === 0) return '审核中'
if (s === 2) return '未通过'
return '去认证'
},
/**
* 商家身份徽章样式类名
* @returns {'approved'|'pending'|'rejected'|'unauth'}
*/
merchantBadgeClass() {
if (this.isMerchantUser) return 'approved'
const s = this.merchantAuditStatus
if (s === 0) return 'pending'
if (s === 2) return 'rejected'
return 'unauth'
},
/**
* 商家身份卡片副描述文案
* @returns {string}
*/
merchantDescText() {
if (this.isMerchantUser) {
return this.merchantShopName || '展示店铺专属信息'
}
const s = this.merchantAuditStatus
if (s === 0) return '资质审核中,点击查看'
if (s === 2) return '资质未通过,点击修改'
return '未认证,点击立即前往认证'
},
/**
* 当前选中的分类对象
* @returns {Object|null}
*/
currentCat() {
return this.categories.find((c) => Number(c.id) === Number(this.form.categoryId)) || null
},
balance() {
try {
return useUserStore().state.userInfo?.balance ?? 0
} catch (e) {
return 0
}
},
imageCount() {
return this.form.media.filter((m) => !this.isVideo(m)).length
},
videoCount() {
return this.form.media.filter((m) => this.isVideo(m)).length
},
discountRate() {
const r = Number(this.publish.discountRate)
return r > 0 && r <= 1 ? r : 1
},
singlePriceFinal() {
return Math.round(Number(this.publish.singlePrice || 0) * this.discountRate * 100) / 100
},
discountLabel() {
return this.discountRate < 1 ? this.publish.discountText || `${Math.round(this.discountRate * 100) / 10} 折` : ''
},
plans() {
const p = this.publish
if (p.mode === 'free') return []
const list = []
const freeLeft = Number(p.freeQuotaLeft)
const freeUnlimited = p.freeUnlimited === true || freeLeft === -1
if (p.quotaLeft > 0) {
list.push({ key: 'quota', name: '套餐额度', price: '免费', desc: `剩余 ${p.quotaLeft} 条`, tag: '可用' })
} else if (freeUnlimited || freeLeft > 0) {
const cycle = { daily: '今日', monthly: '本月', total: '累计' }[p.freeQuotaCycle] || '本期'
list.push({
key: 'quota',
name: `${cycle}免费`,
price: '免费',
desc: freeUnlimited ? '不限条数' : `${cycle}剩余 ${freeLeft} 条`,
tag: '可用',
})
}
list.push({
key: 'single',
name: '会员单条',
price: `¥${this.money(this.singlePriceFinal)}`,
desc: this.discountLabel ? `${this.discountLabel}(原价 ¥${this.money(p.singlePrice)})` : '发布 1 条',
tag: this.discountLabel ? '会员价' : '',
})
list.push({ key: 'month', name: '按月发布', price: `¥${this.money(p.monthPrice)}`, desc: `含 ${p.monthQuota} 条`, tag: '推荐' })
list.push({ key: 'year', name: '按年发布', price: `¥${this.money(p.yearPrice)}`, desc: `含 ${p.yearQuota} 条`, tag: '更划算' })
return list
},
needPay() {
return this.publish.mode === 'paid' && this.form.plan !== 'quota'
},
/**
* 当前分类对应的常用计价单位
*/
currentUnits() {
const catId = Number(this.form.categoryId || 0)
if (catId === 1) return ['元/月', '元/天', '元/小时', '元/次']
if (catId === 2) return ['元/月', '万元', '元/天', '元/季']
if (catId === 3) return ['元']
if (catId === 4) return ['万元', '元']
if (catId === 5) return ['元起', '元/次', '元/小时', '元/天']
if (catId === 6) return ['元/课时', '元', '元/月']
if (catId === 7) return ['元/次', '元/小时', '元/㎡', '元/月']
return ['元', '元/月', '元/次', '万元']
},
/**
* 当前分类对应的常用同城亮点标签
*/
recommendTags() {
const catId = Number(this.form.categoryId || 0)
const map = {
1: ['日结', '包吃住', '双休', '五险一金', '经验不限', '就近分配'],
2: ['精装修', '近地铁', '随时看房', '押一付一', '家电齐全', '独立卫浴'],
3: ['9成新', '支持验机', '同城自提', '急出可刀', '配件齐全', '保修期内'],
4: ['一手车', '无事故', '准新车', '支持检测', '省油好开', '带牌转让'],
5: ['上门服务', '明码标价', '十年经验', '专业高效', '质保售后', '免费检测'],
6: ['体验课', '一对一', '名师授课', '小班教学', '零基础', '包学会'],
7: ['自带工具', '持健康证', '深度保洁', '满意付款', '实名认证'],
8: ['运动搭子', '周末活动', 'AA制', '新手友好', '同城聚会']
}
return map[catId] || ['高品质', '值得信赖', '同城便民', '价格透明']
},
},
onLoad(query) {
if (query && query.categoryId) this.form.categoryId = Number(query.categoryId)
if (query && query.title) this.form.title = decodeURIComponent(query.title)
if (query && query.draftId) {
this.loadDraftPost(Number(query.draftId))
} else if (query && query.editId) {
this.loadEditPost(Number(query.editId))
}
loadUiConfig()
this.init()
},
/**
* 页面每次显示时触发:刷新草稿数并静默同步最新用户资料(如认证状态)
*/
async onShow() {
this.updateDraftCount()
try {
const userStore = useUserStore()
if (userStore.isLogin.value) {
await userStore.fetchProfile()
}
} catch (e) {}
},
methods: {
iconUrl,
assetUrl,
catIcon(c) {
if (!c) return ''
return (c.iconKey && iconUrl(c.iconKey)) || categoryIconUrl(c.id)
},
/**
* 页面基础数据初始化
*/
async init() {
try {
const cats = await homeApi.categories()
this.categories = Array.isArray(cats) ? cats : []
// 若未指定分类且已有分类列表,默认选中第一个
if (!this.form.categoryId && this.categories.length) {
this.form.categoryId = Number(this.categories[0].id)
} else if (this.form.categoryId) {
this.form.categoryId = Number(this.form.categoryId)
}
// 联动初始化当前分类的首选计价单位
if (this.currentUnits && this.currentUnits.length && (!this.form.priceUnit || this.form.priceUnit === '元')) {
this.form.priceUnit = this.currentUnits[0]
}
} catch (e) {
uni.showToast({ title: '分类数据加载失败', icon: 'none' })
}
try {
const rows = await homeApi.areas()
this.areaRows = Array.isArray(rows) ? rows : []
} catch (e) {}
this.loadPublish()
this.updateDraftCount()
},
/**
* 刷新草稿箱数量统计
*/
async updateDraftCount() {
try {
const drafts = await postApi.drafts()
this.draftCount = Array.isArray(drafts) ? drafts.length : 0
} catch (e) {
this.draftCount = 0
}
},
/**
* 跳转独立草稿箱页面
*/
goDraftsPage() {
uni.navigateTo({
url: '/pages/post/drafts',
})
},
/**
* 从草稿中还原数据
* @param {number} draftId
*/
async loadDraftPost(draftId) {
try {
const drafts = await postApi.drafts()
const target = (Array.isArray(drafts) ? drafts : []).find((d) => d.draftId === draftId)
if (target) {
this.currentDraftId = draftId
if (target.categoryId) this.form.categoryId = target.categoryId
this.form.title = target.title || ''
this.form.content = target.content || ''
this.form.contact = target.contact || ''
this.form.phone = target.phone || ''
this.form.wechat = target.wechat || ''
this.form.address = target.address || ''
this.form.areaId = target.areaId || 0
this.form.area = target.area || ''
this.form.lat = target.lat || null
this.form.lng = target.lng || null
if (target.identityType) this.form.identityType = target.identityType
if (target.price != null && target.price > 0) {
this.form.price = String(target.price)
this.form.isNegotiable = false
} else if (target.priceUnit === '面议') {
this.form.price = ''
this.form.isNegotiable = true
}
if (target.priceUnit) this.form.priceUnit = target.priceUnit
if (Array.isArray(target.tags)) this.form.tags = [...target.tags]
if (Array.isArray(target.media)) this.form.media = [...target.media]
uni.showToast({ title: '已载入草稿内容', icon: 'none' })
}
} catch (e) {}
},
/**
* 载入重新编辑信息
* @param {number} id
*/
async loadEditPost(id) {
try {
const p = await postApi.detail(id)
if (p) {
if (p.categoryId) this.form.categoryId = p.categoryId
this.form.title = p.title || ''
this.form.content = p.content || ''
this.form.contact = p.contact || ''
this.form.phone = p.phone || ''
this.form.wechat = p.wechat || ''
this.form.address = p.address || ''
this.form.areaId = p.areaId || 0
this.form.area = p.area || ''
this.form.lat = p.lat || null
this.form.lng = p.lng || null
if (p.identityType) this.form.identityType = p.identityType
if (p.price != null && p.price > 0) {
this.form.price = String(p.price)
this.form.isNegotiable = false
} else if (p.priceUnit === '面议' || p.price === 0) {
this.form.price = ''
this.form.isNegotiable = true
}
if (p.priceUnit) this.form.priceUnit = p.priceUnit
if (Array.isArray(p.tags)) this.form.tags = [...p.tags]
if (Array.isArray(p.media)) this.form.media = [...p.media]
uni.showToast({ title: '已载入待修改内容', icon: 'none' })
}
} catch (e) {}
},
/**
* 切换期望价格面议状态
*/
toggleNegotiable() {
this.form.isNegotiable = !this.form.isNegotiable
if (this.form.isNegotiable) {
this.form.price = ''
this.form.priceUnit = '面议'
} else {
this.form.priceUnit = (this.currentUnits && this.currentUnits[0]) || '元'
}
this.triggerAutoSave()
},
/**
* 快速选择计价单位
* @param {string} u 计价单位
*/
selectPriceUnit(u) {
this.form.priceUnit = u
if (this.form.isNegotiable) {
this.form.isNegotiable = false
}
this.triggerAutoSave()
},
/**
* 点击添加分类推荐亮点标签
* @param {string} rt 推荐标签
*/
addRecommendTag(rt) {
if (!rt) return
if (this.form.tags.includes(rt)) {
this.removeTag(this.form.tags.indexOf(rt))
return
}
if (this.form.tags.length >= 5) {
uni.showToast({ title: '最多添加5个标签', icon: 'none' })
return
}
this.form.tags.push(rt)
this.triggerAutoSave()
},
/**
* 移除已选亮点标签
* @param {number} idx 标签序号
*/
removeTag(idx) {
this.form.tags.splice(idx, 1)
this.triggerAutoSave()
},
/**
* 手动添加自定义标签
*/
addCustomTag() {
const tag = (this.customTagInput || '').trim()
if (!tag) return
if (this.form.tags.includes(tag)) {
uni.showToast({ title: '标签已存在', icon: 'none' })
return
}
if (this.form.tags.length >= 5) {
uni.showToast({ title: '最多添加5个标签', icon: 'none' })
return
}
this.form.tags.push(tag)
this.customTagInput = ''
this.triggerAutoSave()
},
/**
* 选中发布套餐卡片(区分单条与会员月卡/年卡特权)
* @param {Object} p 套餐对象
*/
selectPlan(p) {
if (p.key === 'month' || p.key === 'year') {
uni.showModal({
title: '开通会员特权',
content: `${p.name}为会员特权服务(${p.price} / ${p.desc})。开通后可畅享海量发布额度,是否前往会员中心开通?`,
confirmText: '去开通',
cancelText: '再想想',
confirmColor: '#00e070',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages/member/index' })
}
},
})
return
}
this.form.plan = p.key
},
/**
* 读取后台发布计费配置
*/
async loadPublish() {
try {
const p = await postApi.publishConfig()
this.publish = { ...this.publish, ...p }
if (p.mode === 'free') {
this.form.plan = 'free'
} else {
const freeAvailable = p.quotaLeft > 0 || p.freeUnlimited === true || Number(p.freeQuotaLeft) > 0
this.form.plan = freeAvailable ? 'quota' : 'single'
}
} catch (e) {}
},
/**
* 切换发布身份(个人 / 商家)
* 商家未认证时弹窗提示并引导跳转商家资质认证页面
* @param {'person'|'merchant'} type 身份类型
*/
switchIdentity(type) {
if (type === 'merchant') {
// 未认证商家时,提示并跳转商家认证页面
if (!this.isMerchantUser) {
const status = this.merchantAuditStatus
let title = '商家认证提示'
let content = '发布商家信息需先完成商家资质认证,认证后尊享商家标识与店铺展示。是否立即前往认证?'
let confirmText = '去认证'
if (status === 0) {
content = '您的商家资质正在审核中,工作人员将在1-3个工作日内完成审核。是否前往查看进度?'
confirmText = '查看进度'
} else if (status === 2) {
content = '您的商家资质审核未通过,请核对资料后重新提交。是否前往修改?'
confirmText = '重新提交'
}
uni.showModal({
title,
content,
confirmText,
cancelText: '取消',
confirmColor: '#00e070',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages/merchant/info' })
}
},
})
return
}
this.form.identityType = 'merchant'
// 自动带入商家的认证信息
const u = useUserStore().state.userInfo
if (u && u.merchant) {
const contact = u.merchant.contact_name || u.merchant.contactName || ''
const phone = u.merchant.contact_phone || u.merchant.contactPhone || ''
const address = u.merchant.address || ''
if (!this.form.contact && contact) {
this.form.contact = contact
}
if (!this.form.phone && phone) {
this.form.phone = phone
}
if (!this.form.address && address) {
this.form.address = address
}
}
} else {
this.form.identityType = 'person'
}
},
/**
* 打开全部分类弹窗抽屉
*/
openCatDrawer() {
this.catDrawerVisible = true
setTimeout(() => {
this.catDrawerAnim = true
}, 20)
},
/**
* 关闭全部分类抽屉
*/
closeCatDrawer() {
this.catDrawerAnim = false
setTimeout(() => {
this.catDrawerVisible = false
}, 200)
},
/**
* 在抽屉中点选分类
* @param {Object} c 分类项
*/
onSelectDrawerCat(c) {
this.selectCat(c)
this.closeCatDrawer()
},
/**
* 选中分类
* @param {Object} c 分类项
*/
selectCat(c) {
if (!c) return
const prevCat = this.form.categoryId
this.form.categoryId = c.id
if (prevCat !== c.id && !this.form.isNegotiable) {
this.form.priceUnit = (this.currentUnits && this.currentUnits[0]) || '元'
}
this.triggerAutoSave()
},
money(v) {
const n = Number(v || 0)
return Number.isInteger(n) ? String(n) : n.toFixed(2)
},
isVideo(url) {
return /\.(mp4|mov|m4v|avi|webm|mkv)(\?|$)/i.test(String(url || ''))
},
chooseImage() {
if (this.uploading) return
const remain = 6 - this.imageCount
if (remain <= 0) return uni.showToast({ title: '最多上传 6 张图片', icon: 'none' })
uni.chooseImage({
count: remain,
success: async (res) => {
await this.uploadFiles(res.tempFilePaths || [], 'image')
},
})
},
chooseVideo() {
if (this.uploading) return
if (this.videoCount >= 1) return uni.showToast({ title: '最多上传 1 个视频', icon: 'none' })
uni.chooseVideo({
maxDuration: 60,
success: async (res) => {
await this.uploadFiles([res.tempFilePath], 'video')
},
})
},
async uploadFiles(paths, type) {
if (!paths.length) return
this.uploading = true
uni.showLoading({ title: '上传中…', mask: true })
try {
for (const p of paths) {
const up = await postApi.uploadMedia(p, type)
if (up && up.url) this.form.media.push(up.url)
}
this.triggerAutoSave()
uni.hideLoading()
uni.showToast({ title: '上传成功', icon: 'none' })
} catch (e) {
uni.hideLoading()
uni.showToast({ title: e.message || '上传失败', icon: 'none' })
} finally {
this.uploading = false
}
},
removeMedia(i) {
this.form.media.splice(i, 1)
this.triggerAutoSave()
},
chooseLocation() {
uni.chooseLocation({
success: (res) => {
this.form.address = res.address || res.name
this.form.lat = typeof res.latitude === 'number' ? res.latitude : null
this.form.lng = typeof res.longitude === 'number' ? res.longitude : null
// 定位成功后自动匹配省 / 市 / 区(失败保留用户已选区域)
this.matchRegionByLocation(this.form.lat, this.form.lng)
this.triggerAutoSave()
},
fail: () => {
uni.showToast({ title: '请选择所属区域并填写地址', icon: 'none' })
},
})
},
/** 级联选择器选中服务区域 */
onRegionPicked(area) {
this.form.areaId = area ? Number(area.areaId) || 0 : 0
this.form.area = area ? area.district || '' : ''
this.triggerAutoSave()
},
/**
* 按经纬度匹配服务区域(后端 /home/locate)
* @param {number|null} lat
* @param {number|null} lng
*/
async matchRegionByLocation(lat, lng) {
if (lat == null || lng == null) return
try {
const found = await homeApi.locate({ lat, lng })
if (!found || !found.areaId) return
this.form.areaId = Number(found.areaId) || 0
this.form.area = found.district || ''
} catch (e) {}
},
onFieldInput() {
this.triggerAutoSave()
},
triggerAutoSave() {
if (this.draftTimer) clearTimeout(this.draftTimer)
this.draftTimer = setTimeout(() => {
if (this.form.title || this.form.content) {
this.executeSaveDraft(true)
}
}, 1200)
},
/**
* 用户点击手动存草稿
*/
async saveDraftManual() {
if (!this.form.title && !this.form.content) {
uni.showToast({ title: '内容为空,无需保存', icon: 'none' })
return
}
await this.executeSaveDraft(false)
},
/**
* 执行保存草稿接口
* @param {boolean} silent 是否静默保存
*/
async executeSaveDraft(silent = false) {
try {
const payload = {
...this.form,
price: this.form.isNegotiable ? 0 : (Number(this.form.price) || 0),
priceUnit: this.form.isNegotiable ? '面议' : (this.form.priceUnit || '元'),
tags: this.form.tags,
draftId: this.currentDraftId || 0,
}
const res = await postApi.saveDraft(payload)
if (res && res.draftId) {
this.currentDraftId = res.draftId
}
this.updateDraftCount()
if (!silent) {
uni.showToast({ title: '已存入草稿箱', icon: 'success' })
}
} catch (e) {
if (!silent) {
uni.showToast({ title: (e && e.message) || '保存失败', icon: 'none' })
}
}
},
/**
* 表单合法性验证
* @returns {string} 错误信息,为空则验证通过
*/
validate() {
if (!this.form.categoryId) return '请选择所属分类'
if (this.form.identityType === 'merchant' && !this.isMerchantUser) return '请先完成商家资质认证'
if (!this.form.title || !this.form.title.trim()) return '请填写标题'
if (!this.form.content || !this.form.content.trim()) return '请填写详细内容'
if (!this.form.areaId) return '请选择所属区域'
if (this.form.phone && !/^1\d{10}$/.test(this.form.phone)) return '请输入正确的11位手机号'
return ''
},
/**
* 提交发布信息
*/
async submit() {
if (this.submitting) return
if (this.uploading) {
uni.showToast({ title: '媒体上传中,请稍候', icon: 'none' })
return
}
const err = this.validate()
if (err) {
uni.showToast({ title: err, icon: 'none' })
return
}
this.submitting = true
try {
// 订阅消息授权(审核结果);未配置模板时静默跳过
await requestSubscribe('audit')
const payload = {
...this.form,
price: this.form.isNegotiable ? 0 : (Number(this.form.price) || 0),
priceUnit: this.form.isNegotiable ? '面议' : (this.form.priceUnit || '元'),
tags: this.form.tags,
plan: this.form.plan === 'quota' ? 'free' : this.form.plan,
}
const res = await postApi.create(payload)
// 若有需要支付的订单
if (res && res.order) {
try {
await pay(res.order, this.form.payType)
} catch (payErr) {
uni.showToast({ title: payErr.message || '支付未完成', icon: 'none' })
return
}
}
// 若发布成功且当前编辑自草稿,自动删除该草稿
if (this.currentDraftId) {
postApi.deleteDraft(this.currentDraftId).catch(() => {})
}
uni.showToast({
title: res.auditStatus === 0 ? '发布成功,审核中' : '发布成功',
icon: 'success',
})
setTimeout(() => {
uni.redirectTo({
url: `/pages/detail/detail?id=${res.postId}`,
})
}, 600)
} catch (e) {
uni.showToast({ title: e.message || '发布失败', icon: 'none' })
} finally {
this.submitting = false
}
},
},
}
</script>
<style lang="scss" scoped>
.create {
min-height: 100vh;
background-color: $fc-bg;
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
}
/* 顶部独立草稿箱入口按钮 */
.create__draft-entry {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.18);
border: 1rpx solid rgba(255, 255, 255, 0.35);
border-radius: 28rpx;
padding: 8rpx 18rpx;
position: relative;
cursor: pointer;
transition: opacity 0.15s ease;
&:active {
opacity: 0.8;
}
}
.create__draft-icon {
width: 28rpx;
height: 28rpx;
margin-right: 8rpx;
}
.create__draft-text {
font-size: 24rpx;
font-weight: 600;
color: #ffffff;
}
.create__draft-badge {
position: absolute;
top: -10rpx;
right: -10rpx;
background: #ff5252;
color: #ffffff;
font-size: 18rpx;
font-weight: 700;
min-width: 28rpx;
height: 28rpx;
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 0 6rpx;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.2);
}
/* 表单卡片区域 */
.create__section {
margin: 20rpx 28rpx 0;
padding: 30rpx 28rpx;
background: #ffffff;
border-radius: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(4, 48, 30, 0.03);
}
.create__label {
font-size: 28rpx;
font-weight: 700;
color: $fc-text;
display: flex;
align-items: center;
&--mt {
margin-top: 32rpx;
margin-bottom: 16rpx;
}
}
.create__required {
color: #e02020;
margin-left: 6rpx;
font-weight: 700;
}
.create__label-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
/* 分类选择单元格(唯一入口) */
.create__cell {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 88rpx;
padding: 12rpx 0;
cursor: pointer;
}
.create__cell-left {
display: flex;
align-items: center;
}
.create__cell-right {
display: flex;
align-items: center;
}
.create__cell-ph {
font-size: 28rpx;
color: $fc-text-placeholder;
}
.create__arrow {
font-size: 36rpx;
color: #b5c7bc;
margin-left: 12rpx;
line-height: 1;
}
.create__selected-cat {
display: flex;
align-items: center;
background: rgba(0, 224, 112, 0.12);
border: 1rpx solid rgba(0, 224, 112, 0.3);
border-radius: 30rpx;
padding: 8rpx 20rpx;
}
.create__selected-cat-icon {
width: 32rpx;
height: 32rpx;
margin-right: 10rpx;
}
.create__selected-cat-name {
font-size: 26rpx;
font-weight: 700;
color: #04874a;
}
.create__label-tip {
font-size: 22rpx;
color: $fc-text-placeholder;
font-weight: 400;
margin-left: auto;
}
/* 2. 身份切换组 */
.create__identity-group {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20rpx;
margin-top: 12rpx;
}
.create__identity-item {
border: 2rpx solid #e2eee6;
background: #f8fbf9;
border-radius: 18rpx;
padding: 22rpx 20rpx;
display: flex;
flex-direction: column;
transition: all 0.2s ease;
cursor: pointer;
&--active {
border-color: #00e070;
background: #edfbf3;
.create__identity-title {
color: #04874a;
}
}
&--unauth {
border-style: dashed;
background: #fafdfb;
}
}
.create__identity-title-row {
display: flex;
align-items: center;
}
.create__identity-title {
font-size: 28rpx;
font-weight: 700;
color: $fc-text;
}
.create__merchant-badge {
font-size: 18rpx;
font-weight: 700;
border-radius: 6rpx;
padding: 2rpx 10rpx;
margin-left: 10rpx;
line-height: 26rpx;
display: inline-flex;
align-items: center;
&--approved {
background: #f5a623;
color: #4a2600;
font-weight: 700;
}
&--unauth {
background: #e6f9ef;
color: #04874a;
border: 1rpx solid #b8ebd0;
}
&--pending {
background: #eff6ff;
color: #2563eb;
border: 1rpx solid #bfdbfe;
}
&--rejected {
background: #fef2f2;
color: #dc2626;
border: 1rpx solid #fecaca;
}
}
.create__identity-desc {
margin-top: 8rpx;
font-size: 22rpx;
color: $fc-text-placeholder;
}
/* 输入框与多行文本 */
.create__input {
height: 88rpx;
background: #f8faf8;
border-radius: 16rpx;
border: 1rpx solid #e6f0ea;
padding: 0 24rpx;
font-size: 28rpx;
color: $fc-text;
margin-top: 16rpx;
transition: border-color 0.2s ease;
&:focus {
border-color: #00e070;
background: #ffffff;
}
&--row {
margin-top: 0;
text-align: right;
border: none;
background: transparent;
padding: 0;
}
}
.create__textarea {
width: 100%;
height: 200rpx;
background: #f8faf8;
border-radius: 16rpx;
border: 1rpx solid #e6f0ea;
padding: 20rpx 24rpx;
font-size: 28rpx;
color: $fc-text;
box-sizing: border-box;
&:focus {
border-color: #00e070;
background: #ffffff;
}
}
.create__ph {
color: $fc-text-placeholder;
font-size: 26rpx;
}
.create__count {
display: block;
text-align: right;
font-size: 22rpx;
color: $fc-text-placeholder;
margin-top: 8rpx;
}
/* 媒体上传 */
.create__media {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 16rpx;
}
.create__media-item {
width: 156rpx;
height: 156rpx;
border-radius: 16rpx;
position: relative;
background: #f4f7f5;
overflow: hidden;
}
.create__media-img {
width: 100%;
height: 100%;
}
.create__media-video {
width: 100%;
height: 100%;
background: #1a2d23;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.create__media-video-txt {
font-size: 22rpx;
color: #ffffff;
margin-top: 8rpx;
}
.create__media-del {
position: absolute;
top: 8rpx;
right: 8rpx;
width: 38rpx;
height: 38rpx;
border-radius: 50%;
background: rgba(0, 0, 0, 0.65);
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
line-height: 1;
}
.create__upload {
width: 156rpx;
height: 156rpx;
border-radius: 16rpx;
border: 2rpx dashed #b8d4c4;
background: #f9fbf9;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
&:active {
background: #eef7f2;
}
}
.create__upload-txt {
font-size: 20rpx;
color: $fc-text-sub;
margin-top: 10rpx;
}
.create__uploading {
display: block;
margin-top: 12rpx;
font-size: 22rpx;
color: #04874a;
}
/* 价格与面议开关 */
.create__negotiable {
display: flex;
align-items: center;
cursor: pointer;
}
.create__checkbox {
width: 32rpx;
height: 32rpx;
border-radius: 8rpx;
border: 2rpx solid #b8d4c4;
background: #ffffff;
display: flex;
align-items: center;
justify-content: center;
margin-right: 10rpx;
transition: all 0.15s ease;
&--checked {
border-color: #00e070;
background: #00e070;
}
}
.create__checkbox-tick {
font-size: 20rpx;
font-weight: 900;
color: #04301e;
line-height: 1;
}
.create__negotiable-label {
font-size: 26rpx;
color: $fc-text-sub;
}
/* 标价与计价单位 */
.create__price-box {
margin-top: 16rpx;
}
.create__price-input-row {
display: flex;
align-items: center;
background: #f8faf8;
border-radius: 16rpx;
border: 1rpx solid #e6f0ea;
padding: 0 24rpx;
height: 92rpx;
transition: border-color 0.2s ease;
&:focus-within {
border-color: #00e070;
background: #ffffff;
}
}
.create__price-symbol {
font-size: 34rpx;
font-weight: 800;
color: #f5a623; /* 阳光金强调价值 */
margin-right: 12rpx;
}
.create__price-input {
flex: 1;
height: 88rpx;
font-size: 36rpx;
font-weight: 700;
color: #f5a623;
}
.create__price-unit-text {
font-size: 26rpx;
font-weight: 600;
color: $fc-text-sub;
margin-left: 12rpx;
}
/* 计价单位快捷胶囊 */
.create__unit-chips {
display: flex;
flex-wrap: wrap;
gap: 14rpx;
margin-top: 18rpx;
}
.create__unit-chip {
padding: 10rpx 24rpx;
border-radius: 28rpx;
font-size: 24rpx;
color: $fc-text;
background: #f4f8f5;
border: 1rpx solid #dcece1;
cursor: pointer;
transition: all 0.15s ease;
&--active {
background: rgba(0, 224, 112, 0.16);
border-color: #00e070;
color: #04874a;
font-weight: 700;
}
}
/* 特色亮点标签 */
.create__tags-wrapper {
display: flex;
flex-wrap: wrap;
gap: 14rpx;
margin-top: 16rpx;
}
.create__tag-badge {
display: inline-flex;
align-items: center;
background: #00e070;
border-radius: 28rpx;
padding: 8rpx 20rpx;
cursor: pointer;
box-shadow: 0 2rpx 8rpx rgba(0, 224, 112, 0.2);
}
.create__tag-badge-text {
font-size: 24rpx;
font-weight: 700;
color: #04301e; /* 亮绿背景严格使用深墨绿 */
margin-right: 8rpx;
}
.create__tag-badge-del {
font-size: 26rpx;
font-weight: 900;
color: #04301e;
line-height: 1;
}
/* 推荐标签列表 */
.create__recommend-tags {
margin-top: 20rpx;
padding: 18rpx 20rpx;
background: #f8fbf9;
border-radius: 16rpx;
border: 1rpx dashed #d8eee0;
}
.create__recommend-title {
font-size: 22rpx;
color: #04874a;
font-weight: 600;
display: block;
margin-bottom: 12rpx;
}
.create__recommend-chips {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.create__recommend-chip {
font-size: 22rpx;
color: #04874a;
background: #ffffff;
border: 1rpx solid #cbe9d5;
border-radius: 24rpx;
padding: 6rpx 18rpx;
cursor: pointer;
transition: all 0.15s ease;
&--disabled {
opacity: 0.45;
background: #f0f4f1;
border-color: #dbe4df;
color: $fc-text-placeholder;
pointer-events: none;
}
}
/* 自定义亮点标签输入行 */
.create__tag-custom-row {
display: flex;
align-items: center;
margin-top: 16rpx;
gap: 16rpx;
}
.create__tag-custom-input {
flex: 1;
height: 72rpx;
background: #f8faf8;
border-radius: 14rpx;
border: 1rpx solid #e6f0ea;
padding: 0 20rpx;
font-size: 24rpx;
color: $fc-text;
}
.create__tag-custom-btn {
height: 72rpx;
padding: 0 28rpx;
background: #04874a;
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.create__tag-custom-btn-txt {
font-size: 24rpx;
font-weight: 700;
color: #ffffff;
}
/* 联系信息行 */
.create__row {
display: flex;
align-items: center;
justify-content: space-between;
height: 96rpx;
&--line {
border-top: 1rpx solid #f2f6f3;
}
}
.create__row-label {
font-size: 28rpx;
font-weight: 600;
color: $fc-text;
flex-shrink: 0;
}
.create__area-picker {
flex: 1;
margin-left: 16rpx;
}
.create__location {
display: flex;
align-items: center;
justify-content: flex-end;
flex: 1;
max-width: 500rpx;
cursor: pointer;
}
.create__location-text {
font-size: 26rpx;
color: $fc-text-sub;
margin: 0 8rpx;
}
.create__location-arrow {
font-size: 32rpx;
color: #b5c7bc;
line-height: 1;
}
/* 发布套餐与计费 */
.create__free-tip {
background: rgba(0, 224, 112, 0.1);
border: 1rpx solid rgba(0, 224, 112, 0.25);
border-radius: 14rpx;
padding: 20rpx;
font-size: 24rpx;
color: #04874a;
text-align: center;
}
.create__quota {
font-size: 22rpx;
color: #f5a623;
font-weight: 600;
}
.create__plans {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16rpx;
}
.create__plan {
border: 2rpx solid #e2eee6;
background: #f8fbf9;
border-radius: 18rpx;
padding: 20rpx;
position: relative;
cursor: pointer;
transition: all 0.2s ease;
&--active {
border-color: #00e070;
background: #edfbf3;
.create__plan-name {
color: #04874a;
}
.create__plan-price {
color: #04301e;
}
}
}
.create__plan-name {
font-size: 26rpx;
font-weight: 700;
color: $fc-text;
display: block;
}
.create__plan-price {
font-size: 32rpx;
font-weight: 800;
color: $fc-text;
margin: 8rpx 0;
display: block;
}
.create__plan-desc {
font-size: 20rpx;
color: $fc-text-placeholder;
display: block;
}
.create__plan-tag {
position: absolute;
top: -12rpx;
right: 14rpx;
background: #ff5252;
color: #ffffff;
font-size: 18rpx;
font-weight: 700;
padding: 2rpx 10rpx;
border-radius: 8rpx;
}
/* 支付方式 */
.create__pay {
margin-top: 24rpx;
padding-top: 24rpx;
border-top: 1rpx solid #f2f6f3;
}
.create__pay-label {
font-size: 26rpx;
font-weight: 600;
color: $fc-text;
margin-bottom: 16rpx;
display: block;
}
.create__pay-types {
display: flex;
gap: 20rpx;
}
.create__pay-type {
flex: 1;
height: 84rpx;
border-radius: 16rpx;
border: 1rpx solid #e2eee6;
background: #f8faf8;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
cursor: pointer;
&--active {
border-color: #00e070;
background: #ebfaf0;
.create__pay-text {
color: #04874a;
font-weight: 700;
}
}
}
.create__pay-text {
font-size: 24rpx;
color: $fc-text;
}
/* 底部主控操作条 */
.create__bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 90;
background: #ffffff;
border-top: 1rpx solid #e8f0eb;
padding: 18rpx 32rpx calc(18rpx + env(safe-area-inset-bottom));
display: flex;
align-items: center;
gap: 24rpx;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
}
.create__bar-draft {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 16rpx;
cursor: pointer;
}
.create__bar-draft-icon {
width: 36rpx;
height: 36rpx;
opacity: 0.7;
}
.create__bar-draft-txt {
font-size: 20rpx;
color: $fc-text-sub;
margin-top: 4rpx;
}
.create__bar-submit {
flex: 1;
height: 92rpx;
border-radius: 46rpx;
background: linear-gradient(135deg, #00e070 0%, #00c860 100%);
box-shadow: 0 8rpx 24rpx rgba(0, 224, 112, 0.3);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: opacity 0.15s ease, transform 0.15s ease;
&:active {
transform: scale(0.985);
}
&--disabled {
opacity: 0.55;
pointer-events: none;
}
}
.create__bar-submit-txt {
font-size: 32rpx;
font-weight: 700;
color: #04301e; /* 遵守春绿规范:亮绿上必须深墨绿 */
letter-spacing: 1rpx;
}
/* ================= 全部分类抽屉弹窗 ================= */
.cat-drawer-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
background: rgba(0, 0, 0, 0);
transition: background-color 0.2s ease;
display: flex;
flex-direction: column;
justify-content: flex-end;
&--visible {
background: rgba(0, 0, 0, 0.5);
}
}
.cat-drawer {
background: #ffffff;
border-radius: 36rpx 36rpx 0 0;
max-height: 75vh;
display: flex;
flex-direction: column;
transform: translateY(100%);
transition: transform 0.22s ease-out;
padding-bottom: env(safe-area-inset-bottom);
&--visible {
transform: translateY(0);
}
}
.cat-drawer__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 36rpx 20rpx;
border-bottom: 1rpx solid #f0f4f2;
}
.cat-drawer__title {
font-size: 32rpx;
font-weight: 700;
color: $fc-text;
}
.cat-drawer__close {
width: 54rpx;
height: 54rpx;
border-radius: 50%;
background: #f2f6f4;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.cat-drawer__close-txt {
font-size: 24rpx;
color: #8c9e94;
line-height: 1;
}
.cat-drawer__body {
max-height: 60vh;
padding: 24rpx 32rpx;
box-sizing: border-box;
}
.cat-drawer__grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20rpx;
}
.cat-drawer__item {
background: #f7faf8;
border: 2rpx solid #e7efe9;
border-radius: 20rpx;
padding: 24rpx 16rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
transition: all 0.15s ease;
cursor: pointer;
&--active {
border-color: #00e070;
background: #ebfaf0;
box-shadow: 0 4rpx 16rpx rgba(0, 224, 112, 0.2);
.cat-drawer__name {
color: #04874a;
font-weight: 700;
}
}
}
.cat-drawer__icon-box {
width: 72rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12rpx;
}
.cat-drawer__icon {
width: 60rpx;
height: 60rpx;
}
.cat-drawer__name {
font-size: 26rpx;
color: $fc-text;
text-align: center;
}
.cat-drawer__check {
position: absolute;
top: 8rpx;
right: 12rpx;
font-size: 22rpx;
font-weight: 900;
color: #04874a;
}
.cat-drawer__space {
height: 40rpx;
}
</style>