Files
huangjingfen/pro_v3.5.1_副本/view/uniapp/pages/discover/discoverCreate/index.vue
apple 434aa8c69d feat(fsgx): 完成全部24项开发任务 Phase1-7
Phase1 后端核心:
- 新增 fsgx_v1.sql 迁移脚本(is_queue_goods/frozen_points/available_points/no_assess)
- SystemConfigServices 返佣设置扩展(周期人数/分档比例/范围/时机)
- StoreOrderCreateServices 周期循环佣金计算
- StoreOrderTakeServices 佣金发放后同步冻结积分
- StoreProductServices/StoreProduct 保存 is_queue_goods

Phase2 后端接口:
- GET /api/hjf/brokerage/progress 佣金周期进度
- GET /api/hjf/assets/overview 资产总览
- HjfPointsServices 每日 frozen_points 0.4‰ 释放定时任务
- PUT /adminapi/hjf/member/{uid}/no_assess 不考核接口
- GET /adminapi/hjf/points/release_log 积分日志接口

Phase3 前端清理:
- hjfCustom.js 路由精简(仅保留 points/log)
- hjfQueue.js/hjfMember.js API 清理/重定向至 CRMEB 原生接口
- pages.json 公排→推荐佣金/佣金记录/佣金规则

Phase4-5 前端改造:
- queue/status.vue 推荐佣金进度页整体重写
- 商品详情/订单确认/支付结果页文案与逻辑改造
- 个人中心/资产页/引导页/规则页文案改造
- HjfQueueProgress/HjfRefundNotice/HjfAssetCard 组件改造
- 推广中心嵌入佣金进度摘要
- hjfMockData.js 全量更新(公排字段→佣金字段)

Phase6 Admin 增强:
- 用户列表新增 frozen_points/available_points 列及不考核操作按钮
- hjfPoints.js USE_MOCK=false 对接真实积分日志接口

Phase7 配置文档:
- docs/fsgx-phase7-config-checklist.md 后台配置与全链路验收清单

Made-with: Cursor
2026-03-23 22:32:19 +08:00

