使用vue3创建座位组件

将使用Vue 3创建一个座位组件。座位组件是一个常见的UI组件,用于显示座位信息,并允许用户选择或取消选择座位。该组件是通过js去创建的,没有类型提示,所以加入了一部分jsdoc,使编辑器可以识别类型
组件不太美观!!!
在这里插入图片描述

1、创建页面

这个组件不依赖于脚手架,所以选择创建一个html文件,导入vue.global.js
在文件根目录下创建index.html文件,并写入以下内容

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="utf-8" />
		<meta
			http-equiv="x-ua-compatible"
			content="ie=edge"
		/>
		<meta
			name="viewport"
			content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0 ,user-scalable=no"
		/>

		<title>document</title>
		<script src="http://tuchuang.wxsushang.com/2023/12/05/f5e8d377589b9.js"></script>
		<link
			rel="stylesheet"
			href="./css/index.css"
		/>
		<script src="./js/vue.global.js"></script>
		<style type="text/css">
			* {
				margin: 0;
				padding: 0;
			}
			body {
				-webkit-tap-highlight-color: transparent;
			}
			[v-clack] {
				display: none;
			}
		</style>
	</head>


	<body>
		<div id="app"></div>
	</body>
	<script type="module">
		const { createApp, setup,defineComponent} = Vue;
		const data = defineComponent({
			setup() {
				return {
				};
			},
		});
		const app = createApp(data);
		app.mount('#app');
	</script>
</html>

2、创建js/index.js 文件

写入以下代码

import _Seat, { seatProps } from './Seat.js';
import { withInstall } from './utils.js';
const Seat = withInstall(_Seat);
export { Seat, seatProps };
export * from './utils.js';
export * from './constant.js';

3、创建js/contant.js 文件

const SEAT_STATE = {
	NULL: 0, // 空,没位置
	IDLE: 'available', //可选
	SELECTED: 'selected', // 已选
	LOCKED: 'unavailable', //坏了
};
const camelizeRE = /-(\w)/g;
export { SEAT_STATE, camelizeRE };

4、创建js/utils.js文件

4.1 编写withInstall函数

作用: 为组件添加安装方法

function withInstall(options) {
	options.install = (app) => {
		const { name } = options;
		if (name) {
			app.component(name, options);
			app.component(camelize(`-${name}`), options);
		}
	};
	return options;
}

4.2 编写 camelize 函数

作用: 将一个以连字符分隔的字符串转换为驼峰命名法(Camel Case)的字符串

const camelizeRE= /-(\w)/g;
const camelize = (str) => str.replace(camelizeRE, (_, c) => c.toUpperCase());

4.3 编写createNameSpace 函数

作用: 创建命名空间
这个函数在创建组件的时候有用,生成组件名

function createNameSpace(name) {
	const prefixedName = `n-${name}`;
	return [prefixedName, createBEM(prefixedName)];
}

4.4 编写 createBEM 函数

作用: 创建一个生成BEM类名的函数

function createBEM(name) {
	return (el, mods) => {
		if (el && typeof el !== 'string') {
			mods = el;
			el = '';
		}
		el = el ? `${name}__${el}` : name;
		return `${el}${genBem(el, mods)}`;
	};
}

4.5 编写 genBem 函数

作用: 生成BEM类名

function genBem(name, mods) {
	if (!mods) {
		return '';
	}
	if (typeof mods === 'string') {
		return ` ${name}--${mods}`;
	}
	if (Array.isArray(mods)) {
		return mods.reduce((ret, item) => ret + genBem(name, item), '');
	}
	return Object.keys(mods).reduce(
		(ret, key) => ret + (mods[key] ? genBem(name, key) : ''),
		''
	);
}

4.6 编写makeStringProp和makeArrayProp函数


/**
 * 创建一个用于Vue字符串属性的定义函数
 * @param {string} str - 用于定义属性的初始值
 * @returns {object} - 包含Vue属性类型的对象
 */
