预约规则.

This commit is contained in:
18631081161 2025-12-20 23:19:40 +08:00
parent f4a2826bb8
commit 8529fd1575
13 changed files with 546 additions and 38 deletions

View File

@ -256,6 +256,48 @@
</el-form>
</el-tab-pane>
<el-tab-pane label="预约登记规则" name="booking_rules">
<div class="booking-rules-container">
<el-alert
title="为每个预约表单页面配置登记规则,规则将显示在小程序预约页面顶部"
type="info"
:closable="false"
style="margin-bottom: 20px;" />
<el-collapse v-model="activeBookingRules">
<el-collapse-item
v-for="item in serviceTypeOptions"
:key="item.value"
:name="item.value"
:title="item.label">
<el-form label-width="100px">
<el-form-item label="规则说明">
<el-input
v-model="bookingRulesConfig[item.value]"
type="textarea"
:rows="6"
:placeholder="`请输入${item.label}的预约登记规则`"
maxlength="2000"
show-word-limit />
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" @click="saveBookingRule(item.value)" :loading="savingRule === item.value">
保存此规则
</el-button>
</el-form-item>
</el-form>
</el-collapse-item>
</el-collapse>
<div style="margin-top: 20px;">
<el-button type="primary" @click="saveAllBookingRules" :loading="saving">
<el-icon><Check /></el-icon>
保存全部规则
</el-button>
</div>
</div>
</el-tab-pane>
</el-tabs>
</el-card>
@ -281,6 +323,31 @@ const contactFormRef = ref(null)
const agreementFormRef = ref(null)
const inviteFormRef = ref(null)
//
const activeBookingRules = ref([])
const savingRule = ref('')
const bookingRulesConfig = ref({})
//
const serviceTypeOptions = [
{ value: 'flight', label: '全球机票代理' },
{ value: 'hotel', label: '全球酒店预定' },
{ value: 'lounge', label: '全球机场贵宾室服务' },
{ value: 'airport_transfer', label: '机场接/送机服务' },
{ value: 'unaccompanied_minor', label: '无成人陪伴儿童代办' },
{ value: 'train', label: '高铁票代订' },
{ value: 'telemedicine', label: '远程医疗问诊代理服务' },
{ value: 'special_passenger', label: '特殊(特护)旅客定制服务代办' },
{ value: 'pet_transport', label: '宠物托运代理' },
{ value: 'guide_translation', label: '西班牙语专业导游/翻译服务' },
{ value: 'visa', label: '签证咨询' },
{ value: 'exhibition', label: '墨西哥展会咨询与协办服务' },
{ value: 'air_logistics', label: '跨境航空物流/快递一站式服务' },
{ value: 'sea_freight', label: '海运/清关一站式服务' },
{ value: 'travel_planning', label: '旅游线路规划/咨询' },
{ value: 'insurance', label: '跨境出行意外保险/国际财产保险咨询' }
]
// Original configs for reset
const originalConfigs = reactive({
appearance: {},
@ -431,6 +498,9 @@ const loadConfigs = async () => {
}
})
}
// 使
await loadBookingRules()
} catch (error) {
console.error('Load configs error:', error)
ElMessage.error('加载配置失败')
@ -445,6 +515,21 @@ const refreshConfigs = async () => {
ElMessage.success('配置已刷新')
}
//
const loadBookingRules = async () => {
try {
const response = await api.get('/api/v1/admin/booking-rules')
if (response.data.code === 0) {
const rules = response.data.data || []
rules.forEach(rule => {
bookingRulesConfig.value[rule.serviceType] = rule.rules || ''
})
}
} catch (error) {
console.error('Load booking rules error:', error)
}
}
// Save appearance config
const saveAppearanceConfig = async () => {
if (!appearanceFormRef.value) return
@ -629,6 +714,40 @@ const resetInviteConfig = () => {
inviteFormRef.value?.clearValidate()
}
//
const saveBookingRule = async (serviceType) => {
savingRule.value = serviceType
try {
await api.put(`/api/v1/admin/booking-rules/${serviceType}`, {
rules: bookingRulesConfig.value[serviceType] || ''
})
ElMessage.success('规则保存成功')
} catch (error) {
console.error('Save booking rule error:', error)
ElMessage.error(error.response?.data?.message || '保存规则失败')
} finally {
savingRule.value = ''
}
}
//
const saveAllBookingRules = async () => {
saving.value = true
try {
const rules = serviceTypeOptions.map(item => ({
serviceType: item.value,
rules: bookingRulesConfig.value[item.value] || ''
}))
await api.post('/api/v1/admin/booking-rules/batch', { rules })
ElMessage.success('全部规则保存成功')
} catch (error) {
console.error('Save all booking rules error:', error)
ElMessage.error(error.response?.data?.message || '保存规则失败')
} finally {
saving.value = false
}
}
// Clear images
const clearLogo = async () => {
try {
@ -741,6 +860,8 @@ const handleTabChange = (tabName) => {
contactFormRef.value?.clearValidate()
} else if (tabName === 'agreement') {
agreementFormRef.value?.clearValidate()
} else if (tabName === 'booking_rules') {
// tab
}
}

View File

@ -176,33 +176,6 @@
<el-input v-model="form.titlePt" placeholder="Por favor, insira o título em português" />
</el-form-item>
<el-divider content-position="left">多语言描述</el-divider>
<el-form-item label="中文描述" prop="descriptionZh">
<el-input
v-model="form.descriptionZh"
type="textarea"
:rows="3"
placeholder="请输入中文描述"
/>
</el-form-item>
<el-form-item label="英文描述" prop="descriptionEn">
<el-input
v-model="form.descriptionEn"
type="textarea"
:rows="3"
placeholder="Please enter English description"
/>
</el-form-item>
<el-form-item label="葡语描述" prop="descriptionPt">
<el-input
v-model="form.descriptionPt"
type="textarea"
:rows="3"
placeholder="Por favor, insira a descrição em português"
/>
</el-form-item>
<el-divider content-position="left">其他信息</el-divider>
<el-form-item label="服务图片" prop="image">
@ -313,9 +286,6 @@ const defaultForm = {
titleZh: '',
titleEn: '',
titlePt: '',
descriptionZh: '',
descriptionEn: '',
descriptionPt: '',
image: '',
sortOrder: 0,
status: 'active'
@ -439,9 +409,6 @@ function handleEdit(row) {
titleZh: row.titleZh,
titleEn: row.titleEn,
titlePt: row.titlePt,
descriptionZh: row.descriptionZh || '',
descriptionEn: row.descriptionEn || '',
descriptionPt: row.descriptionPt || '',
image: row.image || '',
sortOrder: row.sortOrder || 0,
status: row.status
@ -491,9 +458,6 @@ async function handleSubmit() {
try {
const data = { ...form }
// Convert empty strings to null for optional fields
if (!data.descriptionZh) data.descriptionZh = null
if (!data.descriptionEn) data.descriptionEn = null
if (!data.descriptionPt) data.descriptionPt = null
if (!data.image) data.image = null
if (data.price === null || data.price === '') data.price = null

View File

@ -130,6 +130,9 @@ const createApp = () => {
const homeRoutes = require('./routes/homeRoutes');
app.use('/api/v1/home', homeRoutes);
const bookingRuleRoutes = require('./routes/bookingRuleRoutes');
app.use('/api/v1/booking-rules', bookingRuleRoutes);
const adminConfigRoutes = require('./routes/adminConfigRoutes');
app.use('/api/v1/admin/config', adminConfigRoutes);
@ -148,6 +151,9 @@ const createApp = () => {
const adminCommissionRoutes = require('./routes/adminCommissionRoutes');
app.use('/api/v1/admin/commissions', adminCommissionRoutes);
const adminBookingRuleRoutes = require('./routes/adminBookingRuleRoutes');
app.use('/api/v1/admin/booking-rules', adminBookingRuleRoutes);
// 404 handler
app.use(notFoundHandler);

View File

@ -0,0 +1,166 @@
const { BookingRule } = require('../models');
/**
* 获取所有预约规则
*/
const getAllRules = async (req, res) => {
try {
const rules = await BookingRule.findAll({
order: [['createdAt', 'ASC']]
});
res.json({
code: 0,
message: 'success',
data: rules
});
} catch (error) {
console.error('Get all booking rules error:', error);
res.status(500).json({
code: 500,
message: '获取预约规则失败',
data: null
});
}
};
/**
* 根据服务类型获取规则
*/
const getRuleByServiceType = async (req, res) => {
try {
const { serviceType } = req.params;
const rule = await BookingRule.findOne({
where: { serviceType }
});
res.json({
code: 0,
message: 'success',
data: rule
});
} catch (error) {
console.error('Get booking rule error:', error);
res.status(500).json({
code: 500,
message: '获取预约规则失败',
data: null
});
}
};
/**
* 创建或更新预约规则
*/
const upsertRule = async (req, res) => {
try {
const { serviceType } = req.params;
const { rules, status } = req.body;
const [rule, created] = await BookingRule.upsert({
serviceType,
rules: rules || '',
status: status || 'active'
}, {
returning: true
});
res.json({
code: 0,
message: created ? '创建成功' : '更新成功',
data: rule
});
} catch (error) {
console.error('Upsert booking rule error:', error);
res.status(500).json({
code: 500,
message: '保存预约规则失败',
data: null
});
}
};
/**
* 批量更新预约规则
*/
const batchUpsertRules = async (req, res) => {
try {
const { rules } = req.body; // [{ serviceType, rules, status }]
if (!Array.isArray(rules)) {
return res.status(400).json({
code: 400,
message: '参数格式错误',
data: null
});
}
const results = [];
for (const item of rules) {
const [rule] = await BookingRule.upsert({
serviceType: item.serviceType,
rules: item.rules || '',
status: item.status || 'active'
}, {
returning: true
});
results.push(rule);
}
res.json({
code: 0,
message: '批量保存成功',
data: results
});
} catch (error) {
console.error('Batch upsert booking rules error:', error);
res.status(500).json({
code: 500,
message: '批量保存预约规则失败',
data: null
});
}
};
/**
* 删除预约规则
*/
const deleteRule = async (req, res) => {
try {
const { serviceType } = req.params;
const deleted = await BookingRule.destroy({
where: { serviceType }
});
if (deleted) {
res.json({
code: 0,
message: '删除成功',
data: null
});
} else {
res.status(404).json({
code: 404,
message: '规则不存在',
data: null
});
}
} catch (error) {
console.error('Delete booking rule error:', error);
res.status(500).json({
code: 500,
message: '删除预约规则失败',
data: null
});
}
};
module.exports = {
getAllRules,
getRuleByServiceType,
upsertRule,
batchUpsertRules,
deleteRule
};

View File

@ -0,0 +1,35 @@
const { BookingRule } = require('../models');
/**
* 根据服务类型获取预约规则公开接口
*/
const getRuleByServiceType = async (req, res) => {
try {
const { serviceType } = req.params;
const rule = await BookingRule.findOne({
where: {
serviceType,
status: 'active'
},
attributes: ['serviceType', 'rules']
});
res.json({
code: 0,
message: 'success',
data: rule ? { rules: rule.rules } : { rules: '' }
});
} catch (error) {
console.error('Get booking rule error:', error);
res.status(500).json({
code: 500,
message: '获取预约规则失败',
data: null
});
}
};
module.exports = {
getRuleByServiceType
};

View File

@ -0,0 +1,42 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
/**
* BookingRule Model
* 预约登记规则配置每个服务类型单独配置
*/
const BookingRule = sequelize.define('BookingRule', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
serviceType: {
type: DataTypes.STRING(50),
allowNull: false,
unique: true,
field: 'service_type',
comment: '服务类型标识',
},
rules: {
type: DataTypes.TEXT,
allowNull: true,
comment: '预约登记规则内容',
},
status: {
type: DataTypes.ENUM('active', 'inactive'),
defaultValue: 'active',
allowNull: false,
comment: '状态',
},
}, {
tableName: 'booking_rules',
timestamps: true,
underscored: true,
indexes: [
{ fields: ['service_type'], unique: true },
{ fields: ['status'] },
],
});
module.exports = BookingRule;

View File

@ -13,6 +13,7 @@ const Config = require('./Config');
const HotService = require('./HotService');
const PaymentOrder = require('./PaymentOrder');
const Commission = require('./Commission');
const BookingRule = require('./BookingRule');
/**
* Define model associations
@ -102,5 +103,6 @@ module.exports = {
HotService,
PaymentOrder,
Commission,
BookingRule,
syncDatabase,
};

View File

@ -0,0 +1,46 @@
const express = require('express');
const adminBookingRuleController = require('../controllers/adminBookingRuleController');
const { authenticateAdmin } = require('../middleware/auth');
const { requireAdmin } = require('../middleware/rbac');
const router = express.Router();
// Apply authentication middleware
router.use(authenticateAdmin);
/**
* @route GET /api/v1/admin/booking-rules
* @desc 获取所有预约规则
* @access Private (Admin)
*/
router.get('/', requireAdmin, adminBookingRuleController.getAllRules);
/**
* @route GET /api/v1/admin/booking-rules/:serviceType
* @desc 根据服务类型获取规则
* @access Private (Admin)
*/
router.get('/:serviceType', requireAdmin, adminBookingRuleController.getRuleByServiceType);
/**
* @route PUT /api/v1/admin/booking-rules/:serviceType
* @desc 创建或更新预约规则
* @access Private (Admin)
*/
router.put('/:serviceType', requireAdmin, adminBookingRuleController.upsertRule);
/**
* @route POST /api/v1/admin/booking-rules/batch
* @desc 批量更新预约规则
* @access Private (Admin)
*/
router.post('/batch', requireAdmin, adminBookingRuleController.batchUpsertRules);
/**
* @route DELETE /api/v1/admin/booking-rules/:serviceType
* @desc 删除预约规则
* @access Private (Admin)
*/
router.delete('/:serviceType', requireAdmin, adminBookingRuleController.deleteRule);
module.exports = router;

View File

@ -0,0 +1,13 @@
const express = require('express');
const bookingRuleController = require('../controllers/bookingRuleController');
const router = express.Router();
/**
* @route GET /api/v1/booking-rules/:serviceType
* @desc 根据服务类型获取预约规则公开接口
* @access Public
*/
router.get('/:serviceType', bookingRuleController.getRuleByServiceType);
module.exports = router;

View File

@ -0,0 +1,31 @@
/**
* 预约登记规则混入
* 用于在预约表单页面加载和显示预约规则
*/
import { AppServer } from '@/modules/api/AppServer.js'
const appServer = new AppServer()
export default {
data() {
return {
bookingRules: '' // 预约登记规则
}
},
methods: {
/**
* 加载预约登记规则
* @param {String} serviceType - 服务类型
*/
async loadBookingRules(serviceType) {
try {
const result = await appServer.GetBookingRule(serviceType)
if (result.code === 0 && result.data && result.data.rules) {
this.bookingRules = result.data.rules
}
} catch (error) {
console.error('加载预约规则失败:', error)
}
}
}
}

View File

@ -66,6 +66,9 @@ serverConfig.apiUrl_Upload_Image = baseUrl + '/api/v1/upload/image' // 上传图
// ==================== 小程序码相关接口 ====================
serverConfig.apiUrl_QRCode_GetMiniProgram = baseUrl + '/api/v1/qrcode/miniprogram' // 获取小程序码
// ==================== 预约规则相关接口 ====================
serverConfig.apiUrl_BookingRule_Get = baseUrl + '/api/v1/booking-rules' // 获取预约规则
/**
* 获取完整的application/x-www-form-urlencoded请求参数
@ -610,6 +613,17 @@ AppServer.prototype.GetMiniProgramQRCode = async function(params = {}) {
})
}
/**
* 获取预约登记规则
* @param {String} serviceType - 服务类型
*/
AppServer.prototype.GetBookingRule = async function(serviceType) {
var url = serverConfig.apiUrl_BookingRule_Get + '/' + serviceType
return this.getData(url).then((data) => {
return data;
})
}
/**
* 上传图片
* @param {String} filePath - 本地文件路径

View File

@ -12,7 +12,12 @@
<!-- 可滚动内容区域 -->
<view class="scroll-content">
<view class="content">
<view class=""
<!-- 预约登记规则区域 -->
<view class="booking-rules-box" v-if="bookingRules">
<view class="rules-title">预约登记规则</view>
<view class="rules-content">{{ bookingRules }}</view>
</view>
<view v-else class=""
style="width: 680rpx; height: 396rpx; background-image: linear-gradient(-45deg, #60D7FF, #68BBD7); margin-top: 32rpx; border-radius: 20rpx; box-shadow: 0 0 10rpx 10rpx rgba(0, 0, 0, 0.1);">
</view>
@ -238,9 +243,11 @@
<script>
import { AppServer } from '@/modules/api/AppServer.js'
import bookingRulesMixin from '@/mixins/bookingRulesMixin.js'
const appServer = new AppServer()
export default {
mixins: [bookingRulesMixin],
data() {
return {
serviceId: "", // ID
@ -326,6 +333,8 @@
if (options.title) {
this.serviceTitle = decodeURIComponent(options.title)
}
//
this.loadBookingRules('flight')
},
methods: {
initDateRange() {
@ -707,4 +716,29 @@
min-width: 80rpx;
text-align: center;
}
/* 预约登记规则样式 */
.booking-rules-box {
width: 680rpx;
margin-top: 32rpx;
padding: 30rpx;
background-image: linear-gradient(-45deg, #60D7FF, #68BBD7);
border-radius: 20rpx;
box-shadow: 0 0 10rpx 10rpx rgba(0, 0, 0, 0.1);
box-sizing: border-box;
}
.rules-title {
font-size: 32rpx;
font-weight: bold;
color: #fff;
margin-bottom: 20rpx;
}
.rules-content {
font-size: 26rpx;
color: #fff;
line-height: 1.6;
white-space: pre-wrap;
}
</style>

View File

@ -12,7 +12,12 @@
<!-- 可滚动内容区域 -->
<view class="scroll-content">
<view class="content">
<view class=""
<!-- 预约登记规则区域 -->
<view class="booking-rules-box" v-if="bookingRules">
<view class="rules-title">预约登记规则</view>
<view class="rules-content">{{ bookingRules }}</view>
</view>
<view v-else class=""
style="width: 680rpx; height: 396rpx; background-image: linear-gradient(-45deg, #60D7FF, #68BBD7); margin-top: 32rpx; border-radius: 20rpx; box-shadow: 0 0 10rpx 10rpx rgba(0, 0, 0, 0.1);">
</view>
@ -209,9 +214,11 @@
<script>
import { AppServer } from '@/modules/api/AppServer.js'
import bookingRulesMixin from '@/mixins/bookingRulesMixin.js'
const appServer = new AppServer()
export default {
mixins: [bookingRulesMixin],
data() {
return {
serviceId: "",
@ -320,6 +327,8 @@
if (options.title) {
this.serviceTitle = decodeURIComponent(options.title)
}
//
this.loadBookingRules('hotel')
},
methods: {
initDateRange() {
@ -686,4 +695,29 @@
min-width: 80rpx;
text-align: center;
}
/* 预约登记规则样式 */
.booking-rules-box {
width: 680rpx;
margin-top: 32rpx;
padding: 30rpx;
background-image: linear-gradient(-45deg, #60D7FF, #68BBD7);
border-radius: 20rpx;
box-shadow: 0 0 10rpx 10rpx rgba(0, 0, 0, 0.1);
box-sizing: border-box;
}
.rules-title {
font-size: 32rpx;
font-weight: bold;
color: #fff;
margin-bottom: 20rpx;
}
.rules-content {
font-size: 26rpx;
color: #fff;
line-height: 1.6;
white-space: pre-wrap;
}
</style>