| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803 |
- <template>
- <div class="app-container">
- <!-- 搜索区域 -->
- <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" @submit.prevent>
- <el-form-item label="酒店名称" prop="hotelName">
- <el-input v-model="queryParams.hotelName" placeholder="请输入酒店名称" clearable @keyup.enter="handleQuery" />
- </el-form-item>
- <el-form-item>
- <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
- <el-button icon="Refresh" @click="resetQuery">重置</el-button>
- </el-form-item>
- </el-form>
- <!-- 操作按钮 -->
- <el-row :gutter="10" class="mb8">
- <el-col :span="1.5">
- <el-button type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
- </el-col>
- <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
- </el-row>
- <!-- 表格 -->
- <el-table v-loading="loading" border :data="dataList">
- <el-table-column label="序号" align="center" width="60">
- <template #default="scope">
- {{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}
- </template>
- </el-table-column>
- <el-table-column label="酒店名称" align="center" prop="hotelName" min-width="120" show-overflow-tooltip />
- <el-table-column label="酒店简介" align="center" prop="introduction" min-width="140" show-overflow-tooltip>
- <template #default="scope">
- {{ scope.row.introduction || '-' }}
- </template>
- </el-table-column>
- <el-table-column label="宣传图片" align="center" width="180">
- <template #default="scope">
- <div v-if="scope.row.imageList && scope.row.imageList.length > 0" class="cell-images">
- <el-image
- v-for="(img, idx) in scope.row.imageList.slice(0, 3)"
- :key="idx"
- :src="img.imageUrl"
- style="width: 44px; height: 44px; border-radius: 3px; cursor: pointer; border: 1px solid #e4e7ed"
- fit="cover"
- :preview-src-list="scope.row.imageList.map((i) => i.imageUrl)"
- :preview-teleported="true"
- :initial-index="idx"
- />
- <span v-if="scope.row.imageList.length > 3" class="cell-images-more">+{{ scope.row.imageList.length - 3 }}</span>
- </div>
- <span v-else class="cell-empty">暂无</span>
- </template>
- </el-table-column>
- <el-table-column label="房型与时段" align="center" min-width="220">
- <template #default="scope">
- <div v-if="scope.row.roomList && scope.row.roomList.length > 0" class="cell-room-block">
- <div v-for="(room, ri) in scope.row.roomList" :key="room.id || ri" class="cell-room-item">
- <div class="cell-room-title">{{ ri + 1 }}.{{ room.roomName }}:</div>
- <div v-for="(p, pi) in room.periodList || []" :key="p.id || pi" class="cell-room-line">
- 时间段{{ pi + 1 }}:{{ p.startDate }}-{{ p.endDate }}
- </div>
- <div v-if="!room.periodList || room.periodList.length === 0" class="cell-room-line">未配置时段</div>
- </div>
- </div>
- <span v-else class="cell-empty">暂无</span>
- </template>
- </el-table-column>
- <el-table-column label="剩余数量" align="center" min-width="150">
- <template #default="scope">
- <div v-if="scope.row.roomList && scope.row.roomList.length > 0" class="cell-room-block">
- <div v-for="(room, ri) in scope.row.roomList" :key="room.id || ri" class="cell-room-item">
- <div v-for="(p, pi) in room.periodList || []" :key="p.id || pi" class="cell-room-line">
- 时间段{{ pi + 1 }}剩余房间:{{ p.remainingCount }}
- </div>
- </div>
- </div>
- <span v-else class="cell-empty">暂无</span>
- </template>
- </el-table-column>
- <el-table-column label="房间介绍" align="center" min-width="140">
- <template #default="scope">
- <div v-if="scope.row.roomList && scope.row.roomList.length > 0" class="cell-room-block">
- <div v-for="(room, ri) in scope.row.roomList" :key="room.id || ri" class="cell-room-line cell-room-item" style="white-space: pre-line">
- {{ ri + 1 }}.{{ room.roomIntro || '暂无' }}
- </div>
- </div>
- <span v-else class="cell-empty">暂无</span>
- </template>
- </el-table-column>
- <el-table-column label="房间图片" align="center" width="180">
- <template #default="scope">
- <div v-if="scope.row.roomList && scope.row.roomList.length > 0" class="cell-room-block">
- <div v-for="(room, ri) in scope.row.roomList" :key="room.id || ri" class="cell-room-images-row cell-room-item">
- <span class="cell-room-img-index">{{ ri + 1 }}.</span>
- <div class="cell-images">
- <el-image
- v-for="(img, idx) in (room.imageList || []).slice(0, 3)"
- :key="idx"
- :src="img.imageUrl"
- style="width: 36px; height: 36px; border-radius: 3px; cursor: pointer; border: 1px solid #e4e7ed"
- fit="cover"
- :preview-src-list="(room.imageList || []).map((i) => i.imageUrl)"
- :preview-teleported="true"
- :initial-index="idx"
- />
- <span v-if="(room.imageList || []).length > 3" class="cell-images-more">+{{ room.imageList.length - 3 }}</span>
- </div>
- </div>
- </div>
- <span v-else class="cell-empty">暂无</span>
- </template>
- </el-table-column>
- <el-table-column label="置顶排序" align="center" prop="sortOrder" width="90" />
- <el-table-column label="操作" align="center" width="140" fixed="right">
- <template #default="scope">
- <div class="action-buttons">
- <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
- <el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
- </div>
- </template>
- </el-table-column>
- </el-table>
- <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
- <!-- 新增/修改对话框 -->
- <el-dialog :title="dialogTitle" v-model="dialogVisible" width="860px" append-to-body :close-on-click-modal="false">
- <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
- <el-form-item label="酒店" prop="hotelName">
- <el-input v-model="form.hotelName" placeholder="请输入酒店名称" />
- </el-form-item>
- <el-form-item label="酒店简介" prop="introduction">
- <el-input v-model="form.introduction" placeholder="请输入酒店简介" />
- </el-form-item>
- <el-form-item label="排序" prop="sortOrder">
- <el-input-number
- v-model="form.sortOrder"
- :min="1"
- :value-on-clear="null"
- placeholder="留空自动排到最后"
- controls-position="right"
- style="width: 200px"
- />
- <span class="form-inline-tip">数字越小越置顶</span>
- </el-form-item>
- <!-- 宣传图片(多张,第一张为封面) -->
- <el-form-item label="宣传图片">
- <div class="image-upload-container">
- <div class="upload-buttons-row">
- <el-upload
- ref="promoUploadRef"
- action="#"
- :auto-upload="false"
- :on-change="handlePromoFileChange"
- accept="image/*"
- :show-file-list="false"
- multiple
- >
- <el-button type="primary" size="small">上传新图片</el-button>
- </el-upload>
- </div>
- <div class="form-tip">支持多张,最多{{ MAX_IMAGES }}张,第一张为封面图片</div>
- <div v-if="form.promoImages.length > 0" class="content-images-grid">
- <div v-for="(img, index) in form.promoImages" :key="index" class="content-image-item">
- <el-image :src="img" style="width: 100%; height: 100px" fit="cover" :preview-src-list="form.promoImages" :preview-teleported="true" />
- <div class="content-image-actions">
- <el-button type="danger" size="small" circle icon="Delete" @click="form.promoImages.splice(index, 1)" />
- <el-button v-if="index > 0" type="warning" size="small" circle @click="moveImageUp(form.promoImages, index)">↑</el-button>
- <el-button
- v-if="index < form.promoImages.length - 1"
- type="warning"
- size="small"
- circle
- @click="moveImageDown(form.promoImages, index)"
- >↓</el-button
- >
- </div>
- <div class="content-image-index">{{ index + 1 }}</div>
- </div>
- </div>
- <div v-else class="no-image-tip">未添加宣传图片</div>
- </div>
- </el-form-item>
- <el-form-item label="位置" prop="location">
- <el-input v-model="form.location" placeholder="请输入位置" />
- </el-form-item>
- <!-- 房型列表 -->
- <el-form-item label="房型">
- <div style="width: 100%">
- <div v-for="(room, ri) in form.rooms" :key="ri" class="room-block">
- <div class="room-block-header">
- <span class="room-block-title">房型{{ ri + 1 }}</span>
- <el-button type="danger" size="small" plain @click="removeRoom(ri)">删除房间</el-button>
- </div>
- <el-form-item label="房间" :prop="'rooms.' + ri + '.roomName'" :rules="rules.roomName" label-width="80px">
- <el-input v-model="room.roomName" placeholder="请输入房间名称" style="width: 260px" />
- </el-form-item>
- <el-form-item label="房间介绍" label-width="80px">
- <el-input
- v-model="room.roomIntro"
- type="textarea"
- :autosize="{ minRows: 2, maxRows: 6 }"
- placeholder="请输入房间介绍,回车换行,APP端按换行显示"
- style="width: 400px"
- />
- </el-form-item>
- <el-form-item label="房间图片" label-width="80px">
- <div class="image-upload-container">
- <div class="upload-buttons-row">
- <el-upload
- action="#"
- :auto-upload="false"
- :on-change="(file) => handleRoomFileChange(file, room)"
- accept="image/*"
- :show-file-list="false"
- multiple
- >
- <el-button type="primary" size="small">上传新图片</el-button>
- </el-upload>
- </div>
- <div class="form-tip">最多上传{{ MAX_IMAGES }}张房间图片</div>
- <div v-if="room.roomImages.length > 0" class="content-images-grid">
- <div v-for="(img, index) in room.roomImages" :key="index" class="content-image-item">
- <el-image
- :src="img"
- style="width: 100%; height: 100px"
- fit="cover"
- :preview-src-list="room.roomImages"
- :preview-teleported="true"
- />
- <div class="content-image-actions">
- <el-button type="danger" size="small" circle icon="Delete" @click="room.roomImages.splice(index, 1)" />
- <el-button v-if="index > 0" type="warning" size="small" circle @click="moveImageUp(room.roomImages, index)">↑</el-button>
- <el-button
- v-if="index < room.roomImages.length - 1"
- type="warning"
- size="small"
- circle
- @click="moveImageDown(room.roomImages, index)"
- >↓</el-button
- >
- </div>
- <div class="content-image-index">{{ index + 1 }}</div>
- </div>
- </div>
- <div v-else class="no-image-tip">未添加房间图片</div>
- </div>
- </el-form-item>
- <!-- 房型时段 -->
- <el-form-item v-for="(p, pi) in room.periods" :key="pi" :label="'房间时段' + (pi + 1)" label-width="80px">
- <div class="period-row">
- <el-date-picker
- :model-value="p.startDate && p.endDate ? [p.startDate, p.endDate] : null"
- @update:model-value="(v) => setPeriodRange(p, v)"
- type="daterange"
- range-separator="至"
- start-placeholder="开始日期"
- end-placeholder="结束日期"
- value-format="YYYY-MM-DD"
- style="width: 260px"
- />
- <span class="period-count-label">剩余间数:</span>
- <el-input-number v-model="p.remainingCount" :min="0" controls-position="right" placeholder="剩余间数" style="width: 110px" />
- <el-button type="danger" size="small" plain @click="removePeriod(room, pi)">删除</el-button>
- </div>
- </el-form-item>
- <el-form-item label-width="80px" class="add-period-item">
- <el-button type="primary" size="small" plain icon="Plus" @click="addPeriod(room)">增加时段</el-button>
- </el-form-item>
- </div>
- <div class="add-room-row">
- <el-button type="primary" icon="Plus" @click="addRoom">添加房间</el-button>
- </div>
- </div>
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button type="primary" @click="submitForm">保 存</el-button>
- <el-button @click="dialogVisible = false">取 消</el-button>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup name="HotelBooking">
- import { listHotel, getHotel, addHotel, updateHotel, delHotel } from '@/api/content/hotel';
- import { uploadImageOnly } from '@/api/content/competitionZoneImage';
- import { ElMessage, ElMessageBox } from 'element-plus';
- const loading = ref(true);
- const showSearch = ref(true);
- const total = ref(0);
- const dataList = ref([]);
- const queryParams = reactive({
- pageNum: 1,
- pageSize: 10,
- hotelName: null
- });
- const queryRef = ref(null);
- // ========== 弹窗表单 ==========
- const dialogVisible = ref(false);
- const dialogTitle = ref('');
- const formRef = ref(null);
- const promoUploadRef = ref(null);
- // 每个上传位最多5张图片(宣传图、每个房型的房间图片各自限5张)
- const MAX_IMAGES = 5;
- // 时段:只含当天,剩余间数创建后可二次修改
- const createPeriod = () => ({ id: null, startDate: null, endDate: null, remainingCount: null });
- const createRoom = () => ({ id: null, roomName: '', roomIntro: '', roomImages: [], periods: [createPeriod()] });
- // 空白时段行/空白房型块:保存时过滤掉,不提交给后端
- function isPeriodEmpty(p) {
- return !p.startDate && !p.endDate && (p.remainingCount === null || p.remainingCount === undefined);
- }
- function isRoomEmpty(room) {
- const nameEmpty = !room.roomName || !room.roomName.trim();
- const noImages = !room.roomImages || room.roomImages.length === 0;
- return nameEmpty && noImages && (room.periods || []).every((p) => isPeriodEmpty(p));
- }
- const initForm = {
- id: null,
- hotelName: '',
- introduction: '',
- location: '',
- // 留空由后端自动分配为「当前最大值+1」,避免默认值与已有记录撞车导致新增失败
- sortOrder: null,
- promoImages: [],
- rooms: []
- };
- const form = ref({ ...initForm, promoImages: [], rooms: [] });
- // 置顶排序校验:新增允许留空(后端自动排到最后),修改必须是一个不小于1的有效值
- function validateSortOrder(rule, value, callback) {
- const empty = value === null || value === undefined || value === '';
- if (empty) {
- if (!form.value.id) return callback();
- return callback(new Error('请填写置顶排序值,最小为1且不可与其他记录重复'));
- }
- if (Number(value) < 1) {
- return callback(new Error('置顶排序最小为1'));
- }
- callback();
- }
- const rules = {
- hotelName: [{ required: true, message: '酒店名称不能为空', trigger: 'blur' }],
- sortOrder: [{ validator: validateSortOrder, trigger: 'blur' }],
- roomName: [{ required: true, message: '房间名称不能为空', trigger: 'blur' }]
- };
- // ========== 列表操作 ==========
- async function getList() {
- loading.value = true;
- try {
- const res = await listHotel(queryParams);
- dataList.value = res.rows || [];
- total.value = res.total;
- } catch (err) {
- console.error('加载数据失败:', err);
- } finally {
- loading.value = false;
- }
- // 等表格渲染完成后拉齐分割线高度
- nextTick(() => equalizeRoomRows());
- }
- // 拉齐四列的房型分割线:同一行内各列同序号房型块取最大高度统一,
- // 再统一整块高度(单元格内容垂直居中,整块等高后起点才一致),框高自适应内容
- function equalizeRoomRows() {
- // 先清掉上次设置的高度,避免旧值影响本次测量
- document.querySelectorAll('.cell-room-item, .cell-room-block').forEach((el) => {
- el.style.minHeight = '';
- });
- document.querySelectorAll('.el-table__body-wrapper tbody tr').forEach((tr) => {
- const groups = {};
- tr.querySelectorAll('.cell-room-block').forEach((block) => {
- Array.from(block.children).forEach((child, idx) => {
- (groups[idx] = groups[idx] || []).push(child);
- });
- });
- Object.values(groups).forEach((group) => {
- const max = Math.max(...group.map((el) => el.offsetHeight));
- group.forEach((el) => {
- el.style.minHeight = `${max}px`;
- });
- });
- const blocks = tr.querySelectorAll('.cell-room-block');
- if (blocks.length > 0) {
- const maxBlock = Math.max(...Array.from(blocks).map((el) => el.offsetHeight));
- blocks.forEach((el) => {
- el.style.minHeight = `${maxBlock}px`;
- });
- }
- });
- }
- onMounted(() => {
- window.addEventListener('resize', equalizeRoomRows);
- });
- onBeforeUnmount(() => {
- window.removeEventListener('resize', equalizeRoomRows);
- });
- function handleQuery() {
- queryParams.pageNum = 1;
- getList();
- }
- function resetQuery() {
- if (queryRef.value) queryRef.value.resetFields();
- handleQuery();
- }
- function handleDelete(row) {
- ElMessageBox.confirm(`确定要删除「${row.hotelName}」吗?其房型、时段与图片将一并删除`, '系统提示', {
- type: 'warning',
- confirmButtonText: '确认删除'
- })
- .then(async () => {
- await delHotel(row.id);
- ElMessage.success('删除成功');
- getList();
- })
- .catch(() => {});
- }
- // ========== 新增/修改 ==========
- function handleAdd() {
- // 默认带一个房型块,与原型一致:房间、房间介绍、房间图片、时段直接可见
- form.value = { ...initForm, promoImages: [], rooms: [createRoom()] };
- dialogTitle.value = '新增酒店订房';
- dialogVisible.value = true;
- }
- async function handleUpdate(row) {
- try {
- const res = await getHotel(row.id);
- const data = res.data;
- form.value = {
- id: data.id,
- hotelName: data.hotelName || '',
- introduction: data.introduction || '',
- location: data.location || '',
- // 库里若为 0/NULL 这类无效值,保持为空强制用户重填,不能兜底成 0(后端会拒绝保存)
- sortOrder: data.sortOrder && data.sortOrder >= 1 ? data.sortOrder : null,
- promoImages: (data.imageList || []).map((img) => img.imageUrl),
- rooms: (data.roomList || []).map((room) => ({
- id: room.id,
- roomName: room.roomName || '',
- roomIntro: room.roomIntro || '',
- roomImages: (room.imageList || []).map((img) => img.imageUrl),
- periods: (room.periodList || []).map((p) => ({
- id: p.id,
- startDate: p.startDate,
- endDate: p.endDate,
- remainingCount: p.remainingCount ?? 0
- }))
- }))
- };
- // 无房型的酒店同样展示一个空白房型块,便于直接录入
- if (form.value.rooms.length === 0) form.value.rooms.push(createRoom());
- dialogTitle.value = '修改酒店订房';
- dialogVisible.value = true;
- } catch (err) {
- console.error('加载详情失败:', err);
- // 不能静默吞掉:否则接口失败/渲染异常时点修改毫无提示,看起来像按钮坏了
- ElMessage.error('加载详情失败:' + (err?.message || err));
- }
- }
- // 提交前校验房型时段完整性(后端也会校验,这里提前拦住给出明确提示)
- function validateRooms(rooms) {
- for (let ri = 0; ri < rooms.length; ri++) {
- const room = rooms[ri];
- if (!room.roomName || !room.roomName.trim()) {
- ElMessage.warning(`房型${ri + 1}的房间名称不能为空`);
- return false;
- }
- for (let pi = 0; pi < room.periods.length; pi++) {
- const p = room.periods[pi];
- if (!p.startDate || !p.endDate) {
- ElMessage.warning(`房型${ri + 1}的时段${pi + 1}请选择完整日期范围`);
- return false;
- }
- if (p.startDate > p.endDate) {
- ElMessage.warning(`房型${ri + 1}的时段${pi + 1}开始日期不能晚于结束日期`);
- return false;
- }
- if (p.remainingCount === null || p.remainingCount === undefined || p.remainingCount < 0) {
- ElMessage.warning(`房型${ri + 1}的时段${pi + 1}剩余间数不能为空`);
- return false;
- }
- }
- }
- return true;
- }
- function submitForm() {
- formRef.value.validate(async (valid) => {
- if (!valid) return;
- // 过滤空白房型块与空白时段行后再校验与提交
- const rooms = form.value.rooms
- .filter((room) => !isRoomEmpty(room))
- .map((room) => ({ ...room, periods: room.periods.filter((p) => !isPeriodEmpty(p)) }));
- if (!validateRooms(rooms)) return;
- const payload = { ...form.value, rooms };
- try {
- if (payload.id) {
- await updateHotel(payload);
- ElMessage.success('修改成功');
- } else {
- await addHotel(payload);
- ElMessage.success('新增成功');
- }
- dialogVisible.value = false;
- getList();
- } catch (err) {
- console.error('保存失败:', err);
- }
- });
- }
- // ========== 房型/时段编辑 ==========
- function addRoom() {
- form.value.rooms.push(createRoom());
- }
- function removeRoom(ri) {
- ElMessageBox.confirm(`确定要删除房型${ri + 1}吗?`, '系统提示', { type: 'warning', confirmButtonText: '确认删除' })
- .then(() => {
- form.value.rooms.splice(ri, 1);
- })
- .catch(() => {});
- }
- function addPeriod(room) {
- room.periods.push(createPeriod());
- }
- function removePeriod(room, pi) {
- room.periods.splice(pi, 1);
- }
- function setPeriodRange(period, value) {
- period.startDate = value && value.length === 2 ? value[0] : null;
- period.endDate = value && value.length === 2 ? value[1] : null;
- }
- // ========== 图片上传(仅上传OSS,不存图库) ==========
- async function uploadFileToUrl(rawFile) {
- if (!rawFile.type.startsWith('image/')) {
- ElMessage.error('只能上传图片文件');
- return null;
- }
- if (rawFile.size > 5 * 1024 * 1024) {
- ElMessage.error('图片大小不能超过5MB');
- return null;
- }
- const formData = new FormData();
- formData.append('file', rawFile);
- const res = await uploadImageOnly(formData);
- if (res.code === 200 && res.data && res.data.imageUrl) {
- return res.data.imageUrl;
- }
- ElMessage.error(res.msg || '上传失败');
- return null;
- }
- async function handlePromoFileChange(file) {
- if (form.value.promoImages.length >= MAX_IMAGES) {
- ElMessage.warning(`宣传图片最多上传${MAX_IMAGES}张`);
- return;
- }
- try {
- const url = await uploadFileToUrl(file.raw);
- if (url) {
- form.value.promoImages.push(url);
- ElMessage.success('图片上传成功');
- }
- } catch (err) {
- console.error('上传失败:', err);
- }
- }
- async function handleRoomFileChange(file, room) {
- if (room.roomImages.length >= MAX_IMAGES) {
- ElMessage.warning(`每个房型的房间图片最多上传${MAX_IMAGES}张`);
- return;
- }
- try {
- const url = await uploadFileToUrl(file.raw);
- if (url) {
- room.roomImages.push(url);
- ElMessage.success('图片上传成功');
- }
- } catch (err) {
- console.error('上传失败:', err);
- }
- }
- function moveImageUp(arr, index) {
- [arr[index - 1], arr[index]] = [arr[index], arr[index - 1]];
- }
- function moveImageDown(arr, index) {
- [arr[index], arr[index + 1]] = [arr[index + 1], arr[index]];
- }
- getList();
- </script>
- <style scoped>
- .cell-images {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 4px;
- flex-wrap: wrap;
- }
- .cell-images-more {
- font-size: 11px;
- color: #909399;
- }
- .cell-empty {
- color: #c0c4cc;
- font-size: 12px;
- }
- .cell-room-block {
- text-align: left;
- padding: 4px 8px;
- font-size: 13px;
- line-height: 1.8;
- }
- /* 操作按钮一上一下竖排:两个按钮同宽且水平居中,图标与文字对齐在同一条中线上 */
- .action-buttons {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 6px;
- }
- .action-buttons .el-button {
- width: 64px;
- margin-left: 0;
- }
- /* 房型之间的分割线:单元格内每个房型块之间加虚线分隔,最后一个不加 */
- .cell-room-item:not(:last-child) {
- border-bottom: 1px dashed #dcdfe6;
- padding-bottom: 6px;
- margin-bottom: 6px;
- }
- .cell-room-title {
- font-weight: bold;
- color: #303133;
- }
- .cell-room-line {
- color: #606266;
- padding-left: 8px;
- }
- .cell-room-images-row {
- display: flex;
- align-items: center;
- gap: 4px;
- margin-bottom: 4px;
- }
- .cell-room-img-index {
- font-size: 13px;
- color: #606266;
- width: 14px;
- }
- .form-inline-tip {
- margin-left: 10px;
- font-size: 12px;
- color: #909399;
- }
- .image-upload-container {
- border: 1px solid #e4e7ed;
- border-radius: 6px;
- padding: 12px;
- width: 100%;
- }
- .upload-buttons-row {
- display: flex;
- align-items: center;
- margin-bottom: 12px;
- }
- .no-image-tip {
- text-align: center;
- padding: 20px;
- color: #909399;
- font-size: 13px;
- border: 1px dashed #dcdfe6;
- border-radius: 4px;
- }
- .form-tip {
- font-size: 12px;
- color: #909399;
- margin-bottom: 8px;
- }
- .content-images-grid {
- display: grid;
- grid-template-columns: repeat(4, 1fr);
- gap: 12px;
- margin-top: 8px;
- }
- .content-image-item {
- position: relative;
- border: 1px solid #e4e7ed;
- border-radius: 6px;
- overflow: hidden;
- }
- .content-image-actions {
- display: flex;
- justify-content: center;
- gap: 4px;
- padding: 6px;
- background: #fafafa;
- }
- .content-image-index {
- position: absolute;
- top: 4px;
- left: 4px;
- background: rgba(0, 0, 0, 0.6);
- color: #fff;
- font-size: 11px;
- width: 20px;
- height: 20px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .room-block {
- border: 1px solid #dcdfe6;
- border-radius: 6px;
- padding: 16px 12px 16px;
- margin-bottom: 16px;
- background: #fafafa;
- }
- .room-block-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 16px;
- }
- .room-block-title {
- font-size: 14px;
- font-weight: bold;
- color: #303133;
- }
- .period-row {
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .period-count-label {
- font-size: 13px;
- color: #606266;
- }
- /* 增加时段按钮与上方时段行、卡片底边拉开距离 */
- .add-period-item {
- margin-top: 12px;
- margin-bottom: 0;
- }
- .add-room-row {
- display: flex;
- justify-content: center;
- margin-bottom: 16px;
- }
- </style>
|