function makeStringProp(str) {
	return { type: String, default: str };
}
/**
 * 创建一个数组类型的属性
 * @returns {object} 包含type为Array和default为函数的对象
 */
function makeArrayProp() {
	return { type: Array, default: () => [] };
}

4.7 编写toArray函数

作用: 将字符串转换成数组

function toArray(str) {
	if (typeof str !== 'string') {
		throw new Error('function arguments must be string');
	}
	return str.split(',').filter(Boolean);
}

4.8 编写 createSeat 函数

作用: 创建座位数组

/**
 * @description 座位类型
 * @typedef {Object} Seat
 * @property {string | null} [title] - 座位标题
 * @property {Boolean} choose - 是否选中
 * @property {Number} xCoordinate - x坐标
 * @property {Number} yCoordinate - y坐标
 * @property {Number} number - 座位号
 * @property {String | null} src - 座位图片
 * @property {String} type - 座位类型
 */
 /**
 * 创建座位
 * @param {Array<string | Number>|string} horizontal - 水平
 * @param {Array<string | Number>|string} vertical - 垂直
 * @returns {Array<Seat>} - 座位数组
 * @throws {Error} - 如果horizontal和vertical没有同时为数组或字符串则抛出错误
 */
function createSeat(horizontal, vertical, title = null) {
	if (typeof horizontal === 'undefined' || typeof vertical === 'undefined') {
		throw new Error(
			'Function arguments horizontal and vertical cannot be undefined'
		);
	}

	if (typeof horizontal === 'string') {
		horizontal = toArray(horizontal);
	} else if (!Array.isArray(horizontal)) {
		throw new Error(
			'Function argument horizontal must be a string or an array'
		);
	}

	if (typeof vertical === 'string') {
		vertical = toArray(vertical);
	} else if (!Array.isArray(vertical)) {
		throw new Error('Function argument vertical must be a string or an array');
	}
	/**
	 * @type {Array<Seat>}
	 */
	const seats = [];
	for (let x = 0; x < horizontal.length; x++) {
		for (let y = 0; y < vertical.length; y++) {
			const seat = {
				id: x * vertical.length + y + 1,
				title,
				choose: false,
				xCoordinate: x + 1,
				yCoordinate: y + 1,
				number: x * vertical.length + y + 1,
				type: SEAT_STATE.IDLE,
				src: null,
			};
			seat.choose = seat.type == SEAT_STATE.SELECTED;
			seats.push(seat);
		}
	}

	return seats;
}
function isArray(obj) {
	return Array.isArray(obj);
}

5、创建js/hooks.js文件

useSeat 钩子函数提供了处理座位选择和相关操作的功能,返回了一些用于在组件中使用的属性和方法

返回的属性和方法

  1. seats:Vue ref 对象,保存着一个座位对象数组
  2. selected:通过筛选 seats 数组来返回一个已选择的座位数组,并按照它们的 ID 进行排序。
  3. updateSeat:接受座位对象数组作为参数,并使用新的值更新 seats
  4. changeType:处理单个座位类型函数,接受座位对象和 type 参数,并根据提供的类型更新座位的属性(如 src、title 和 money)
  5. singleChoice:处理单个座位选择的函数,它将选中的座位标记为 SEAT_STATE.SELECTED,并更新其他座位的选择状态
  6. multipleChoice:处理多个座位选择的函数,它根据座位的选择状态更新座位的类型
  7. selectClick:处理已选择座位点击的函数,它根据点击的座位项更新座位的选择状态
  8. totalMoney:用于计算选中位置的总价格
import { createSeat } from './utils.js';
import { SEAT_STATE } from './constant.js';
/**
 * @typedef {Object} Type
 * @property {String} title - 名称
 * @property {String} [price] - 价格
 * @property {String} [state] - 状态
 * @property {String} [icon] - 图标
 * @reference
 */
