feat(community,faq): support dynamic community topics, notice, and FAQ center

这个提交包含在:
xcbyy
2026-10-10 10:04:49 +08:00
父节点 82ba59f373
当前提交 8845fbe7cb
共有 5 个文件被更改,包括 383 次插入 和 16 次删除

查看文件

@@ -1,7 +1,7 @@
<template>
<div class="container">
<Breadcrumb :items="['menu.safety', 'menu.safety.platformConfig']" />
<a-card class="general-card" :title="'平台客服与官方指引/协议运营配置'">
<a-card class="general-card" :title="'平台运营与内容指引配置中心'">
<template #extra>
<a-button type="primary" :loading="loading" @click="fetchConfigs">
<template #icon><icon-refresh /></template>
@@ -11,7 +11,7 @@
<a-tabs default-active-key="customer_service" type="rounded">
<!-- Tab 1: 官方客服配置 -->
<a-tab-pane key="customer_service" title="官方客服与热线配置">
<a-tab-pane key="customer_service" title="官方客服与热线">
<div class="tab-pane-content">
<a-alert type="info" style="margin-bottom: 20px">
在此配置小程序个人中心“官方在线客服”弹层的联系电话与专属微信号,支持即时保存生效。
@@ -53,7 +53,7 @@
</a-tab-pane>
<!-- Tab 2: 官方安全履约公告 -->
<a-tab-pane key="safety_notice" title="安全履约公告指引">
<a-tab-pane key="safety_notice" title="履约安全公告">
<div class="tab-pane-content">
<a-alert type="warning" style="margin-bottom: 20px">
在此配置小程序个人中心“平台消息与通知”弹层的履约安全指引与风险防范须知。
@@ -87,7 +87,7 @@
</a-tab-pane>
<!-- Tab 3: 用户服务协议与规范 -->
<a-tab-pane key="platform_protocol" title="平台规范与服务协议">
<a-tab-pane key="platform_protocol" title="平台规范协议">
<div class="tab-pane-content">
<a-alert type="success" style="margin-bottom: 20px">
在此配置“平台规范与服务协议”弹层的协议条款与合规内容,严格遵循微信内容安全风控标准。
@@ -122,6 +122,123 @@
</a-form>
</div>
</a-tab-pane>
<!-- Tab 4: 友圈热门话题与社区置顶公告 (P4) -->
<a-tab-pane key="community_config" title="友圈话题与头条公告">
<div class="tab-pane-content">
<a-alert type="info" style="margin-bottom: 20px">
在此管理友圈广场顶部头条横幅公告,以及移动端发布动态时的推荐热门话题库(支持HOT高亮)。
</a-alert>
<!-- 友圈置顶公告 -->
<a-card title="友圈顶部置顶头条公告" :bordered="false" style="margin-bottom: 24px; background: var(--color-fill-2)">
<a-form :model="commNoticeForm" layout="vertical" style="max-width: 750px">
<a-form-item label="头条公告标题">
<a-input v-model="commNoticeForm.valueJson.title" placeholder="如:友圈同城互动与公约置顶" />
</a-form-item>
<a-form-item label="活动角标/标签">
<a-input v-model="commNoticeForm.valueJson.tag" placeholder="如:置顶推荐 / 官方公告" />
</a-form-item>
<a-form-item label="公告详情内容">
<a-textarea v-model="commNoticeForm.valueJson.content" placeholder="输入公告内容" :auto-size="{ minRows: 3, maxRows: 6 }" />
</a-form-item>
<a-form-item label="启用横幅置顶展示">
<a-switch :checked-value="1" :unchecked-value="0" v-model="commNoticeForm.status" />
</a-form-item>
<a-form-item>
<a-button type="primary" :loading="saving" @click="saveConfig('community_notice', commNoticeForm)">
保存头条公告
</a-button>
</a-form-item>
</a-form>
</a-card>
<!-- 热门话题列表 -->
<a-card title="热门推荐话题库 (移动端发帖即选)" :bordered="false" style="background: var(--color-fill-2)">
<template #extra>
<a-button type="outline" size="small" @click="addTopicItem">
<template #icon><icon-plus /></template>
新增话题
</a-button>
</template>
<a-table :data="topicsList" :pagination="false" row-key="id">
<template #columns>
<a-table-column title="话题标签" :width="200">
<template #cell="{ record }">
<a-input v-model="record.name" placeholder="话题名称如: 三里屯探店" />
</template>
</a-table-column>
<a-table-column title="HOT高亮" :width="120" align="center">
<template #cell="{ record }">
<a-switch v-model="record.hot" />
</template>
</a-table-column>
<a-table-column title="讨论热度基数" :width="160">
<template #cell="{ record }">
<a-input-number v-model="record.postsCount" :min="0" />
</template>
</a-table-column>
<a-table-column title="操作" :width="100" align="center">
<template #cell="{ rowIndex }">
<a-button type="text" status="danger" size="small" @click="removeTopicItem(rowIndex)">
删除
</a-button>
</template>
</a-table-column>
</template>
</a-table>
<div style="margin-top: 16px">
<a-button type="primary" :loading="saving" @click="saveTopicsConfig">
保存热门话题库
</a-button>
</div>
</a-card>
</div>
</a-tab-pane>
<!-- Tab 5: 帮助中心与常见问题 FAQ (P5) -->
<a-tab-pane key="faq_config" title="帮助中心与常见问题">
<div class="tab-pane-content">
<a-alert type="success" style="margin-bottom: 20px">
在此维护小程序【常见问题与帮助中心】问答列表,解决买家组局履约、核销码流程、资金退款等高频疑惑。
</a-alert>
<a-card title="常见问题 FAQ 问答列表" :bordered="false" style="background: var(--color-fill-2)">
<template #extra>
<a-button type="outline" size="small" @click="addFaqItem">
<template #icon><icon-plus /></template>
新增问答
</a-button>
</template>
<div v-for="(item, idx) in faqList" :key="item.id || idx" style="margin-bottom: 16px; padding: 16px; background: var(--color-bg-2); border-radius: 6px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<a-space>
<a-tag color="blue">问答 #{{ idx + 1 }}</a-tag>
<a-input v-model="item.category" placeholder="分类如: 新手预约/资金退款" style="width: 180px;" />
</a-space>
<a-button type="text" status="danger" size="small" @click="removeFaqItem(idx)">删除问答</a-button>
</div>
<a-form layout="vertical">
<a-form-item label="问题 (Title)">
<a-input v-model="item.question" placeholder="如:什么是4位纯数字核销码?" />
</a-form-item>
<a-form-item label="回答解答 (Answer)">
<a-textarea v-model="item.answer" placeholder="输入权威解答内容" :auto-size="{ minRows: 2, maxRows: 5 }" />
</a-form-item>
</a-form>
</div>
<div style="margin-top: 16px">
<a-button type="primary" :loading="saving" @click="saveFaqConfig">
保存帮助中心 FAQ
</a-button>
</div>
</a-card>
</div>
</a-tab-pane>
</a-tabs>
</a-card>
</div>
@@ -175,6 +292,50 @@ const protoForm = reactive({
}
});
// P4 友圈配置
const commNoticeForm = reactive({
name: '友圈社区置顶头条公告',
description: '友圈广场顶部置顶横幅公告与社区公约',
status: 1,
valueJson: {
title: '友圈同城互动与公约置顶',
content: '欢迎来到同城搭子友圈!文明交友,发布真实组局诉求;平台全天候对动态与评论实施微信官方内容安全过滤,严禁广告引流及私下交易。',
tag: '置顶推荐',
showInBanner: true
}
});
const topicsList = ref<any[]>([]);
const addTopicItem = () => {
topicsList.value.push({
id: `top_${Date.now()}`,
name: '新同城话题',
hot: false,
postsCount: 100
});
};
const removeTopicItem = (index: number) => {
topicsList.value.splice(index, 1);
};
// P5 帮助中心 FAQ
const faqList = ref<any[]>([]);
const addFaqItem = () => {
faqList.value.push({
id: `faq_${Date.now()}`,
category: '新手预约与组局',
question: '新常见问题',
answer: '在此输入权威官方解答...'
});
};
const removeFaqItem = (index: number) => {
faqList.value.splice(index, 1);
};
const fetchConfigs = async () => {
loading.value = true;
try {
@@ -199,6 +360,15 @@ const fetchConfigs = async () => {
protoForm.description = item.description;
protoForm.status = item.status;
protoForm.valueJson = { ...protoForm.valueJson, ...(item.valueJson || {}) };
} else if (item.configKey === 'community_notice') {
commNoticeForm.name = item.name;
commNoticeForm.description = item.description;
commNoticeForm.status = item.status;
commNoticeForm.valueJson = { ...commNoticeForm.valueJson, ...(item.valueJson || {}) };
} else if (item.configKey === 'community_topics') {
topicsList.value = Array.isArray(item.valueJson) ? item.valueJson : [];
} else if (item.configKey === 'faq_list') {
faqList.value = Array.isArray(item.valueJson) ? item.valueJson : [];
}
});
}
@@ -241,6 +411,24 @@ const saveNoticeConfig = () => {
saveConfig('safety_notice', noticeForm);
};
const saveTopicsConfig = () => {
saveConfig('community_topics', {
name: '友圈热门推荐话题库',
description: '友圈广场发帖推荐话题与热门标签',
status: 1,
valueJson: topicsList.value
});
};
const saveFaqConfig = () => {
saveConfig('faq_list', {
name: '帮助中心与常见问题FAQ',
description: '小程序端帮助中心常见问答与指引',
status: 1,
valueJson: faqList.value
});
};
onMounted(() => {
fetchConfigs();
});

