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

306 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?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)) {
configs.value = AppData.vrGoodsConfig.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;
}
}