const { computed, ref, onMounted } = Vue;
const useSeat = (options) => {
	const { horizontal, vertical, seats: _seats } = options;
	/**
	 * @type {import('vue').Ref<import('./utils').Seat>}
	 */
	const seats = ref(_seats ? _seats : createSeat(horizontal, vertical));
	const selected = computed(() => {
		const arr = seats.value
			.filter((seat) => seat.type === SEAT_STATE.SELECTED)
			.sort((a, b) => a.id - b.id);
		return arr;
	});
	const select = selected.value.map((item) => item.id);
	/**
	 * @description 更新座位
	 * @param {import('./utils').Seat[]} _seats
	 * @returns {import('./utils').Seat[]}
	 */
	const updateSeat = (_seats) => {
		const newSeat = _seats.map((seat, i) => {
			if (seats.value[i]) {
				seat = { ...seat, ...seats.value[i] };
			}
			return seat;
		});
		seats.value = newSeat;
	};
	/**
	 * @description 更改座位的类型
	 * @param {import('./utils').Seat} seat 座位
	 * @param {Type} type
	 */
	const changeType = (seat, type) => {
		const index = seats.value.findIndex((item) => item.id === seat.id);
		if (index !== -1) {
			seats.value[index] = {
				...seat,
				src: type.icon,
				title: type.title,
				money: type.price,
			};
		}
	};
	/**
	 * @description 选座(单选)
	 * @param {import('./utils.js').Seat} seat 座位
	 * @reference
	 */
	const singleChoice = (seat) => {
		const index = seats.value.findIndex((item) => item.id === seat.id);
		if (seat.type === SEAT_STATE.LOCKED) return alert('已被选择了');
		seats.value.forEach((_, i) => {
			_.type =
				i === index
					? SEAT_STATE.SELECTED
					: _.type === SEAT_STATE.LOCKED
					? SEAT_STATE.LOCKED
					: select.includes(_.id)
					? SEAT_STATE.SELECTED
					: SEAT_STATE.IDLE;
			_.choose = i === index;
		});
	};
	/**
	 * @description 选座(多选)
	 * @param {import('./utils.js').Seat} seat  座位
	 * @reference
	 */
	const multipleChoice = (seat) => {
		const index = seats.value.findIndex((item) => item.id === seat.id);
		const selIndex = selected.value.findIndex((item) => item.id === seat.id);
		if (seat.type === SEAT_STATE.LOCKED) return alert('已被选择了');
		seats.value[index].choose = selIndex === -1;
		seats.value[index].type =
			selIndex === -1 ? SEAT_STATE.SELECTED : SEAT_STATE.IDLE;
	};
	/**
	 * @description 点击已选择
	 * @param {import('./utils.js').Seat} seat 点击项
	 * @reference
	 */
	const selectClick = (_seat) => {
		const index = seats.value.findIndex((item) => item.id === _seat.id);
		seats.value = seats.value.map((seat, i) => ({
			...seat,
			choose: i === index ? !seat.choose : seat.choose,
			type: i === index ? SEAT_STATE.IDLE : seat.type,
		}));
	};
	/**
	 * @description 计算选中位置的价格
	 * @reference
	 */
	const totalMoney = computed(() => {
		return seats.value.reduce((pre, cur) => {
			return pre + (cur.type === SEAT_STATE.SELECTED ? Number(cur.money) : 0);
		}, 0);
	});
	onMounted(() => {
		seats.value = seats.value.map((seat) => {
			return {
				...seat,
				type: seat.choose ? SEAT_STATE.LOCKED : SEAT_STATE.IDLE,
			};
		});
	});
	return {
		seats,
		selected,
		updateSeat,
		changeType,
		singleChoice,
		multipleChoice,
		selectClick,
		totalMoney,
	};
};

export { useSeat };

6、创建js/Seat.js文件

这个文件用于编写组件代码

类型

/**
 * @typedef {Object} Type
 * @property {String} title - 名称
 * @property {String} [price] - 价格
 * @property {String} [state] - 状态
 * @property {String} [icon] - 图标
 * @reference
 */