698 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view :style="colorStyle">
<view>
<view class="w-full notice flex-y-center pl-30 fs-26"
v-show="(configData.community_integral_num > 0 || configData.community_exp_num > 0) && !id">
完成本次发布审核通过可得
<text class="font-num" v-show="configData.community_integral_num > 0">{{configData.community_integral_num}}积分</text>
<text class="font-num" v-show="configData.community_integral_num > 0 && configData.community_exp_num > 0">+</text>
<text class="font-num" v-show="configData.community_exp_num > 0">{{configData.community_exp_num}}经验值</text>奖励
</view>
<view class="px-30">
<view class="pt-30 grid-column-3 grid-gap-20rpx">
<view class="relative w-full h-214" v-for="(item, index) in formData.slider_image" :key="index"
@tap="proviewPic(item)">
<image :src="item" mode="aspectFill" class="w-full h-214 rd-16rpx"></image>
<view class="abs-rt w-32 h-32 del-pic flex-center" @tap.stop="DelPic(index)">
<text class="iconfont icon-ic_close text--w111-fff fs-20"></text>
</view>
<view class="abs-lt poster-badge flex-center fs-18" v-if="index == 0">封面</view>
</view>
<view
class="w-full h-214 flex-col flex-center upload bg--w111-f5f5f5 rd-16rpx text--w111-999"
@click="uploadpic"
v-if="!formData.video_url && formData.slider_image.length < 9"
>
<text class="iconfont icon-ic_camera fs-60"></text>
<text class="fs-28 lh-40rpx pt-20">添加照片</text>
</view>
<view
class="w-full h-214 flex-col flex-center upload bg--w111-f5f5f5 rd-16rpx text--w111-999"
@click="uploadVideo"
v-if="!formData.slider_image.length && !formData.video_url"
>
<text class="iconfont icon-icon_video fs-60"></text>
<text class="fs-28 lh-40rpx pt-20">添加视频</text>
</view>
<block v-if="formData.video_url">
<view class="w-full h-214 flex-col flex-center video-bg rd-16rpx relative" @tap="openVideoPopup">
<view class="abs-rt w-32 h-32" @tap.stop="delVideo()"></view>
<view class="video-player flex-center">
<text class="iconfont icon-ic_right2 fs-28 text--w111-fff relative"></text>
</view>
<text class="pt-22 fs-18 text--w111-fff">点击可预览视频</text>
</view>
<view class="w-full h-214 flex-col flex-center upload bg--w111-f5f5f5 rd-16rpx text--w111-999" @click="uploadPoster" v-if="!formData.image">
<text class="iconfont icon-ic_camera fs-60"></text>
<text class="fs-28 lh-40rpx pt-12">添加封面</text>
</view>
<view class="w-full h-214 flex-col flex-center upload bg--w111-f5f5f5 rd-16rpx relative text--w111-999" @click="uploadPoster" v-if="formData.image">
<image :src="formData.image" mode="aspectFill" class="w-full h-214 rd-16rpx"></image>
<view class="abs-lt poster-badge flex-center fs-18">封面</view>
<view class="poster-huan flex-center text--w111-fff fs-18">更换封面</view>
</view>
</block>
</view>
<view class="py-32 bb-e">
<input class="fs-30 fw-500" placeholder="填写标题会有更多赞噢~" placeholder-class="text--w111-999" v-model="formData.title" maxlength="20" />
</view>
<view class="pt-18 pb-46 bb-e textarea">
<textarea
class="w-full fs-28 h-280"
placeholder="分享你的体验和心得,你的文字我们都很喜欢~"
name="comment"
placeholder-class="text--w111-999"
v-model="formData.content"
:maxlength="600"
></textarea>
</view>
<view class="h-100 flex-between-center" @tap="openDrawer(0)">
<view class="flex-y-center">
<text class="iconfont icon-icon_talk_2-2"></text>
<view class="pl-8">添加话题</view>
</view>
<view class="fs-28 text--w111-999 flex-y-center" v-show="!topicSelectedList.length">
<text>去添加</text>
<text class="iconfont icon-ic_rightarrow"></text>
</view>
<view class="fs-28 flex-y-center" v-show="topicSelectedList.length">
<view class="w-428 line1 text-right">
<text class="pl-8" v-for="(item, index) in topicSelectedList" :key="index">#{{ item.name }}</text>
</view>
<text class="iconfont icon-ic_rightarrow text--w111-999"></text>
</view>
</view>
<view class="mt-30 flex-between-center" @tap="openDrawer(1)">
<view class="flex-y-center">
<text class="iconfont icon-icon_Link"></text>
<view class="pl-8">
关联商品
<!-- <text v-show="productNum">({{ productNum }})</text> -->
</view>
</view>
<view class="flex-y-center justify-end" v-show="productSelect.length">
<image class="w-60 h-60 rd-6rpx block mr-12" v-for="(item, index) in productSelect" :key="index" :src="item.image"></image>
<text class="iconfont icon-ic_rightarrow fs-32 text--w111-999"></text>
</view>
<view class="fs-28 text--w111-999 flex-y-center" v-show="!productSelect.length">
<text>请选择</text>
<text class="iconfont icon-ic_rightarrow"></text>
</view>
</view>
</view>
</view>
<view class="pb-safe">
<view class="h-200"></view>
</view>
<view class="fixed-lb w-full pb-safe bg--w111-fff z-100">
<view class="footer-box flex-center">
<view class="w-690 h-88 flex-center bg-color text--w111-fff fs-28 rd-44rpx" @tap="confirmSave">发布</view>
</view>
</view>
<base-drawer mode="bottom" :visible="visible" background-color="transparent" mask maskClosable @close="confirmProductDrawer">
<view class="w-full bg--w111-fff rd-t-40rpx py-32 relative">
<view class="close-btn flex-center" @tap="confirmProductDrawer">
<text class="iconfont icon-ic_close text--w111-666 fs-24"></text>
</view>
<view class="flex-center fs-30 text--w111-999">
<view :class="{ 'tab-active': tabActive == 0 }" @tap="changeTab(0, 'pay')">已购</view>
<view class="ml-70" :class="{ 'tab-active': tabActive == 1 }" @tap="changeTab(1, 'collect')">收藏</view>
<view class="ml-70" :class="{ 'tab-active': tabActive == 2 }" @tap="changeTab(2, 'visit')">浏览</view>
</view>
<view class="px-30 mt-50 flex-between-center">
<view class="flex-1 px-32 h-58 rd-30rpx bg--w111-f5f5f5 flex-y-center">
<text class="iconfont icon-ic_search text--w111-999 fs-28"></text>
<input type="text" class="flex-1 pl-20 fs-26" placeholder="输入关键字搜索" placeholder-class="text--w111-ccc" v-model="where.keyword" />
</view>
<text class="fs-28 pl-24" @tap="getSearch">搜索</text>
</view>
<view class="px-30">
<scroll-view scroll-y="true" @scrolltolower="getProductList" style="height: 780rpx">
<view class="flex-between-center" v-for="(item, index) in productList" :key="index">
<text
class="iconfont text--w111-ccc fs-36"
:class="item.isSelect ? 'icon-a-ic_CompleteSelect' : 'icon-ic_unselect'"
@tap="selectItem(item, index)"
></text>
<view class="flex-1 flex h-160 mt-30 pl-30">
<image class="w-160 h-160 rd-16rpx" :src="item.image" mode="aspectFill"></image>
<view class="h-160 flex-1 flex-col justify-between pl-30">
<view class="h-68 lh-34rpx fs-30 line2">{{ item.store_name }}</view>
<baseMoney :money="item.price" symbolSize="24" integerSize="40" decimalSize="24" weight></baseMoney>
</view>
</view>
</view>
<view v-if="!productList.length">
<emptyPage title="暂无商品,去看点别的吧~"></emptyPage>
</view>
</scroll-view>
</view>
<view class="mx-20 pb-safe bg--w111-fff">
<view class="mt-52 h-88 flex-center rd-44rpx bg-color fs-28 text--w111-fff mt-24" @tap="confirmProductDrawer">确定({{ productNum }}/5)</view>
</view>
</view>
</base-drawer>
<uni-popup ref="popup">
<view class="flex-center">
<view class="fixed-lt h-80 flex-y-center pl-20 z-999" :style="{ top: sysHeight + 'px' }" @tap="hideModal">
<text class="iconfont icon-ic_close fs-40 text--w111-fff"></text>
</view>
<!--#ifndef APP-PLUS-->
<video id="myVideo" autoplay loop muted :src="formData.video_url" :style="[videoStyle]"></video>
<!--#endif-->
<!--#ifdef APP-PLUS-->
<view v-html="videoHtml"></view>
<!--#endif-->
</view>
</uni-popup>
<canvas
canvas-id="canvas"
v-if="canvasStatus"
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px', position: 'absolute', left: '-100000px', top: '-100000px' }"
></canvas>
</view>
</template>
<script>
import colors from '@/mixins/color';
import store from '@/store';
import { mapState, mapGetters } from 'vuex';
import { toLogin } from '@/libs/login.js';
import { TOKENNAME, HTTP_REQUEST_URL } from '@/config/app.js';
import baseDrawer from '@/components/tui-drawer/tui-drawer.vue';
import emptyPage from '@/components/emptyPage.vue';
import { getProductslist } from '@/api/store.js';
import {
getTopicApi,
getProductApi,
communitySaveApi,
communityInfoApi,
communityUpdateApi,
getCommunityConfig ,
} from '@/api/community.js';
import { Debounce } from '@/utils/validate.js'
let app = getApp();
export default {
data() {
return {
sysHeight: app.globalData.sysHeight,
id: '',
canvasStatus: false,
visible: false,
tabActive: 0,
keyword: '',
topicList: [],
recommendTopic: [],
topicSelectedList: [],
productList: [],
productSelect: [],
loading: false,
where: {
page: 1,
limit: 20,
type: 'pay',
keyword: ''
},
formData: {
content_type: null, //内容类型1图文2视频
title: '',
content: '',
image: '', //视频封面
video_url: '',
slider_image: [],
topic_name: [],
product_id: []
},
upload_max: 50,
uploadUrl: `${HTTP_REQUEST_URL}/api/upload/video`,
videoContext: '',
productNum: 0,
topicNum: 0,
canvasWidth: '',
canvasHeight: '',
configData:{
community_exp_num:0, //经验
community_integral_num:0 //积分
},
};
},
components: { baseDrawer, emptyPage },
computed: {
...mapGetters(['isLogin']),
videoHtml() {
return `<video autoplay loop muted controls="controls" width="100%" height="870px"><source src="${this.formData.video_url}" type="video/mp4"></video>`;
},
videoStyle() {
return {
width: uni.getWindowInfo().windowWidth + 'px',
height: uni.getWindowInfo().windowHeight + 'px'
};
},
popupHeight(){
return uni.getWindowInfo().windowHeight * 0.8
},
},
mixins: [colors],
onShow() {
let that = this;
uni.$on("topicSave", data => {
if (data) {
that.topicSelectedList = data;
}
// 清除监听
uni.$off('topicSave');
})
},
onLoad(options) {
if (this.isLogin) {
this.getConfig();
if (options.id) {
this.id = options.id;
this.getInfo();
}
if (options.topic_name) {
// this.topicName = options.topic_name;
this.topicSelectedList.push({name:options.topic_name})
}
this.getProductList();
// #ifndef APP-PLUS
this.videoContext = uni.createVideoContext('myVideo', this);
// #endif
} else {
toLogin();
}
},
methods: {
getConfig(){
getCommunityConfig().then(res=>{
this.configData = res.data;
})
},
proviewPic(url){
uni.previewImage({
urls: this.formData.slider_image,
current: url
});
},
DelPic(index) {
this.formData.slider_image.splice(index, 1);
},
delVideo() {
this.formData.video_url = '';
},
openVideoPopup() {
this.$refs.popup.open();
this.videoContext = uni.createVideoContext('myVideo', this);
this.$nextTick(() => {
this.videoContext.play();
});
},
hideModal() {
this.$refs.popup.close();
},
uploadPoster() {
let that = this;
this.canvasStatus = true;
that.$util.uploadImageChange(
{ count: 1, url: 'upload/image' },
function (res) {
that.formData.image = res.data.url;
},
(res) => {
this.canvasStatus = false;
},
(res) => {
this.canvasWidth = res.w;
this.canvasHeight = res.h;
}
);
},
uploadpic() {
let that = this;
this.canvasStatus = true;
that.$util.uploadImageChange(
{ count: 9, url: 'upload/image' },
function (res) {
if (that.formData.slider_image.length == 9)
return that.$util.Tips({
title: '最多上传9张'
});
that.formData.slider_image.push(res.data.url);
},
(res) => {
this.canvasStatus = false;
},
(res) => {
this.canvasWidth = res.w;
this.canvasHeight = res.h;
}
);
},
// 上传视频
uploadVideo() {
let that = this;
uni.chooseVideo({
sourceType: ['camera', 'album'],
success: (res) => {
uni.showLoading({
title: '视频上传中'
});
if (Math.ceil(res.size / 1024) < that.upload_max * 1024) {
uni.uploadFile({
url: that.uploadUrl,
filePath: res.tempFilePath,
name: 'file',
header: {
// #ifdef MP
'Content-Type': 'multipart/form-data',
// #endif
[TOKENNAME]: 'Bearer ' + store.state.app.token
},
success: (uploadFileRes) => {
uni.hideLoading();
let data = JSON.parse(uploadFileRes.data);
if (data.status == 200) {
that.formData.video_url = data.data.url;
} else {
return this.$util.Tips({
title: data.msg
});
}
},
complete: function (err) {
uni.hideLoading();
}
});
} else {
uni.hideLoading();
uni.showModal({
title: '提示',
showCancel: false,
content: `视频超出限制${that.upload_max}MB`
});
}
},
fail: (err) => {
that.$util.Tips({
title: err
});
}
});
},
confirmProductDrawer() {
this.visible = false;
},
openDrawer(type) {
if (type == 1) {
if (this.id) {
this.productList.map((itemA) => {
this.formData.product_id.forEach((itemB) => {
if (itemB === itemA.product_id) {
this.$set(itemA, 'isSelect', true);
}
});
});
}
this.visible = !this.visible;
} else {
let that = this;
// uni.$emit("topicOpen", that.topicSelectedList)
let jsonStr = '';
if(this.topicSelectedList.length){
jsonStr = JSON.stringify(this.topicSelectedList)
}
uni.navigateTo({
url: '/pages/discover/discoverCreate/topic?data=' + jsonStr
})
// this.showTopic = !this.showTopic;
// if (this.showTopic) {
// this.topicKeyword = '#';
// this.focus = true;
// }
}
},
getProductList() {
if (this.loading) return;
this.loading = true;
getProductApi(this.where)
.then((res) => {
res.data.map((item) => {
this.$set(item, 'isSelect', false);
this.formData.product_id.forEach((itemB) => {
if (itemB === item.product_id) {
this.$set(item, 'isSelect', true);
}
});
});
let list = res.data;
let loading = list.length < this.where.limit;
this.productList = this.productList.concat(list);
this.where.page++;
this.loading = loading;
})
.catch((err) => {
this.loading = false;
return this.$util.Tips({
title: err
});
});
},
getSearch() {
this.loading = false;
this.where.page = 1;
this.productList = [];
this.getProductList();
},
changeTab(val, type) {
this.tabActive = val;
this.where.type = type;
this.getSearch();
},
selectItem(item) {
if (item.isSelect) {
item.isSelect = false;
if (this.formData.product_id.includes(item.product_id)) {
let i = this.productSelect.findIndex((o) => o.id == item.product_id);
this.productSelect.splice(i, 1);
this.formData.product_id.splice(i, 1);
this.productNum--;
}
} else {
if (this.productSelect.length == 5)
return this.$util.Tips({
title: '最多选择5件商品'
});
item.isSelect = true;
let arr = [];
this.productSelect.forEach((k) => {
arr.push(k.id);
});
if (!arr.includes(item.product_id)) {
this.productNum++;
this.productSelect.push(item);
this.formData.product_id.push(item.product_id);
}
}
},
confirmSave() {
// if(!this.formData.title) return this.$util.Tips({
// title: '请输入标题'
// })
if (!this.formData.content)
return this.$util.Tips({
title: '请输入内容'
});
if (!this.formData.video_url && !this.formData.slider_image.length)
return this.$util.Tips({
title: '请上传要分享的图片'
});
if (this.formData.video_url && !this.formData.image)
return this.$util.Tips({
title: '请上传海报'
});
this.formData.topic_name = this.topicSelectedList.map(item => item.name);
this.formData.content_type = this.formData.slider_image.length ? 1 : 2;
uni.showLoading({
title: '正在提交...'
});
let funApi = '';
funApi = this.id ? communityUpdateApi(this.id, this.formData) : communitySaveApi(this.formData);
funApi
.then((res) => {
uni.hideLoading();
let exp = this.configData.community_exp_num;
let integral = this.configData.community_integral_num;
return this.$util.Tips(
{
title: res.msg
},
{
tab: 4,
url: '/pages/discoverIndex/index'
}
);
})
.catch((err) => {
uni.hideLoading();
return this.$util.Tips({
title: err
});
});
},
getInfo() {
communityInfoApi(this.id)
.then((res) => {
let info = res.data;
let keys = Object.keys(this.formData);
keys.map((i) => {
this.formData[i] = info[i];
});
if (this.formData.product_id == null) {
this.formData.product_id = [];
}
if (res.data.topic.length) {
this.topicSelectedList = res.data.topic;
}
if (res.data.product && res.data.product.length) {
this.productSelect = res.data.product;
}
this.productNum = res.data.product.length;
this.topicNum = res.data.topic.length;
})
.catch((err) => {
return this.$util.Tips({
title: err
});
});
},
}
};
</script>
<style>
page {
background-color: #ffffff;
}
</style>
<style lang="scss">
.notice {
height: 84rpx;
background: var(--view-minorColorT);
}
.del-pic {
background-color: #999;
border-radius: 0 16rpx 0 16rpx;
}
.close-btn {
position: absolute;
right: 28rpx;
top: 28rpx;
width: 36rpx;
height: 36rpx;
border-radius: 50%;
background-color: #eee;
}
.bb-e {
border-bottom: 1rpx solid #eee;
}
.b-e {
border: 1rpx solid #eee;
}
.b-d {
border: 1rpx solid #ddd;
}
.filter-cell ~ .filter-cell{
border-top: 1px solid #f5f5f5;
}
.textarea {
height: 100%;
}
.label-item {
color: var(--view-theme);
background: var(--view-minorColorT);
border: 1rpx solid transparent;
}
.icon-a-ic_CompleteSelect {
color: var(--view-theme);
}
.pro-cell {
padding: 36rpx 30rpx;
}
.add-btn {
width: 690rpx;
height: 88rpx;
background: var(--view-theme);
border-radius: 50rpx;
background-color: var(--view-theme);
margin-bottom: 32rpx;
}
.tab-active {
font-weight: 500;
color: #333;
position: relative;
&:after {
content: '';
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: -14rpx;
width: 40rpx;
height: 5rpx;
background: var(--view-theme);
border-radius: 4rpx;
}
}
.poster-badge {
width: 54rpx;
height: 29rpx;
background: var(--view-theme);
color: #fff;
border-radius: 16rpx 0rpx 16rpx 0rpx;
}
.video-bg {
background-image: url('@/static/img/video_bg.png');
background-repeat: no-repeat;
background-size: cover;
}
.video-player {
width: 50rpx;
height: 50rpx;
border-radius: 50%;
background: rgba(0, 0, 0, 0.5);
.iconfont {
left: 2rpx;
}
}
.poster-huan {
position: absolute;
bottom: 0;
width: 100%;
height: 40rpx;
background: rgba(0, 0, 0, 0.5);
border-radius: 0rpx 0rpx 16rpx 16rpx;
}
.canvas-box {
position: absolute;
left: -100px;
top: -100px;
width: 750px;
height: 1624px;
}
.footer-box {
height: 126rpx;
.w-690 {
width: 690rpx;
}
}
.mt-62{
margin-top: 62rpx;
}
.close-icon{
top: 36rpx;
right: 32rpx;
}
.overflow-scroll{
overflow-y: scroll;
}
</style>