|
|
@@ -0,0 +1,994 @@
|
|
|
+<!--
|
|
|
+ 分类广告设置页
|
|
|
+ 用途:配置最多3张轮播图,数据存 xhCategorySetting.advertisement JSON
|
|
|
+ 格式:[{"type":1,"img":"path","value":"id1,id2"}] type:1图文 2商品 3分类
|
|
|
+-->
|
|
|
+<template>
|
|
|
+ <view class="page-container">
|
|
|
+ <view class="page-header">
|
|
|
+ <text class="page-title">分类广告图片</text>
|
|
|
+ <text class="page-sub">最多支持上传3张轮播图</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="ad-card" v-for="(item, index) in adList" :key="index">
|
|
|
+ <view class="card-title">轮播图{{ index + 1 }}</view>
|
|
|
+
|
|
|
+ <view class="upload-area" @click="chooseImage(index)">
|
|
|
+ <block v-if="item.img">
|
|
|
+ <image class="banner-img" :src="imgFullUrl(item.img)" mode="aspectFill" />
|
|
|
+ <view class="remove-btn" @click.stop="removeImage(index)">×</view>
|
|
|
+ </block>
|
|
|
+ <block v-else>
|
|
|
+ <view class="upload-placeholder">
|
|
|
+ <text class="upload-icon">☁</text>
|
|
|
+ <text class="upload-text">点击上传图片</text>
|
|
|
+ <text class="upload-tip">建议尺寸:750*300,支持 jpg/png</text>
|
|
|
+ </view>
|
|
|
+ </block>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="field-row">
|
|
|
+ <text class="field-label">关联类型</text>
|
|
|
+ <view class="type-tabs">
|
|
|
+ <view
|
|
|
+ class="type-tab"
|
|
|
+ v-for="opt in typeOptions"
|
|
|
+ :key="opt.value"
|
|
|
+ :class="{ active: item.type === opt.value }"
|
|
|
+ @click="changeType(index, opt.value)"
|
|
|
+ >{{ opt.label }}</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="field-row field-row-link" @click="openLinkSelect(index)">
|
|
|
+ <text class="field-label">关联内容</text>
|
|
|
+ <view class="link-select">
|
|
|
+ <text class="link-text" :class="{ placeholder: !getActiveLink(item).valueLabel }">{{ getActiveLink(item).valueLabel || '选择关联内容' }}</text>
|
|
|
+ <text class="link-arrow">></text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="bottom-space"></view>
|
|
|
+
|
|
|
+ <!-- 分类多选弹层:支持搜索与已选预览,分类较多时便于查找 -->
|
|
|
+ <view v-if="categoryPickerShow" class="picker-overlay" @touchmove.stop.prevent="preventTouchMove">
|
|
|
+ <view class="picker-mask" @tap.stop="closeCategoryPicker"></view>
|
|
|
+ <view class="picker-panel category-picker-panel">
|
|
|
+ <view class="picker-head">
|
|
|
+ <text class="picker-title">选择分类</text>
|
|
|
+ <text class="picker-count">已选 {{ categoryPickCount }} 个</text>
|
|
|
+ <text class="picker-close" @tap.stop="closeCategoryPicker">×</text>
|
|
|
+ </view>
|
|
|
+ <view class="picker-search-row">
|
|
|
+ <view class="picker-search-box">
|
|
|
+ <text class="picker-search-icon">⌕</text>
|
|
|
+ <input
|
|
|
+ v-model="categorySearchText"
|
|
|
+ class="picker-search-input"
|
|
|
+ placeholder="搜索分类名称"
|
|
|
+ confirm-type="search"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+ <text
|
|
|
+ v-if="categorySearchText"
|
|
|
+ class="picker-search-clear"
|
|
|
+ @tap.stop="clearCategorySearch"
|
|
|
+ >清空</text>
|
|
|
+ </view>
|
|
|
+ <scroll-view
|
|
|
+ v-if="selectedCategoryItems.length"
|
|
|
+ scroll-x
|
|
|
+ class="picker-selected-row"
|
|
|
+ :show-scrollbar="false"
|
|
|
+ >
|
|
|
+ <view class="picker-selected-wrap">
|
|
|
+ <view
|
|
|
+ class="picker-selected-chip"
|
|
|
+ v-for="item in selectedCategoryItems"
|
|
|
+ :key="item.id"
|
|
|
+ >
|
|
|
+ <text class="chip-name">{{ item.name }}</text>
|
|
|
+ <text class="chip-remove" :data-id="item.id" @tap.stop="removeCategoryPickByEvent">×</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </scroll-view>
|
|
|
+ <scroll-view scroll-y class="picker-body">
|
|
|
+ <block v-if="filteredBouquetCategories.length">
|
|
|
+ <view
|
|
|
+ class="picker-item"
|
|
|
+ v-for="cat in filteredBouquetCategories"
|
|
|
+ :key="cat.id"
|
|
|
+ :data-id="cat.id"
|
|
|
+ @tap.stop="toggleCategoryPickByEvent"
|
|
|
+ >
|
|
|
+ <view class="check-box" :class="{ checked: !!categoryPickMap[String(cat.id)] }">
|
|
|
+ <text v-if="categoryPickMap[String(cat.id)]" class="check-mark">✓</text>
|
|
|
+ </view>
|
|
|
+ <text class="picker-item-name">{{ cat.categoryName }}</text>
|
|
|
+ </view>
|
|
|
+ </block>
|
|
|
+ <view v-else class="picker-empty">
|
|
|
+ <text>{{ categorySearchText ? '未找到匹配的分类' : '暂无分类' }}</text>
|
|
|
+ </view>
|
|
|
+ </scroll-view>
|
|
|
+ <view class="picker-footer">
|
|
|
+ <button class="picker-btn cancel" hover-class="none" @click="closeCategoryPicker">取消</button>
|
|
|
+ <button class="picker-btn confirm" hover-class="none" @click="confirmCategoryPick">确定</button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 预览 -->
|
|
|
+ <view v-if="previewShow" class="picker-overlay" @touchmove.stop.prevent="preventTouchMove">
|
|
|
+ <view class="picker-mask" @click.stop="previewShow = false"></view>
|
|
|
+ <view class="preview-panel">
|
|
|
+ <swiper class="preview-swiper" indicator-dots circular>
|
|
|
+ <swiper-item v-for="(slide, idx) in previewSlides" :key="idx">
|
|
|
+ <image class="preview-img" :src="slide" mode="aspectFit" />
|
|
|
+ </swiper-item>
|
|
|
+ </swiper>
|
|
|
+ <button class="preview-close" hover-class="none" @click="previewShow = false">关闭预览</button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="bottom-bar">
|
|
|
+ <button class="bar-btn bar-btn-preview" hover-class="none" @click="previewAd">
|
|
|
+ <text class="preview-eye">👁</text>
|
|
|
+ <text>预览</text>
|
|
|
+ </button>
|
|
|
+ <button class="bar-btn bar-btn-save" hover-class="none" @click="saveAd">保存</button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { APIHOST } from '@/config';
|
|
|
+import { categoryListInfo } from '@/api/goods';
|
|
|
+import { categorySettingDetail, categorySettingSaveAdvertisement } from '@/api/category-setting';
|
|
|
+
|
|
|
+const EMPTY_LINK = () => ({ value: '', valueLabel: '' });
|
|
|
+
|
|
|
+const EMPTY_SLOT = () => ({
|
|
|
+ type: 1,
|
|
|
+ img: '',
|
|
|
+ // 各关联类型独立缓存,切换类型时不丢失已选内容
|
|
|
+ links: {
|
|
|
+ 1: EMPTY_LINK(),
|
|
|
+ 2: EMPTY_LINK(),
|
|
|
+ 3: EMPTY_LINK()
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'category-ad',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ constant: this.$constant,
|
|
|
+ settingId: 0,
|
|
|
+ adList: [EMPTY_SLOT(), EMPTY_SLOT(), EMPTY_SLOT()],
|
|
|
+ typeOptions: [
|
|
|
+ { label: '图文', value: 1 },
|
|
|
+ { label: '商品', value: 2 },
|
|
|
+ { label: '分类', value: 3 }
|
|
|
+ ],
|
|
|
+ bouquetCategories: [],
|
|
|
+ categoryPickerShow: false,
|
|
|
+ categoryPickIndex: 0,
|
|
|
+ categoryPickMap: {},
|
|
|
+ categorySearchText: '',
|
|
|
+ previewShow: false,
|
|
|
+ saving: false
|
|
|
+ };
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ previewSlides() {
|
|
|
+ return this.adList
|
|
|
+ .filter((item) => item.img)
|
|
|
+ .map((item) => this.imgFullUrl(item.img));
|
|
|
+ },
|
|
|
+ /** 分类弹层内已选数量 */
|
|
|
+ categoryPickCount() {
|
|
|
+ return Object.keys(this.categoryPickMap).length;
|
|
|
+ },
|
|
|
+ /** 按关键词过滤花束分类,分类多时可快速定位 */
|
|
|
+ filteredBouquetCategories() {
|
|
|
+ const keyword = (this.categorySearchText || '').trim().toLowerCase();
|
|
|
+ if (!keyword) return this.bouquetCategories;
|
|
|
+ return this.bouquetCategories.filter((cat) => {
|
|
|
+ const name = (cat.categoryName || '').toLowerCase();
|
|
|
+ return name.indexOf(keyword) > -1;
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /** 已选分类标签,便于回显与快速取消 */
|
|
|
+ selectedCategoryItems() {
|
|
|
+ return Object.keys(this.categoryPickMap).map((id) => {
|
|
|
+ const cat = this.bouquetCategories.find((c) => String(c.id) === String(id));
|
|
|
+ return {
|
|
|
+ id,
|
|
|
+ name: cat ? cat.categoryName : id
|
|
|
+ };
|
|
|
+ });
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onLoad() {
|
|
|
+ this.loadSetting();
|
|
|
+ this.loadBouquetCategories();
|
|
|
+ uni.$on('categoryAdPicTextSelected', this.onPicTextSelected);
|
|
|
+ uni.$on('categoryAdGoodsSelected', this.onGoodsSelected);
|
|
|
+ },
|
|
|
+ onUnload() {
|
|
|
+ uni.$off('categoryAdPicTextSelected', this.onPicTextSelected);
|
|
|
+ uni.$off('categoryAdGoodsSelected', this.onGoodsSelected);
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ preventTouchMove() {},
|
|
|
+ imgFullUrl(path) {
|
|
|
+ if (!path) return '';
|
|
|
+ if (path.indexOf('http') === 0) return path;
|
|
|
+ const base = (this.constant.imgUrl || '').replace(/\/$/, '');
|
|
|
+ return `${base}/${path.replace(/^\//, '')}`;
|
|
|
+ },
|
|
|
+ loadSetting() {
|
|
|
+ categorySettingDetail().then((res) => {
|
|
|
+ if (res.code != 1) return;
|
|
|
+ const data = res.data || {};
|
|
|
+ this.settingId = data.id || 0;
|
|
|
+ this.adList = this.parseAdvertisement(data.advertisement);
|
|
|
+ });
|
|
|
+ },
|
|
|
+ loadBouquetCategories() {
|
|
|
+ categoryListInfo().then((res) => {
|
|
|
+ const list = res.data || [];
|
|
|
+ this.bouquetCategories = list.filter((item) => item.flower == 0);
|
|
|
+ // 分类名称依赖列表,加载后刷新各 slot 分类类型展示文案
|
|
|
+ this.adList = this.adList.map((item) => this.refreshSlotLinkLabels(item));
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /** 取当前关联类型下的内容与展示文案 */
|
|
|
+ getActiveLink(item) {
|
|
|
+ const type = item.type || 1;
|
|
|
+ return (item.links && item.links[type]) || EMPTY_LINK();
|
|
|
+ },
|
|
|
+ /** 写入指定类型的关联内容 */
|
|
|
+ setSlotLink(item, type, value, valueLabel) {
|
|
|
+ if (!item.links) {
|
|
|
+ item.links = { 1: EMPTY_LINK(), 2: EMPTY_LINK(), 3: EMPTY_LINK() };
|
|
|
+ }
|
|
|
+ this.$set(item.links, type, { value: value || '', valueLabel: valueLabel || '' });
|
|
|
+ },
|
|
|
+ /** 分类列表就绪后,重算 slot 内各类型展示文案 */
|
|
|
+ refreshSlotLinkLabels(item) {
|
|
|
+ const links = item.links || { 1: EMPTY_LINK(), 2: EMPTY_LINK(), 3: EMPTY_LINK() };
|
|
|
+ [1, 2, 3].forEach((type) => {
|
|
|
+ const link = links[type] || EMPTY_LINK();
|
|
|
+ if (link.value) {
|
|
|
+ links[type] = {
|
|
|
+ value: link.value,
|
|
|
+ valueLabel: this.buildValueLabel(type, link.value, link.valueLabel)
|
|
|
+ };
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return { ...item, links };
|
|
|
+ },
|
|
|
+ parseAdvertisement(raw) {
|
|
|
+ const slots = [EMPTY_SLOT(), EMPTY_SLOT(), EMPTY_SLOT()];
|
|
|
+ if (!raw) return slots;
|
|
|
+ let arr = [];
|
|
|
+ try {
|
|
|
+ arr = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
|
+ } catch (e) {
|
|
|
+ return slots;
|
|
|
+ }
|
|
|
+ if (!Array.isArray(arr)) return slots;
|
|
|
+ arr.slice(0, 3).forEach((item, index) => {
|
|
|
+ const type = parseInt(item.type, 10) || 1;
|
|
|
+ const value = item.value ? String(item.value) : '';
|
|
|
+ const links = {
|
|
|
+ 1: EMPTY_LINK(),
|
|
|
+ 2: EMPTY_LINK(),
|
|
|
+ 3: EMPTY_LINK()
|
|
|
+ };
|
|
|
+ links[type] = {
|
|
|
+ value,
|
|
|
+ valueLabel: this.buildValueLabel(type, value)
|
|
|
+ };
|
|
|
+ slots[index] = {
|
|
|
+ type,
|
|
|
+ img: item.img || '',
|
|
|
+ links
|
|
|
+ };
|
|
|
+ });
|
|
|
+ return slots;
|
|
|
+ },
|
|
|
+ buildValueLabel(type, value, fallbackLabel) {
|
|
|
+ if (!value) return '';
|
|
|
+ const ids = value.split(',').filter(Boolean);
|
|
|
+ if (type === 1) {
|
|
|
+ if (fallbackLabel && fallbackLabel.indexOf('图文ID:') !== 0) return fallbackLabel;
|
|
|
+ return ids.length ? `图文ID:${ids.join(',')}` : '';
|
|
|
+ }
|
|
|
+ if (type === 2) return `已选${ids.length}个商品`;
|
|
|
+ if (type === 3) {
|
|
|
+ const names = ids.map((id) => {
|
|
|
+ const cat = this.bouquetCategories.find((c) => String(c.id) === String(id));
|
|
|
+ return cat ? cat.categoryName : id;
|
|
|
+ });
|
|
|
+ return names.join('、') || `已选${ids.length}个分类`;
|
|
|
+ }
|
|
|
+ return '';
|
|
|
+ },
|
|
|
+ /** 仅切换展示类型,不清空各类型已缓存的关联内容 */
|
|
|
+ changeType(index, type) {
|
|
|
+ const item = this.adList[index];
|
|
|
+ if (item.type === type) return;
|
|
|
+ item.type = type;
|
|
|
+ },
|
|
|
+ openLinkSelect(index) {
|
|
|
+ const item = this.adList[index];
|
|
|
+ const link = this.getActiveLink(item);
|
|
|
+ if (item.type === 1) {
|
|
|
+ uni.navigateTo({
|
|
|
+ url: `/admin/picText/picTextList?mode=selectAd&slotIndex=${index}&picTextId=${link.value || 0}`
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (item.type === 2) {
|
|
|
+ uni.navigateTo({
|
|
|
+ url: `/admin/goods/ad-goods-select?slotIndex=${index}&selectedIds=${link.value || ''}`
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (item.type === 3) {
|
|
|
+ this.categoryPickIndex = index;
|
|
|
+ this.categorySearchText = '';
|
|
|
+ this.categoryPickMap = {};
|
|
|
+ const ids = link.value ? link.value.split(',').filter(Boolean) : [];
|
|
|
+ ids.forEach((id) => {
|
|
|
+ this.$set(this.categoryPickMap, String(id), true);
|
|
|
+ });
|
|
|
+ this.categoryPickerShow = true;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onPicTextSelected(payload) {
|
|
|
+ const index = payload.slotIndex;
|
|
|
+ if (index === undefined || !this.adList[index]) return;
|
|
|
+ const valueLabel = payload.title || `图文ID:${payload.picTextId}`;
|
|
|
+ this.setSlotLink(this.adList[index], 1, String(payload.picTextId || ''), valueLabel);
|
|
|
+ },
|
|
|
+ onGoodsSelected(payload) {
|
|
|
+ const index = payload.slotIndex;
|
|
|
+ if (index === undefined || !this.adList[index]) return;
|
|
|
+ const ids = (payload.goodsIds || []).map(String);
|
|
|
+ const value = ids.join(',');
|
|
|
+ this.setSlotLink(
|
|
|
+ this.adList[index],
|
|
|
+ 2,
|
|
|
+ value,
|
|
|
+ ids.length ? `已选${ids.length}个商品` : ''
|
|
|
+ );
|
|
|
+ },
|
|
|
+ /** 从 data-id 切换分类勾选,兼容小程序 tap 传参 */
|
|
|
+ toggleCategoryPickByEvent(e) {
|
|
|
+ const id = e.currentTarget.dataset.id;
|
|
|
+ if (id === undefined || id === null || id === '') return;
|
|
|
+ const key = String(id);
|
|
|
+ if (this.categoryPickMap[key]) {
|
|
|
+ this.$delete(this.categoryPickMap, key);
|
|
|
+ } else {
|
|
|
+ this.$set(this.categoryPickMap, key, true);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ /** 从已选标签移除单个分类 */
|
|
|
+ removeCategoryPickByEvent(e) {
|
|
|
+ const id = e.currentTarget.dataset.id;
|
|
|
+ if (id === undefined || id === null || id === '') return;
|
|
|
+ this.$delete(this.categoryPickMap, String(id));
|
|
|
+ },
|
|
|
+ clearCategorySearch() {
|
|
|
+ this.categorySearchText = '';
|
|
|
+ },
|
|
|
+ closeCategoryPicker() {
|
|
|
+ this.categoryPickerShow = false;
|
|
|
+ this.categorySearchText = '';
|
|
|
+ },
|
|
|
+ confirmCategoryPick() {
|
|
|
+ const index = this.categoryPickIndex;
|
|
|
+ const ids = Object.keys(this.categoryPickMap);
|
|
|
+ const value = ids.join(',');
|
|
|
+ this.setSlotLink(this.adList[index], 3, value, this.buildValueLabel(3, value));
|
|
|
+ this.categoryPickerShow = false;
|
|
|
+ this.categorySearchText = '';
|
|
|
+ },
|
|
|
+ chooseImage(index) {
|
|
|
+ uni.chooseImage({
|
|
|
+ count: 1,
|
|
|
+ sizeType: ['compressed'],
|
|
|
+ sourceType: ['album', 'camera'],
|
|
|
+ success: (res) => {
|
|
|
+ const filePath = res.tempFilePaths[0];
|
|
|
+ this.uploadImageFile(filePath, index);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /** 通过 multipart 上传广告图,存相对路径 shortUrl 供保存与展示 */
|
|
|
+ uploadImageFile(filePath, index) {
|
|
|
+ uni.showLoading({ title: '上传中', mask: true });
|
|
|
+ const token = uni.getStorageSync('token') || '';
|
|
|
+ uni.uploadFile({
|
|
|
+ url: `${APIHOST}/upload/save-file`,
|
|
|
+ filePath,
|
|
|
+ name: 'file',
|
|
|
+ fileType: 'image',
|
|
|
+ header: { token },
|
|
|
+ success: (uploadRes) => {
|
|
|
+ uni.hideLoading();
|
|
|
+ try {
|
|
|
+ const res = JSON.parse(uploadRes.data || '{}');
|
|
|
+ if (res.code == 1 && res.data) {
|
|
|
+ // 保存相对路径,展示时由 imgFullUrl 拼接域名
|
|
|
+ this.adList[index].img = res.data.shortUrl || res.data.url || '';
|
|
|
+ if (!this.adList[index].img) {
|
|
|
+ this.$msg('上传失败');
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.$msg(res.message || '上传失败');
|
|
|
+ } catch (e) {
|
|
|
+ this.$msg('上传失败');
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail: () => {
|
|
|
+ uni.hideLoading();
|
|
|
+ this.$msg('上传失败');
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ removeImage(index) {
|
|
|
+ this.adList[index].img = '';
|
|
|
+ },
|
|
|
+ previewAd() {
|
|
|
+ if (!this.previewSlides.length) {
|
|
|
+ this.$msg('请先上传广告图');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.previewShow = true;
|
|
|
+ },
|
|
|
+ saveAd() {
|
|
|
+ const payload = [];
|
|
|
+ for (let i = 0; i < this.adList.length; i++) {
|
|
|
+ const item = this.adList[i];
|
|
|
+ const link = this.getActiveLink(item);
|
|
|
+ if (!item.img && !link.value) continue;
|
|
|
+ if (item.img && !link.value) {
|
|
|
+ this.$msg(`请完善轮播图${i + 1}的关联内容`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (link.value && !item.img) {
|
|
|
+ this.$msg(`请上传轮播图${i + 1}的图片`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ payload.push({
|
|
|
+ type: item.type,
|
|
|
+ img: item.img,
|
|
|
+ value: link.value
|
|
|
+ });
|
|
|
+ }
|
|
|
+ if (this.saving) return;
|
|
|
+ this.saving = true;
|
|
|
+ categorySettingSaveAdvertisement({
|
|
|
+ advertisement: JSON.stringify(payload)
|
|
|
+ }).then((res) => {
|
|
|
+ if (res.code == 1) {
|
|
|
+ this.$msg('保存成功');
|
|
|
+ setTimeout(() => uni.navigateBack(), 500);
|
|
|
+ }
|
|
|
+ }).finally(() => {
|
|
|
+ this.saving = false;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.page-container {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: #f5f5f5;
|
|
|
+ padding-bottom: 160rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.page-header {
|
|
|
+ padding: 24rpx 30rpx 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.page-title {
|
|
|
+ display: block;
|
|
|
+ font-size: 30rpx;
|
|
|
+ color: #333;
|
|
|
+ font-weight: 600;
|
|
|
+ margin-bottom: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.page-sub {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+
|
|
|
+.ad-card {
|
|
|
+ margin: 20rpx 24rpx 0;
|
|
|
+ padding: 24rpx;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ border: 1rpx solid #eee;
|
|
|
+}
|
|
|
+
|
|
|
+.card-title {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #333;
|
|
|
+ font-weight: 600;
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-area {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ height: 240rpx;
|
|
|
+ border-radius: 8rpx;
|
|
|
+ overflow: hidden;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.banner-img {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.remove-btn {
|
|
|
+ position: absolute;
|
|
|
+ top: 12rpx;
|
|
|
+ right: 12rpx;
|
|
|
+ width: 44rpx;
|
|
|
+ height: 44rpx;
|
|
|
+ line-height: 40rpx;
|
|
|
+ text-align: center;
|
|
|
+ background: rgba(0, 0, 0, 0.45);
|
|
|
+ color: #fff;
|
|
|
+ border-radius: 50%;
|
|
|
+ font-size: 32rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-placeholder {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ border: 2rpx dashed #52c41a;
|
|
|
+ border-radius: 8rpx;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: #fafafa;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-icon {
|
|
|
+ font-size: 48rpx;
|
|
|
+ color: #52c41a;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-text {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #52c41a;
|
|
|
+ margin-bottom: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-tip {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+
|
|
|
+.field-row {
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.field-row-link {
|
|
|
+ margin-bottom: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.field-label {
|
|
|
+ display: block;
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #333;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.type-tabs {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+}
|
|
|
+
|
|
|
+.type-tab {
|
|
|
+ flex: 1;
|
|
|
+ height: 64rpx;
|
|
|
+ line-height: 64rpx;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #666;
|
|
|
+ border: 2rpx solid #e5e5e5;
|
|
|
+ background: #fff;
|
|
|
+ margin-right: 16rpx;
|
|
|
+ border-radius: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.type-tab:last-child {
|
|
|
+ margin-right: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.type-tab.active {
|
|
|
+ color: #52c41a;
|
|
|
+ border-color: #52c41a;
|
|
|
+ background: #f6ffed;
|
|
|
+}
|
|
|
+
|
|
|
+.link-select {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ height: 72rpx;
|
|
|
+ padding: 0 20rpx;
|
|
|
+ border: 1rpx solid #e5e5e5;
|
|
|
+ border-radius: 8rpx;
|
|
|
+ background: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.link-text {
|
|
|
+ flex: 1;
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #333;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.link-text.placeholder {
|
|
|
+ color: #bbb;
|
|
|
+}
|
|
|
+
|
|
|
+.link-arrow {
|
|
|
+ color: #ccc;
|
|
|
+ font-size: 28rpx;
|
|
|
+ margin-left: 12rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.bottom-space {
|
|
|
+ height: 40rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.bottom-bar {
|
|
|
+ position: fixed;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ align-items: center;
|
|
|
+ padding: 20rpx 24rpx;
|
|
|
+ padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
|
|
+ background: #fff;
|
|
|
+ box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.06);
|
|
|
+ z-index: 100;
|
|
|
+}
|
|
|
+
|
|
|
+.bar-btn {
|
|
|
+ height: 80rpx;
|
|
|
+ line-height: 80rpx;
|
|
|
+ margin: 0;
|
|
|
+ padding: 0;
|
|
|
+ font-size: 28rpx;
|
|
|
+ border-radius: 8rpx;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.bar-btn::after {
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+.bar-btn-preview {
|
|
|
+ width: 200rpx;
|
|
|
+ background: #fff;
|
|
|
+ color: #52c41a;
|
|
|
+ border: 2rpx solid #ddd;
|
|
|
+ margin-right: 16rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ line-height: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-eye {
|
|
|
+ margin-right: 6rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.bar-btn-save {
|
|
|
+ flex: 1;
|
|
|
+ background: #52c41a;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-overlay {
|
|
|
+ position: fixed;
|
|
|
+ left: 0;
|
|
|
+ top: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ z-index: 200;
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-end;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-mask {
|
|
|
+ position: absolute;
|
|
|
+ left: 0;
|
|
|
+ top: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
+}
|
|
|
+
|
|
|
+.picker-panel {
|
|
|
+ position: relative;
|
|
|
+ z-index: 1;
|
|
|
+ width: 100%;
|
|
|
+ max-height: 70vh;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 24rpx 24rpx 0 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.category-picker-panel {
|
|
|
+ max-height: 82vh;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-head {
|
|
|
+ position: relative;
|
|
|
+ height: 88rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border-bottom: 1rpx solid #f0f0f0;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-title {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-count {
|
|
|
+ position: absolute;
|
|
|
+ left: 24rpx;
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #52c41a;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-close {
|
|
|
+ position: absolute;
|
|
|
+ right: 24rpx;
|
|
|
+ font-size: 36rpx;
|
|
|
+ color: #999;
|
|
|
+ padding: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-search-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 16rpx 24rpx;
|
|
|
+ border-bottom: 1rpx solid #f5f5f5;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-search-box {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ height: 68rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 0 20rpx;
|
|
|
+ border: 2rpx solid #e8e8e8;
|
|
|
+ border-radius: 34rpx;
|
|
|
+ background: #fafafa;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-search-icon {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #999;
|
|
|
+ margin-right: 12rpx;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-search-input {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ height: 64rpx;
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-search-clear {
|
|
|
+ margin-left: 16rpx;
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #52c41a;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-selected-row {
|
|
|
+ width: 100%;
|
|
|
+ white-space: nowrap;
|
|
|
+ border-bottom: 1rpx solid #f5f5f5;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-selected-wrap {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ padding: 16rpx 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-selected-chip {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ max-width: 280rpx;
|
|
|
+ height: 52rpx;
|
|
|
+ padding: 0 12rpx 0 16rpx;
|
|
|
+ margin-right: 12rpx;
|
|
|
+ background: #f6ffed;
|
|
|
+ border: 1rpx solid #b7eb8f;
|
|
|
+ border-radius: 26rpx;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.chip-name {
|
|
|
+ max-width: 220rpx;
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #389e0d;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.chip-remove {
|
|
|
+ width: 32rpx;
|
|
|
+ height: 32rpx;
|
|
|
+ line-height: 30rpx;
|
|
|
+ text-align: center;
|
|
|
+ margin-left: 8rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-body {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
+ max-height: 46vh;
|
|
|
+ padding: 8rpx 24rpx 16rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-empty {
|
|
|
+ padding: 80rpx 0;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 22rpx 0;
|
|
|
+ border-bottom: 1rpx solid #f5f5f5;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-item:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+}
|
|
|
+
|
|
|
+.check-box {
|
|
|
+ width: 36rpx;
|
|
|
+ height: 36rpx;
|
|
|
+ border: 2rpx solid #ccc;
|
|
|
+ border-radius: 6rpx;
|
|
|
+ margin-right: 16rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.check-box.checked {
|
|
|
+ background: #52c41a;
|
|
|
+ border-color: #52c41a;
|
|
|
+}
|
|
|
+
|
|
|
+.check-mark {
|
|
|
+ color: #fff;
|
|
|
+ font-size: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-item-name {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #333;
|
|
|
+ line-height: 1.4;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-footer {
|
|
|
+ display: flex;
|
|
|
+ padding: 20rpx 24rpx;
|
|
|
+ padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
|
|
+ flex-shrink: 0;
|
|
|
+ border-top: 1rpx solid #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-btn {
|
|
|
+ flex: 1;
|
|
|
+ height: 80rpx;
|
|
|
+ line-height: 80rpx;
|
|
|
+ margin: 0;
|
|
|
+ font-size: 28rpx;
|
|
|
+ border-radius: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-btn::after {
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-btn.cancel {
|
|
|
+ margin-right: 20rpx;
|
|
|
+ background: #fff;
|
|
|
+ color: #333;
|
|
|
+ border: 2rpx solid #ddd;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-btn.confirm {
|
|
|
+ background: #52c41a;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-panel {
|
|
|
+ position: relative;
|
|
|
+ z-index: 1;
|
|
|
+ width: 90%;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ padding: 24rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-swiper {
|
|
|
+ height: 320rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-img {
|
|
|
+ width: 100%;
|
|
|
+ height: 320rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-close {
|
|
|
+ margin-top: 20rpx;
|
|
|
+ height: 72rpx;
|
|
|
+ line-height: 72rpx;
|
|
|
+ background: #52c41a;
|
|
|
+ color: #fff;
|
|
|
+ font-size: 28rpx;
|
|
|
+ border-radius: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-close::after {
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+</style>
|