/**
 * @typedef {Object} SeatProps
 * @property {Array<import('./utils.js').Seat>}  [seats]  -座位
 * @property {String} [tag] -标签
 * @property {String | Array<String | Number>} [horizontal] -行
 * @property {String | Array<String | Number>} [vertical] -列
 * @property {Array<Type>} [types] -类型
 * @property {String} [status] -状态
 * @property {Boolean} [multiple] -多选
 */

组件代码

const { defineComponent, createVNode, createTextVNode } = Vue;
import { SEAT_STATE } from './constant.js';
import {
	createNameSpace,
	makeStringProp,
	makeArrayProp,
	toArray,
	isArray,
} from './utils.js';
const [name, bem] = createNameSpace('seat');
/**
 * @description
 * @type {SeatProps}
 * @reference
 */
const seatProps = {
	tag: makeStringProp('div'),
	horizontal: makeStringProp(''), // '行'
	vertical: makeStringProp(''), // '列'
	seats: makeArrayProp(), // '座位'
	types: makeArrayProp(), // '类型'  修改座位的类型需要传参数 []
	status: makeStringProp('show'), // '状态' 展示还是可修改
	multiple: Boolean, // 是否可多选
};

const stdin_default = defineComponent({
	props: seatProps,
	name,
	emits: ['change-type', 'selected', 'before-change', 'click-seat'],
	setup(props, { emit, slots }) {
		/**
		 * @type {import('vue').Ref<import('./utils.js').Seat>}
		 */
		const renderRowHeader = () => {
			if (slots.header) return slots.header();
			const { horizontal, tag } = props;
			const row = isArray(horizontal) ? horizontal : toArray(horizontal);
			return createVNode(
				tag,
				{
					class: bem('header'),
				},
				row.map((r, index) => {
					return createVNode(
						'span',
						{
							class: [{ [bem('row-first')]: index == 0 }, bem('row-item')],
							key: `xAxis_${index}`,
						},
						[createTextVNode(r)]
					);
				})
			);
		};
		const renderColumn = () => {
			if (slots.column) return slots.column;
			const { vertical, tag } = props;
			const column = isArray(vertical) ? vertical : toArray(vertical);
			return createVNode(
				tag,
				{ class: bem('column') },
				column.map((col, index) => {
					return createVNode(
						'span',
						{ class: bem('column-item'), key: `yAxis_${index}` },
						[createTextVNode(col)]
					);
				})
			);
		};
		/**
		 * @description 点击事件
		 * @param {Event} e 事件
		 * @param {import('./utils.js').Seat} seat 座位信息
		 * @reference
		 */
		const onClick = (e, seat) => {
			e.preventDefault();
			const { status, multiple } = props;
			status !== 'show'
				? emit('change-type', seat)
				: multiple
				? emit('selected', seat)
				: emit('click-seat', seat);
		};
		const renderContainer = () => {
			if (slots.body) return slots.body();
			const { horizontal, vertical, tag, types, seats } = props;

			const row = isArray(horizontal) ? horizontal : toArray(horizontal);
			const column = isArray(vertical) ? vertical : toArray(vertical);
			/**
			 * @description
			 * @type {Type[]}
			 * @reference
			 */
			return createVNode(tag, { class: bem('container') }, [
				renderColumn(),
				createVNode(
					tag,
					{
						class: bem('body'),
						style: {
							gridTemplateColumns: `repeat(${row.length}, 1fr)`,
							gridTemplateRows: `repeat(${column.length}, 1fr)`,
						},
					},
					seats.map((seat, index) => {
						return createVNode(
							'span',
							{
								class: [
									seat.choose ? bem(SEAT_STATE.SELECTED) : bem(seat.type),
									bem('item'),
								],
								key: index,
								role: 'button',
								style: {
									backgroundImage: `url(${seat.src})`,
									pointerEvent:
										seat.type === SEAT_STATE.LOCKED ? 'none' : 'all',
									cursor:
										seat.type === SEAT_STATE.LOCKED ? 'no-drop' : 'pointer',
								},
								'data-y': seat.yCoordinate,
								'data-x': seat.xCoordinate,
								'data-tile': seat.title,
								onClick: (e) => onClick(e, seat),
							},
							[createTextVNode(seat.number)]
						);
					})
				),
			]);
		};
		return () => {
			const { tag } = props;
			return createVNode(
				tag,
				{
					class: bem(['map']),
				},
				slots.default ? slots.default() : [renderRowHeader(), renderContainer()]
			);
		};
	},
});

