vr-shopxo-plugin/shopxo/app/plugins/vr_ticket/hook/AdminGoodsSave.php

312 lines
14 KiB
PHP
Raw Normal View History

<?php
namespace app\plugins\vr_ticket\hook;
use think\facade\Db;
class AdminGoodsSave
{
public function handle($params = [])
{
$data = $params['data'] ?? [];
$isTicket = ($data['item_type'] ?? '') === 'ticket' ? 1 : 0;
// 解析原有配置
$vrGoodsConfig = [];
$rawConfig = $data['vr_goods_config'] ?? '';
if (!empty($rawConfig)) {
$parsed = json_decode($rawConfig, true);
if (json_last_error() === JSON_ERROR_NONE) {
if (is_string($parsed)) {
$parsed = json_decode($parsed, true);
}
if (is_array($parsed)) {
$vrGoodsConfig = $parsed;
}
}
}
// 查询模板
$templates = Db::name(self::table('seat_templates'))
->where('status', 1)
->field('id, name, seat_map')
->select()
->toArray();
$templateData = [];
foreach ($templates as $t) {
$seatMap = json_decode($t['seat_map'] ?? '{}', true);
// 补全缺失的 room.id老格式 seat_map 里没有 id 字段)
if (!empty($seatMap['rooms'])) {
foreach ($seatMap['rooms'] as $rIdx => &$room) {
if (empty($room['id'])) {
$room['id'] = 'room_' . $rIdx;
}
}
unset($room);
}
$t['seat_map'] = $seatMap;
$templateData[] = $t;
}
$initData = [
'isTicket' => $isTicket,
'vrGoodsConfig' => $vrGoodsConfig,
'templates' => $templateData,
];
$jsonInitData = base64_encode(json_encode($initData, JSON_UNESCAPED_UNICODE));
$html = <<<EOF
<div id="vr-ticket-plugin-app" style="margin: 20px 0; border: 1px solid #ebedf0; padding: 15px; border-radius: 4px; background: #fafafa;">
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div class="am-form-group">
<label>票务商品设置 <span class="am-form-group-label-tips">(使用插件覆盖基础多规格体系)</span></label>
<div>
<label class="am-checkbox-inline">
<input type="checkbox" v-model="isTicket" />
<span style="margin-left:5px;">设为在线选座的票务商品(勾选后将开启场馆节点配置)</span>
</label>
</div>
</div>
<div v-show="isTicket" style="margin-top:20px; border-top:1px solid #eee; padding-top:15px;" v-cloak>
<div class="am-form-group">
<label>请选择场馆模板(可多选)</label>
<div style="margin-top:10px;">
<label v-for="t in templates" :key="t.id" class="am-checkbox-inline" style="margin-right:20px;">
<input type="checkbox" :value="t.id" v-model="selectedTemplateIds" @change="onTemplateChange" />
{{ t.name }}
</label>
</div>
<div v-if="templates.length === 0" style="color:#999;font-size:12px;">暂无启用的场馆模板。</div>
</div>
<div v-for="config in configs" :key="config.template_id" style="margin-top: 15px; background: #fff; padding: 15px; border: 1px solid #ddd; border-radius: 4px;">
<p style="font-weight:bold;margin-bottom:10px;"> 场馆配置:{{ getTemplateName(config.template_id) }}</p>
<!-- 场次管理 -->
<div class="am-form-group" style="background: #fffbf0; padding: 12px; border: 1px solid #f5e79e; border-radius: 4px; margin-bottom:15px;">
<label style="font-weight:bold;">
<span style="color:#d2322d; margin-right:4px;">*</span>场次时段设置
<span style="color:#999; font-size:12px; font-weight:normal; margin-left:8px;">(至少设置一个场次)</span>
</label>
<div style="margin-top:8px;">
<div v-for="(session, idx) in config.sessions" :key="idx"
style="display:flex; align-items:center; gap:8px; margin-bottom:6px;">
<input type="time" v-model="session.start"
@change="validateSession(session)"
class="am-form-field"
style="width:110px; display:inline-block;" />
<span style="color:#666;"></span>
<input type="time" v-model="session.end"
@change="validateSession(session)"
class="am-form-field"
style="width:110px; display:inline-block;" />
<button type="button" class="am-btn am-btn-danger am-btn-xs"
@click="removeSession(config, idx)">删除</button>
<span v-if="session._error" style="color:red; font-size:12px;">{{ session._error }}</span>
</div>
<button type="button" class="am-btn am-btn-default am-btn-xs"
style="margin-top:4px;" @click="addSession(config)">+ 添加场次</button>
</div>
</div>
<div class="am-form-group">
<label>演播厅选择</label>
<div style="display:flex; flex-wrap:wrap; gap:15px; margin-top:5px;">
<label v-for="room in getRooms(config.template_id)" :key="room.id" class="am-checkbox-inline">
<input type="checkbox" :value="room.id" v-model="config.selected_rooms" />
<span style="margin-left:5px;">{{ room.name }}</span>
</label>
<span v-if="getRooms(config.template_id).length === 0" style="color:#999;font-size:12px;">该场馆内无放映室/演播厅数据</span>
</div>
</div>
<div class="am-form-group" v-if="config.selected_rooms.length > 0">
<label>分区选择 (仅限已选的演播厅)</label>
<div v-for="roomId in config.selected_rooms" :key="roomId" style="margin-left:20px; margin-top:10px;">
<p style="color:#666; font-size:13px; margin-bottom:5px;"> {{ getRoomName(config.template_id, roomId) }}</p>
<div style="display:flex; flex-wrap:wrap; gap:10px;">
<label v-for="sec in getSections(config.template_id, roomId)" :key="sec.char" class="am-checkbox-inline">
<input type="checkbox" :value="sec.char" v-model="config.selected_sections[roomId]" />
<span style="display:inline-block; width:12px; height:12px; margin:0 5px; vertical-align:middle;"
:style="{backgroundColor: sec.color || '#ccc'}"></span>
{{ sec.name }} ({{ sec.char }}) - {{ sec.price }}
</label>
<span v-if="getSections(config.template_id, roomId).length === 0" style="color:#999;font-size:12px;">该放映室无分区数据</span>
</div>
</div>
</div>
</div>
</div>
<input type="hidden" name="vr_is_ticket" :value="isTicket ? 1 : 0" />
<input type="hidden" name="vr_goods_config_base64" :value="outputBase64" />
</div>
<style>
[v-cloak] { display: none !important; }
</style>
<script>
const AppData = JSON.parse(decodeURIComponent(escape(atob('{$jsonInitData}'))));
const { createApp, ref, computed, watch } = Vue;
createApp({
setup() {
const isTicket = ref(AppData.isTicket === 1);
const templates = ref(AppData.templates || []);
const configs = ref([]);
const selectedTemplateIds = ref([]);
// 辅助函数移至顶部,确保初始化时可用
const getTemplateName = (tid) => {
const t = templates.value.find(x => x.id === tid);
return t ? t.name : 'Unknown';
};
const getRooms = (tid) => {
const t = templates.value.find(x => x.id === tid);
return (t && t.seat_map && t.seat_map.rooms) ? t.seat_map.rooms : [];
};
const getRoomName = (tid, roomId) => {
const r = getRooms(tid).find(x => x.id === roomId);
return r ? r.name : 'Unknown Room';
};
const getSections = (tid, roomId) => {
const r = getRooms(tid).find(x => x.id === roomId);
if (!r) return [];
if (r.sections && r.sections.length > 0) {
return r.sections.filter(s => s.char && s.char !== '_' && s.char !== '-');
}
return Object.entries(r.seats || {})
.filter(([char]) => char !== '_' && char !== '-')
.map(([char, info]) => ({
char,
name: info.name || info.label || char,
price: parseFloat(info.price) || 0,
color: info.color || '#ccc',
}));
};
const defaultSessions = () => [{ start: '08:00', end: '23:59' }];
// 还原已保存的配置并清洗历史脏数据
if (AppData.vrGoodsConfig && Array.isArray(AppData.vrGoodsConfig)) {
// 构建有效模板 ID 集合(只含 status=1 的模板)
const validTemplateIds = new Set((AppData.templates || []).map(t => t.id));
configs.value = AppData.vrGoodsConfig
// 过滤掉软删除模板的配置(幽灵配置)
.filter(c => validTemplateIds.has(c.template_id))
.map(c => {
// 确保 sessions 结构正确
if (!c.sessions || c.sessions.length === 0) {
c.sessions = defaultSessions();
}
if (!c.selected_sections) c.selected_sections = {};
// 【核心清洗】过滤非法 room ID
const validRooms = getRooms(c.template_id);
const validRoomIds = validRooms.map(r => String(r.id));
c.selected_rooms = (c.selected_rooms || [])
.map(rid => String(rid))
.filter(rid => rid && rid !== 'null' && rid !== 'undefined' && validRoomIds.includes(rid));
// 清理无效分区键
const newSections = {};
c.selected_rooms.forEach(rid => {
if (c.selected_sections[rid]) {
newSections[rid] = c.selected_sections[rid];
}
});
c.selected_sections = newSections;
return c;
});
selectedTemplateIds.value = configs.value.map(c => c.template_id);
}
const outputBase64 = computed(() => {
const clean = configs.value.map(c => {
const activeSections = {};
(c.selected_rooms || []).forEach(rid => {
if (c.selected_sections[rid]) {
activeSections[rid] = c.selected_sections[rid];
}
});
return {
...c,
selected_sections: activeSections,
sessions: (c.sessions || []).map(s => ({ start: s.start, end: s.end }))
};
});
return btoa(unescape(encodeURIComponent(JSON.stringify(clean))));
});
const onTemplateChange = () => {
const oldConfigs = [...configs.value];
configs.value = selectedTemplateIds.value.map(tid => {
const existing = oldConfigs.find(c => c.template_id === tid);
if (existing) return existing;
return {
template_id: tid,
selected_rooms: [],
selected_sections: {},
sessions: defaultSessions(),
};
});
};
const addSession = (config) => {
config.sessions.push({ start: '08:00', end: '23:59' });
};
const removeSession = (config, idx) => {
if (config.sessions.length <= 1) {
alert('至少需要保留一个场次时段');
return;
}
config.sessions.splice(idx, 1);
};
const validateSession = (session) => {
if (session.start && session.end && session.start >= session.end) {
session._error = '结束时间必须晚于开始时间';
} else {
delete session._error;
}
};
watch(configs, (newConfigs) => {
newConfigs.forEach(conf => {
if (!conf.selected_sections) conf.selected_sections = {};
(conf.selected_rooms || []).forEach(roomId => {
if (roomId && !conf.selected_sections[roomId]) {
conf.selected_sections[roomId] = [];
}
});
});
}, { deep: true });
return {
isTicket, templates, configs, selectedTemplateIds, outputBase64,
onTemplateChange, addSession, removeSession, validateSession,
getTemplateName, getRooms, getRoomName, getSections,
};
}
}).mount('#vr-ticket-plugin-app');
</script>
EOF;
return $html;
}
private static function table($name)
{
return 'vr_' . $name;
}
}