查看文件

@@ -21,6 +21,12 @@
</view>
</view>
<!-- 友圈置顶头条公告 (根据后台配置动态展示) -->
<view class="community-notice-bar" v-if="communityNotice && communityNotice.title">
<view class="notice-badge">{{ communityNotice.tag || '置顶公告' }}</view>
<text class="notice-title-text">{{ communityNotice.title }}:{{ communityNotice.content }}</text>
</view>
<!-- 分类筛选栏 (高能矢量Tab标签与标准HOT角标) -->
<view class="feed-tabs-wrap">
<scroll-view class="feed-scroll" scroll-x :show-scrollbar="false">
@@ -299,11 +305,20 @@ import SkeletonScreen from '@/components/SkeletonScreen.vue';
import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
import { getNavBarInfo } from '@/common/nav.js';
import { getPosts, createPost, likePost, getPostComments, addPostComment, applyPostParty, uploadImage } from '@/common/api.js';
import { getPosts, createPost, likePost, getPostComments, addPostComment, applyPostParty, uploadImage, getPlatformConfigs } from '@/common/api.js';
const isLoading = ref(true);
const currentThemeKey = ref(getCurrentThemeKey());
const navInfo = ref(getNavBarInfo());
const communityNotice = ref(null);
const availableTopics = ref([
'三里屯探店',
'铜锅涮肉AA',
'国服上分车队',
'饭搭子招募',
'工体纯K麦霸',
'同城搭子招募'
]);
const navStyle = computed(() => {
return {
@@ -334,6 +349,7 @@ onMounted(() => {
applyThemeToNativeUI(currentThemeKey.value);
uni.$on('theme-change', onThemeChanged);
loadInitialCommunity();
loadPlatformCommunityConfigs();
});
onShow(() => {
@@ -342,6 +358,7 @@ onShow(() => {
applyThemeToNativeUI(currentThemeKey.value);
uni.hideTabBar({ animation: false, fail: () => {} });
loadRemotePosts();
loadPlatformCommunityConfigs();
});
onUnmounted(() => {
@@ -359,6 +376,22 @@ const loadRemotePosts = async () => {
}
};
const loadPlatformCommunityConfigs = async () => {
try {
const configs = await getPlatformConfigs();
if (configs && typeof configs === 'object') {
if (configs.community_notice && configs.community_notice.title) {
communityNotice.value = configs.community_notice;
}
if (Array.isArray(configs.community_topics) && configs.community_topics.length > 0) {
availableTopics.value = configs.community_topics.map((t) => t.name || t);
}
}
} catch (e) {
console.warn('[Community] 获取平台话题公告异常降级:', e);
}
};
const feedTabs = ref([
{ key: 'all', name: '全部推荐', icon: ICONS.gameAll, hot: false },
@@ -588,15 +621,6 @@ const publishCategories = [
{ key: 'highlight', name: '生活随笔' }
];
const availableTopics = [
'三里屯探店',
'铜锅涮肉AA',
'王者上分',
'饭搭子招募',
'KTV麦霸',
'周末微醺'
];
const newPostForm = ref({
category: 'partner',
content: '',
@@ -865,6 +889,36 @@ const submitPublishPost = async () => {
}
}
.community-notice-bar {
display: flex;
align-items: center;
background: rgba(99, 102, 241, 0.12);
border: 1px solid rgba(99, 102, 241, 0.3);
border-radius: 12rpx;
padding: 12rpx 20rpx;
margin-bottom: 20rpx;
gap: 12rpx;
.notice-badge {
font-size: 20rpx;
font-weight: 700;
color: #FFFFFF;
background: linear-gradient(135deg, #F43F5E, #FB7185);
padding: 2rpx 12rpx;
border-radius: 6rpx;
flex-shrink: 0;
}
.notice-title-text {
font-size: 22rpx;
color: #CBD5E1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
}
.feed-tabs-wrap {
margin-bottom: 24rpx;
width: 100%;

查看文件

@@ -129,6 +129,11 @@
<text class="row-label">平台消息与通知</text>
<image class="row-arrow-svg" :src="ICONS.chevronRight" mode="aspectFit" />
</view>
<view class="list-row btn-active" @click="openFaq">
<image class="row-svg" :src="ICONS.messageSquare" mode="aspectFit" />
<text class="row-label">帮助中心与常见问题</text>
<image class="row-arrow-svg" :src="ICONS.chevronRight" mode="aspectFit" />
</view>
<view class="list-row btn-active" @click="openAbout">
<image class="row-svg" :src="ICONS.shield" mode="aspectFit" />
<text class="row-label">平台规范与服务协议</text>
@@ -674,6 +679,34 @@ const openNotice = () => {
});
};
const openFaq = () => {
const list = platformConfigs.value.faq_list || [];
if (Array.isArray(list) && list.length > 0) {
const questions = list.map((item, idx) => `${idx + 1}. ${item.question}`);
uni.showActionSheet({
itemList: questions,
success: (res) => {
const item = list[res.tapIndex];
if (item) {
uni.showModal({
title: item.question,
content: `【${item.category || '常见问题'}】\n${item.answer}`,
showCancel: false,
confirmText: '我知道了',
confirmColor: '#6366F1'
});
}
}
});
} else {
uni.showModal({
title: '帮助中心与常见问题',
content: '1. 什么是4位核销码?\n到达活动现场后出示给陪伴师或房主签到核销,保障真实履约。\n2. 如何申请退款?\n在未接单或未核销前可随时全额退款,或发起争议仲裁由官方判定。',
showCancel: false
});
}
};
const openAbout = () => {
const proto = platformConfigs.value.platform_protocol || {};
if (privacyModalRef.value && privacyModalRef.value.show) {

查看文件

@@ -669,12 +669,58 @@ async function seedInitialDataIfEmpty(conn: any): Promise<void> {
content: '1. 账号使用规范:用户注册与使用陪玩服务须符合国家法律法规,严禁利用平台从事涉黄、赌博、欺诈等违规行为。\n2. 资金与交易保障:平台提供全额担保交易,订单完成前款项由官方账户托管,核销确认后自动结算至大神钱包。\n3. 争议与售后:若遇服务不符或搭子违约,可随时提交争议仲裁由运营平台人工介入裁决。'
});
const communityTopicsVal = JSON.stringify([
{ id: 'top_1', name: '三里屯探店', hot: true, postsCount: 1280 },
{ id: 'top_2', name: '铜锅涮肉AA', hot: true, postsCount: 890 },
{ id: 'top_3', name: '工体纯K麦霸', hot: false, postsCount: 650 },
{ id: 'top_4', name: '国服上分车队', hot: true, postsCount: 2310 },
{ id: 'top_5', name: '望京跳海微醺', hot: false, postsCount: 420 },
{ id: 'top_6', name: '同城搭子招募', hot: true, postsCount: 3100 }
]);
const communityNoticeVal = JSON.stringify({
title: '友圈同城互动与公约置顶',
content: '欢迎来到同城搭子友圈!文明交友,发布真实组局诉求;平台全天候对动态与评论实施微信官方内容安全过滤,严禁广告引流及私下交易。',
tag: '置顶推荐',
showInBanner: true
});
const faqListVal = JSON.stringify([
{
id: 'faq_1',
category: '新手预约与组局',
question: '什么是4位纯数字核销码?',
answer: '为确保搭子碰面和订单真实履约,下单成功或报名组局后系统自动生成专属4位防伪数字码,到达公开活动现场后出示给陪伴师或房主签到核销。'
},
{
id: 'faq_2',
category: '新手预约与组局',
question: 'AA制拼桌与房主请客有什么区别?',
answer: '【AA制拼桌】由参与人员平摊费用,报名时支付人均分摊金额;【房主请客/项目制】由发起房主全额买单,且房主可随时追加赏金或提高出价。'
},
{
id: 'faq_3',
category: '资金与退款',
question: '如何申请退款与争议仲裁?',
answer: '在订单【待接单】或【待付款】状态可随时取消全额退款;在【服务中】或遇违约可在订单详情点击【发起争议仲裁】,由平台官方运营人工介入判定。'
},
{
id: 'faq_4',
category: '大神与收益',
question: '成为陪玩大神有什么资质要求?',
answer: '需在小程序个人中心提交实名认证与技能资质,通过平台运营团队人工审核后,即可解锁接单工作台与附近雷达扫单功能。'
}
]);
await conn.query(`
INSERT INTO platform_configs (config_key, name, value_json, description, status)
VALUES
('customer_service', '官方在线客服与服务热线', ?, '小程序端个人中心客服联系方式', 1),
('safety_notice', '安全履约须知与官方公告', ?, '小程序端官方指引与安全红线弹窗', 1),
('platform_protocol', '平台服务协议与合规规范', ?, '小程序端服务协议与隐私政策详情', 1);
`, [customerServiceVal, safetyNoticeVal, protocolVal]);
('platform_protocol', '平台服务协议与合规规范', ?, '小程序端服务协议与隐私政策详情', 1),
('community_topics', '友圈热门推荐话题库', ?, '友圈广场发帖推荐话题与热门标签', 1),
('community_notice', '友圈社区置顶头条公告', ?, '友圈广场顶部置顶横幅公告与社区公约', 1),
('faq_list', '帮助中心与常见问题FAQ', ?, '小程序端帮助中心常见问答与指引', 1);
`, [customerServiceVal, safetyNoticeVal, protocolVal, communityTopicsVal, communityNoticeVal, faqListVal]);
}
}

查看文件

@@ -80,4 +80,50 @@ describe('平台通用配置与合规管理 TDD 自动化测试', () => {
expect(merged.customer_service.hotline).toBe('400-123-4567');
expect(merged.safety_notice.title).toBe('官方公告');
});
it('6. 友圈热门话题与置顶公告配置校验 (P4)', () => {
const defaultTopics = [
{ name: '#深夜电台', count: '1.2w+', hot: true },
{ name: '#周末密室剧本杀', count: '8.5k', hot: true },
{ name: '#王者峡谷车队', count: '2.1w+', hot: false }
];
expect(defaultTopics.length).toBe(3);
expect(defaultTopics.filter(t => t.hot).length).toBe(2);
const notice = {
tag: '头条',
title: '友圈同城搭子发帖公约:严禁发布广告与不文明交友行为'
};
const secResult = secService.checkText(notice.title);
expect(secResult.pass).toBe(true);
const badNotice = {
tag: '广告',
title: '添加客服微信提供上门特殊服务'
};
const badSec = secService.checkText(badNotice.title);
expect(badSec.pass).toBe(false);
expect(badSec.code).toBe(87014);
});
it('7. 帮助中心与常见问题 FAQ 配置校验 (P5)', () => {
const faqList = [
{
question: '如何成为认证陪伴师/陪玩大神?',
answer: '进入【我的】->【入驻申请】,提交实名认证与技能资质,平台将在 24 小时内完成人工审核。',
category: '陪伴师相关'
},
{
question: '遇到订单纠纷如何申请平台仲裁?',
answer: '在【订单详情】中点击【申请仲裁退款】,客服与平台专员将在 2 小时内介入调解。',
category: '订单与退款'
}
];
expect(faqList.length).toBe(2);
expect(faqList[0].category).toBe('陪伴师相关');
const check = secService.checkText(JSON.stringify(faqList));
expect(check.pass).toBe(true);
});
});