export { stdin_default as default, seatProps };

7、组件样式

7.1 css/index.scss

@import url(./reset.css);
@import url(./var.css);
$parent: '.n-seat';
#{$parent} {
	display: flex;
	flex-direction: column;
	overflow: auto;
	background-color: #7fffd4;
	width: 8rem;
	margin: auto;
	padding: 0.2rem;
	height: 8rem;
	&__header,
	&__column,
	&__body {
		display: flex;
		span {
			-webkit-text-emphasis: none;
			text-emphasis: none;
			text-align: center;
			color: brown;
			background-color: antiquewhite;
			font-size: var(--font-size);
			padding: calc(var(--seat-size) / 6);
			display: block;
			box-sizing: border-box;
			width: calc(var(--seat-size) + var(--seat-size) / 3);
			height: calc(var(--seat-size) + var(--seat-size) / 3);
			cursor: pointer;
		}
	}
	&__header {
		width: -webkit-max-content;
		width: -moz-max-content;
		width: max-content;
		margin-left: calc(var(--seat-size) + var(--seat-margin) * 2);
		span {
			&:not(:first-child) {
				margin-left: var(--seat-margin);
			}
		}
	}
	&__column {
		flex-direction: column;
		span {
			&:not(:last-child) {
				margin-bottom: var(--seat-margin);
			}
		}
	}
	&__container {
		display: flex;
		margin-top: var(--seat-margin);
	}
	&__body {
		display: grid;
		gap: calc(var(--seat-margin));
		margin-left: var(--seat-margin);
		span {
			border: 1px solid brown;
			border-radius: 0.05rem;
			background-color: transparent;
			&#{$parent}__item {
				background-size: cover;
				background-repeat: no-repeat;
				background-position: 0 0;
			}
			&#{$parent}__selected {
				background-color: red;
				color: #fff;
			}
			&#{$parent}__unavailable {
				background-color: #fff;
			}
		}
	}
}

7.2 reset.css

/* http://meyerweb.com/eric/tools/css/reset/
   v2.0 | 20110126
   License: none (public domain)
*/

html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
  margin: 0;
  padding: 0;
  border: 0;
  font-size: 100%;
  font: inherit;
  vertical-align: baseline;
}

/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
  display: block;
}

body {
  line-height: 1;
}

ol,
ul {
  list-style: none;
}

blockquote,
q {
  quotes: none;
}

blockquote:before,
blockquote:after,
q:before,
q:after {
  content: '';
  content: none;
}

table {
  border-collapse: collapse;
  border-spacing: 0;
}

7.3 var.css

:root {
  --font-size: 0.6rem;
  --bg-color: #fff;
  --animation-delay: 0.15s;
  --seat-size: 0.9rem;
  --seat-margin: 0.3rem;
}

用法

Props

序号名称说明
1seats座位
2horizontal控制列数
3vertical控制行数
4types类型
5status组件展示的形式 默认值 show 可选值 update
6multiple是否可以多选 默认值 false 可选值 true

Event

序号名称说明
1click-seat选择哪个座位
2change-type改变哪个座位的类型(只有 status 不为 show 时有效)
3selected选择哪个座位(只有 multiple 为 true 时有效)

Slots

序号Slot nameDescription
1default自定义内容
2header自定义头部
3body自定义内容
4column自定义列
  • 49
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值