@scx-js/scx-ui 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js ADDED
@@ -0,0 +1,44 @@
1
+ export * from "./scx-context-menu/index.js";
2
+ export * from "./scx-drag/index.js";
3
+
4
+ import ScxGroup from "./scx-group/index.vue";
5
+ import ScxIcon from "./scx-icon/index.vue";
6
+ import ScxInput from "./scx-input/index.vue";
7
+ import ScxPanel from "./scx-panel/index.vue";
8
+ import ScxPanelItem from "./scx-panel/scx-panel-item.vue";
9
+ import ScxProgress from "./scx-progress/index.vue";
10
+ import ScxSwitch from "./scx-switch/index.vue";
11
+ import ScxUpload from "./scx-upload/index.vue";
12
+ import ScxUploadList from "./scx-upload-list/index.vue";
13
+ import {ScxContextMenuDirective} from "./scx-context-menu/index.js";
14
+ import {ScxDragDirective} from "./scx-drag/index.js";
15
+
16
+ //以下为组件
17
+ const components = [ScxGroup, ScxIcon, ScxInput, ScxPanel, ScxPanelItem, ScxProgress, ScxSwitch, ScxUpload, ScxUploadList];
18
+
19
+ //以下为指令
20
+ const directives = [ScxContextMenuDirective, ScxDragDirective];
21
+
22
+ const ScxComponent = {
23
+ install: (app) => {
24
+ //安装组件
25
+ components.forEach(c => app.component(c.name, c));
26
+ //安装指令
27
+ directives.forEach(d => app.directive(d.name, d));
28
+ },
29
+ };
30
+
31
+ export {
32
+ ScxGroup,
33
+ ScxIcon,
34
+ ScxInput,
35
+ ScxPanel,
36
+ ScxPanelItem,
37
+ ScxProgress,
38
+ ScxSwitch,
39
+ ScxUpload,
40
+ ScxUploadList,
41
+ ScxContextMenuDirective,
42
+ ScxDragDirective,
43
+ ScxComponent,
44
+ };
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@scx-js/scx-ui",
3
+ "version": "0.0.1",
4
+ "description": "SCX UI",
5
+ "license": "MIT",
6
+ "author": "scx567888",
7
+ "main": "index.js",
8
+ "type": "module",
9
+ "scripts": {
10
+ "dev:test": "vite _test --host",
11
+ "build:test": "vite build _test",
12
+ "preview:test": "vite preview _test --host"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/scx567888/scx-js.git"
17
+ },
18
+ "dependencies": {
19
+ "@scx-js/scx-common": "0.0.1",
20
+ "@scx-js/scx-app-x": "0.0.1",
21
+ "@scx-js/scx-dom": "0.0.1"
22
+ },
23
+ "devDependencies": {
24
+ "@vitejs/plugin-vue": "^5.0.0",
25
+ "clipboard": "^2.0.0",
26
+ "vite": "^6.0.0",
27
+ "vue-router": "^4.0.0"
28
+ }
29
+ }
@@ -0,0 +1,51 @@
1
+ /*全局右键菜单*/
2
+ .scx-context-menu {
3
+ margin: 0;
4
+ background: var(--scx-glass-bg);
5
+ z-index: 3000;
6
+ position: absolute;
7
+ list-style-type: none;
8
+ padding: 3px;
9
+ border-radius: 2px;
10
+ font-size: 12px;
11
+ font-weight: 400;
12
+ box-shadow: var(--scx-box-shadow);
13
+ max-height: 500px;
14
+ overflow: auto;
15
+ backdrop-filter: var(--scx-glass-bg-filter);
16
+ box-sizing: border-box;
17
+ width: max-content;
18
+ }
19
+
20
+ @keyframes from-top {
21
+ 0% {
22
+ opacity: 0;
23
+ transform: translateY(-10px);
24
+ }
25
+ }
26
+
27
+ @keyframes from-bottom {
28
+ 0% {
29
+ opacity: 0;
30
+ transform: translateY(10px);
31
+ }
32
+ }
33
+
34
+ .scx-context-menu.top {
35
+ animation: from-top 200ms cubic-bezier(.23, 1, .32, 1);
36
+ }
37
+
38
+ .scx-context-menu.bottom {
39
+ animation: from-bottom 200ms cubic-bezier(.23, 1, .32, 1);
40
+ }
41
+
42
+ .scx-context-menu-item {
43
+ margin: 0;
44
+ padding: 7px 16px;
45
+ cursor: pointer;
46
+ }
47
+
48
+ .scx-context-menu-item:hover {
49
+ background: var(--scx-theme);
50
+ color: var(--scx-bg);
51
+ }
@@ -0,0 +1,65 @@
1
+ import ScxContextMenu from "./index.vue";
2
+ import {h, render} from "vue";
3
+
4
+ // 这里因为 vite 每次导入时都使用不同的 上下文导致 contextMenuInstance 为空 这里直接绑定到 window 上
5
+ // let contextMenuInstance;
6
+
7
+ function getInstance() {
8
+ return window["__SCX_CONTEXT_MENU_INSTANCE__"];
9
+ }
10
+
11
+ function setInstance(instance) {
12
+ return window["__SCX_CONTEXT_MENU_INSTANCE__"] = instance;
13
+ }
14
+
15
+ function bodyClick(e) {
16
+ let isOnContextmenu = e.target.closest(".scx-context-menu");
17
+
18
+ if (!isOnContextmenu && getInstance()) {
19
+ closeContextMenu();
20
+ }
21
+ }
22
+
23
+ function showContextMenu(e, value) {
24
+ //默认阻止事件冒泡
25
+ e.stopPropagation();
26
+ const instance = getInstance();
27
+ if (instance) {
28
+ document.body.removeChild(instance);
29
+ }
30
+ const container = document.createElement("div");
31
+ let vm = h(ScxContextMenu, {
32
+ mouseEvent: e,
33
+ contextMenuItems: value,
34
+ });
35
+ render(vm, container);
36
+ setInstance(container);
37
+ document.body.appendChild(container);
38
+ document.body.addEventListener("click", bodyClick);
39
+ }
40
+
41
+ function closeContextMenu() {
42
+ document.body.removeChild(getInstance());
43
+ setInstance(null);
44
+ document.body.removeEventListener("click", bodyClick);
45
+ }
46
+
47
+ const ScxContextMenuDirective = {
48
+ name: "contextmenu",
49
+ //待处理
50
+ mounted(el, {value}) {
51
+ el.oncontextmenu = (e) => {
52
+ showContextMenu(e, value);
53
+ return false;
54
+ };
55
+ },
56
+ updated(el, {value}) {
57
+
58
+ },
59
+ };
60
+
61
+ export {
62
+ ScxContextMenuDirective,
63
+ showContextMenu,
64
+ closeContextMenu,
65
+ };
@@ -0,0 +1,86 @@
1
+ <template>
2
+ <div ref="scxContextMenuRef" class="scx-context-menu" @contextmenu.prevent="">
3
+ <div v-for="item in contextMenuItems" class="scx-context-menu-item" @click="callItemCallBack(item)">
4
+ <component :is="renderLabel(item)"></component>
5
+ </div>
6
+ </div>
7
+ </template>
8
+
9
+ <script>
10
+ //todo 多级菜单 ?
11
+ import "./index.css";
12
+ import {nextTick, onMounted, ref} from "vue";
13
+ import {closeContextMenu} from "./index.js";
14
+ import {isFunction} from "../../scx-common/index.js";
15
+
16
+ export default {
17
+ name: "scx-context-menu",
18
+ props: {
19
+ mouseEvent: MouseEvent,
20
+ contextMenuItems: Array
21
+ },
22
+ setup(props, context) {
23
+ const scxContextMenuRef = ref(null);
24
+
25
+ //设置初始状态 保证元素可以正确的获取的内容大小以便后续计算
26
+ function initStatus() {
27
+ scxContextMenuRef.value.style.top = 0;
28
+ scxContextMenuRef.value.style.left = 0;
29
+ scxContextMenuRef.value.style.visibility = "hidden";
30
+ }
31
+
32
+ function setStatus(top, left, type) {
33
+ scxContextMenuRef.value.classList.add(type);
34
+ scxContextMenuRef.value.style.top = top + "px";
35
+ scxContextMenuRef.value.style.left = left + "px";
36
+ scxContextMenuRef.value.style.visibility = "unset";
37
+ }
38
+
39
+ function show(mouseEvent) {
40
+ initStatus();
41
+ //todo 或者用 clientX ?
42
+ let x = mouseEvent.pageX;
43
+ let y = mouseEvent.pageY;
44
+ const clientWidth = window.innerWidth;
45
+ const clientHeight = window.innerHeight;
46
+ nextTick(() => {
47
+ const {
48
+ width,
49
+ height
50
+ } = scxContextMenuRef.value.getBoundingClientRect();
51
+ //todo 这里还可以做优化 以保证 菜单展示不下的时候出现滚动条 通过设置 height 和 width 实现
52
+ const top = clientHeight > height + y ? y : y - height;
53
+ const type = clientHeight > height + y ? "top" : "bottom";
54
+ const left = clientWidth > width + x ? x : x - width;
55
+ setStatus(top, left, type);
56
+ });
57
+ }
58
+
59
+ function callItemCallBack(item) {
60
+ if (item.callback) {
61
+ item.callback(closeContextMenu);
62
+ } else {
63
+ closeContextMenu();
64
+ }
65
+ }
66
+
67
+ function renderLabel(c) {
68
+ const {label} = c;
69
+ if (isFunction(label)) {
70
+ return label;
71
+ } else {
72
+ return () => label;
73
+ }
74
+ }
75
+
76
+ onMounted(() => show(props.mouseEvent));
77
+
78
+ return {
79
+ scxContextMenuRef,
80
+ callItemCallBack,
81
+ renderLabel
82
+ };
83
+ }
84
+
85
+ };
86
+ </script>
@@ -0,0 +1,30 @@
1
+ import {onBeforeUnmount} from "vue";
2
+ import {ScxDrag} from "@scx-js/scx-dom";
3
+
4
+ /**
5
+ *
6
+ * @param targetElement
7
+ * @param options {{dragElement, callback, bounds}}}
8
+ * @return {ScxDrag}
9
+ */
10
+ function useScxDrag(targetElement, options = {}) {
11
+ const scxDrag = new ScxDrag(targetElement, options);
12
+ scxDrag.enable();
13
+ onBeforeUnmount(() => scxDrag.disable());
14
+ return scxDrag;
15
+ }
16
+
17
+ const ScxDragDirective = {
18
+ name: "drag",
19
+ mounted(el, {value}) {
20
+ useScxDrag(el, value);
21
+ },
22
+ updated(el, {value}) {
23
+
24
+ },
25
+ };
26
+
27
+ export {
28
+ useScxDrag,
29
+ ScxDragDirective,
30
+ };
@@ -0,0 +1,66 @@
1
+ .scx-group {
2
+ position: relative;
3
+ width: 100%;
4
+ box-sizing: border-box;
5
+ padding: 10px;
6
+ /* 特有 css */
7
+ display: flex;
8
+ flex-direction: column;
9
+ row-gap: 10px;
10
+ align-items: center;
11
+ }
12
+
13
+ .scx-group-item {
14
+ position: relative;
15
+ width: 100%;
16
+ box-sizing: border-box;
17
+ padding: 10px;
18
+ /* 特有 css */
19
+ border: 1px dashed var(--scx-text-placeholder-color);
20
+ background-color: var(--scx-theme-bg);
21
+ transition: transform 600ms ease, opacity 600ms ease;
22
+ }
23
+
24
+ .scx-group-item-operation {
25
+ position: absolute;
26
+ top: 0;
27
+ right: 0;
28
+ display: flex;
29
+ }
30
+
31
+ .scx-group-item-move-up-button {
32
+ /* 占位用 方便开发者覆盖 css */
33
+ }
34
+
35
+ .scx-group-item-move-down-button {
36
+ /* 占位用 方便开发者覆盖 css */
37
+ }
38
+
39
+ .scx-group-item-remove-button {
40
+ /* 占位用 方便开发者覆盖 css */
41
+ }
42
+
43
+ /*简单设置一下 默认占位按钮的样式*/
44
+ .scx-group-item-operation .placeholder-button {
45
+ display: flex;
46
+ height: 22px;
47
+ width: 22px;
48
+ justify-content: center;
49
+ align-items: center;
50
+ }
51
+
52
+ /*以下为 vue 动画*/
53
+ .scx-group-list-enter-from {
54
+ opacity: 0;
55
+ transform: scale(0.98);
56
+ }
57
+
58
+ .scx-group-list-leave-to {
59
+ opacity: 0;
60
+ transform: scale(0.98);
61
+ }
62
+
63
+ /* 重要 !!!!! 避免嵌套动画延迟 */
64
+ .scx-group-list-move .scx-group-list-move {
65
+ transition: unset;
66
+ }
@@ -0,0 +1,154 @@
1
+ <template>
2
+ <div class="scx-group">
3
+ <slot name="before"></slot>
4
+ <transition-group name="scx-group-list" @before-leave="fixedElement">
5
+ <div v-for="(item,i) in list" :key="item" class="scx-group-item">
6
+ <slot :index="i" :item="item"></slot>
7
+ <div class="scx-group-item-operation">
8
+ <slot :index="i" :item="item" name="itemOperation"></slot>
9
+ <div v-if="showMoveUp(i)" class="scx-group-item-move-up-button" @click="groupItemMoveUp(i)">
10
+ <slot name="moveUpButton">
11
+ <button class="placeholder-button" type="button">↑</button>
12
+ </slot>
13
+ </div>
14
+ <div v-if="showMoveDown(i)" class="scx-group-item-move-down-button" @click="groupItemMoveDown(i)">
15
+ <slot name="moveDownButton">
16
+ <button class="placeholder-button" type="button">↓</button>
17
+ </slot>
18
+ </div>
19
+ <div v-if="showRemoveButton" class="scx-group-item-remove-button" @click="groupItemRemove(i)">
20
+ <slot name="removeButton">
21
+ <button class="placeholder-button" type="button">X</button>
22
+ </slot>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ </transition-group>
27
+ <slot name="after"></slot>
28
+ </div>
29
+ </template>
30
+
31
+ <script>
32
+ import "./index.css";
33
+ import {computed} from "vue";
34
+ import {fixedElement, moveDownByIndex, moveUpByIndex, removeByIndex} from "../../scx-common/index.js";
35
+
36
+ export default {
37
+ name: "scx-group",
38
+ props: {
39
+ modelValue: {
40
+ type: Array,
41
+ required: true,
42
+ default: [],
43
+ },
44
+ beforeRemove: {
45
+ type: Function,
46
+ default: null
47
+ },
48
+ beforeMoveUp: {
49
+ type: Function,
50
+ default: null
51
+ },
52
+ beforeMoveDown: {
53
+ type: Function,
54
+ default: null
55
+ },
56
+ loop: {
57
+ type: Boolean,
58
+ default: true
59
+ },
60
+ showRemoveButton: {
61
+ type: Boolean,
62
+ default: true
63
+ },
64
+ showMoveButton: {
65
+ type: Boolean,
66
+ default: true
67
+ }
68
+ },
69
+ setup(props, ctx) {
70
+
71
+ if (!props.modelValue) {
72
+ ctx.emit("update:modelValue", []);
73
+ }
74
+
75
+ const list = computed({
76
+ get() {
77
+ return props.modelValue;
78
+ },
79
+ set(value) {
80
+ ctx.emit("update:modelValue", value);
81
+ }
82
+ });
83
+
84
+ async function groupItemRemove(index) {
85
+ if (props.beforeRemove) {
86
+ //如果返回值是 false 则不添加
87
+ const result = await props.beforeRemove(list.value[index]);
88
+ if (!result) {
89
+ return;
90
+ }
91
+ }
92
+ removeByIndex(list.value, index);
93
+ }
94
+
95
+ async function groupItemMoveUp(index) {
96
+ if (props.beforeMoveUp) {
97
+ //如果返回值是 false 则不添加
98
+ const result = await props.beforeMoveUp(index);
99
+ if (!result) {
100
+ return;
101
+ }
102
+ }
103
+ moveUpByIndex(list.value, index, props.loop);
104
+ }
105
+
106
+ async function groupItemMoveDown(index) {
107
+ if (props.beforeMoveDown) {
108
+ //如果返回值是 false 则不添加
109
+ const result = await props.beforeMoveDown(index);
110
+ if (!result) {
111
+ return;
112
+ }
113
+ }
114
+ moveDownByIndex(list.value, index, props.loop);
115
+ }
116
+
117
+ function showMoveUp(i) {
118
+ if (!props.showMoveButton) {
119
+ return false;
120
+ }
121
+ const minIndex = 0;
122
+ //数据量小的时候没必要显示
123
+ if (list.value.length <= 2 && i === minIndex) {
124
+ return false;
125
+ } else {//数据量大的时候 如果没启用循环 第一项不显示
126
+ return props.loop ? true : i !== minIndex;
127
+ }
128
+ }
129
+
130
+ function showMoveDown(i) {
131
+ if (!props.showMoveButton) {
132
+ return false;
133
+ }
134
+ const maxIndex = list.value.length - 1;
135
+ //数据量小的时候没必要显示
136
+ if (list.value.length <= 2 && i === maxIndex) {
137
+ return false;
138
+ } else { //数据量大的时候 如果没启用循环 最后一项不显示
139
+ return props.loop ? true : i !== maxIndex;
140
+ }
141
+ }
142
+
143
+ return {
144
+ list,
145
+ groupItemRemove,
146
+ fixedElement,
147
+ groupItemMoveUp,
148
+ groupItemMoveDown,
149
+ showMoveUp,
150
+ showMoveDown
151
+ };
152
+ }
153
+ };
154
+ </script>
@@ -0,0 +1,4 @@
1
+ .scx-icon {
2
+ width: 1em;
3
+ height: 1em;
4
+ }
@@ -0,0 +1,20 @@
1
+ <template>
2
+ <svg class="scx-icon">
3
+ <use :href="'#scx-icon_' + icon"></use>
4
+ </svg>
5
+ </template>
6
+
7
+ <script>
8
+ import "./index.css";
9
+
10
+ export default {
11
+ name: "scx-icon",
12
+ props: {
13
+ icon: {
14
+ type: String,
15
+ default: "",
16
+ required: true
17
+ }
18
+ }
19
+ };
20
+ </script>
@@ -0,0 +1,39 @@
1
+ .scx-input {
2
+ display: flex;
3
+ justify-content: space-between;
4
+ align-self: center;
5
+ align-items: center;
6
+ /* 32px 是当前这个元素的高度 默认被内部的 input 撑起来的高度*/
7
+ border-radius: calc(32px / 2);
8
+ background: var(--scx-input_bg);
9
+ box-shadow: var(--scx-input_box-shadow);
10
+ transition: all 100ms cubic-bezier(0.23, 1, 0.32, 1);
11
+ padding-left: 10px;
12
+ padding-right: 10px;
13
+ box-sizing: border-box;
14
+ }
15
+
16
+ .scx-input > input {
17
+ width: 100%;
18
+ outline: none;
19
+ font-size: 15px;
20
+ height: 30px;
21
+ border: 0;
22
+ background-color: transparent;
23
+ margin-left: 10px;
24
+ margin-right: 10px;
25
+ }
26
+
27
+ .scx-input > input::placeholder {
28
+ color: var(--scx-text-placeholder-color);
29
+ }
30
+
31
+ .scx-input > svg {
32
+ width: 20px;
33
+ flex-shrink: 0;
34
+ cursor: pointer;
35
+ }
36
+
37
+ .scx-input:focus-within {
38
+ box-shadow: var(--scx-input-hover_box-shadow);
39
+ }
@@ -0,0 +1,44 @@
1
+ <template>
2
+ <div class="scx-input">
3
+ <scx-icon v-if="icon" :icon="icon"/>
4
+ <input v-model="m" :placeholder="placeholder" type="text"/>
5
+ <scx-icon v-show="m!==''" icon="outlined-close" @click="m=''"/>
6
+ </div>
7
+ </template>
8
+
9
+ <script>
10
+ import "./index.css";
11
+ import {computed} from "vue";
12
+
13
+ export default {
14
+ name: "scx-input",
15
+ props: {
16
+ modelValue: {
17
+ type: String,
18
+ required: true,
19
+ default: "",
20
+ },
21
+ placeholder: {
22
+ type: String,
23
+ default: "输入名称搜索",
24
+ },
25
+ icon: {
26
+ type: String,
27
+ default: null
28
+ }
29
+ },
30
+ setup(props, context) {
31
+
32
+ const m = computed({
33
+ get() {
34
+ return props.modelValue;
35
+ },
36
+ set(value) {
37
+ context.emit("update:modelValue", value);
38
+ }
39
+ });
40
+
41
+ return {m};
42
+ }
43
+ };
44
+ </script>
@@ -0,0 +1,47 @@
1
+ .scx-panel {
2
+ border-radius: 4px;
3
+ padding: 10px;
4
+ background: var(--scx-glass-bg);
5
+ transition: background-color 0.25s cubic-bezier(0.7, 0.3, 0.1, 1);
6
+ backdrop-filter: var(--scx-glass-bg-filter);
7
+ overflow: hidden;
8
+ display: flex;
9
+ flex-direction: column;
10
+ box-shadow: var(--scx-box-shadow-center);
11
+ }
12
+
13
+ /********************* vue 过渡动画 *******************/
14
+ .scx-panel-t_left-bottom-enter-active,
15
+ .scx-panel-t_left-bottom-leave-active,
16
+ .scx-panel-t_left-top-enter-active,
17
+ .scx-panel-t_left-top-leave-active,
18
+ .scx-panel-t_top-enter-active,
19
+ .scx-panel-t_top-leave-active {
20
+ opacity: 1;
21
+ transition: transform 200ms cubic-bezier(.23, 1, .32, 1), opacity 100ms cubic-bezier(.23, 1, .32, 1);
22
+ }
23
+
24
+ .scx-panel-t_left-top-enter-from,
25
+ .scx-panel-t_left-top-leave-to,
26
+ .scx-panel-t_left-bottom-enter-from,
27
+ .scx-panel-t_left-bottom-leave-to,
28
+ .scx-panel-t_top-enter-from,
29
+ .scx-panel-t_top-leave-to {
30
+ opacity: 0;
31
+ transform: scale(0.95);
32
+ }
33
+
34
+ .scx-panel-t_left-bottom-enter-active,
35
+ .scx-panel-t_left-bottom-leave-active {
36
+ transform-origin: left bottom;
37
+ }
38
+
39
+ .scx-panel-t_left-top-enter-active,
40
+ .scx-panel-t_left-top-leave-active {
41
+ transform-origin: left top;
42
+ }
43
+
44
+ .scx-panel-t_top-enter-active,
45
+ .scx-panel-t_top-leave-active {
46
+ transform-origin: top;
47
+ }