瀏覽代碼

Merge branch '2023-8-28' into online

liushengqiang 1 年之前
父節點
當前提交
756d1f4390

+ 57 - 0
src/helpers/parseABC.ts

@@ -21,3 +21,60 @@ export const getMinNoteUnit = (abcNotation: string) => {
 
 	return minNoteUnit;
 };
+
+// Convert an AudioBuffer to a Blob using WAVE representation
+export const bufferToWave = (audioBuffer: AudioBuffer) => {
+	var numOfChan = audioBuffer.numberOfChannels;
+	var length = audioBuffer.length * numOfChan * 2 + 44;
+	var buffer = new ArrayBuffer(length);
+	var view = new DataView(buffer);
+	var channels = [];
+	var i;
+	var sample;
+	var offset = 0;
+	var pos = 0;
+
+	// write WAVE header
+	setUint32(0x46464952);                         // "RIFF"
+	setUint32(length - 8);                         // file length - 8
+	setUint32(0x45564157);                         // "WAVE"
+
+	setUint32(0x20746d66);                         // "fmt " chunk
+	setUint32(16);                                 // length = 16
+	setUint16(1);                                  // PCM (uncompressed)
+	setUint16(numOfChan);
+	setUint32(audioBuffer.sampleRate);
+	setUint32(audioBuffer.sampleRate * 2 * numOfChan); // avg. bytes/sec
+	setUint16(numOfChan * 2);                      // block-align
+	setUint16(16);                                 // 16-bit (hardcoded in this demo)
+
+	setUint32(0x61746164);                         // "data" - chunk
+	setUint32(length - pos - 4);                   // chunk length
+
+	// write interleaved data
+	for(i = 0; i < numOfChan; i++)
+		channels.push(audioBuffer.getChannelData(i));
+
+	while(pos < length) {
+		for(i = 0; i < channels.length; i++) {             // interleave channels
+			sample = Math.max(-1, Math.min(1, channels[i][offset])); // clamp
+			sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767)|0; // scale to 16-bit signed int
+			view.setInt16(pos, sample, true);          // write 16-bit sample
+			pos += 2;
+		}
+		offset++; // next source sample
+	}
+
+	// create Blob
+	return new Blob([buffer], {type: "audio/wav"});
+
+	function setUint16(data: any) {
+		view.setUint16(pos, data, true);
+		pos += 2;
+	}
+
+	function setUint32(data: any) {
+		view.setUint32(pos, data, true);
+		pos += 4;
+	}
+}

+ 15 - 2
src/pc/api.ts

@@ -17,8 +17,8 @@ export const api_musicSheetCreationPage = (data: any) => {
 	return request.post(`/musicSheetCreation/page`, { data, requestType: 'json' });
 };
 /** 删除曲谱 */
-export const api_musicSheetCreationRemove = (data: any) => {
-	return request.post(`/musicSheetCreation/remove?id=` + data);
+export const api_musicSheetCreationRemove = (id: any, delMusicSheet: number) => {
+	return request.post(`/musicSheetCreation/remove?id=${id}&delMusicSheet=${delMusicSheet}`);
 };
 /** 曲谱详情 */
 export const api_musicSheetCreationDetail = (data: any) => {
@@ -40,3 +40,16 @@ export const api_xmlToAbc = (data: any) => {
 		data: data
 	});
 }
+/** 创建曲谱 */
+export const api_musicSheetCreationSaveMusic = (data: any) => {
+	return request.post(`/musicSheetCreation/saveMusic`, {
+		data: data,
+		requestType: 'json'
+	});
+}
+/** wav转mp3 */
+export const api_musicSheetCreationWav2mp3 = (data: any) => {
+	return request.get(`/musicSheetCreation/wav2mp3?url=` + data);
+}
+
+

+ 102 - 0
src/pc/component/upload-to-resources/index.module.less

@@ -0,0 +1,102 @@
+.setbox {
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    width: 427px;
+    border-radius: 16px;
+    background: #fff;
+    overflow: hidden;
+}
+
+.head {
+    position: relative;
+    line-height: 53px;
+    height: 60px;
+    text-align: center;
+    background: #F5F6FA;
+    color: #131415;
+    font-weight: 600;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 16px;
+    flex-shrink: 0;
+
+    .close {
+        position: absolute;
+        top: 50%;
+        right: 20px;
+        transform: translateY(-50%);
+    }
+}
+
+.form {
+    padding: 26px;
+    :global {
+        .n-form-item .n-form-item-feedback-wrapper{
+            --n-feedback-height: 26px;
+        }
+        .n-form-item.n-form-item--left-labelled:last-child {
+            .n-form-item-feedback-wrapper {
+                display: none;
+            }
+        }
+        .n-form-item .n-form-item-label{
+            color: #666;
+        }
+        .n-form-item-label.n-form-item-label--right-mark{
+            min-height: 46px;
+        }
+        .n-base-selection{
+            --n-height: 46px;
+        }
+    }
+
+    .checkbox {
+        :global {
+            .n-button {
+                border-radius: 6px;
+                width: 73px;
+            }
+
+            .n-button--default-type {
+                background: #F5F6FA;
+                ;
+            }
+
+            .n-button--primary-type {
+                background: #198CFE;
+                color: #fff;
+            }
+        }
+    }
+}
+
+.btns {
+    padding-bottom: 26px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    :global {
+        .n-button {
+            width: 135px;
+            height: 41px;
+            border-radius: 20px;
+            border-color: #AAAAAA;
+            margin: 0 8px;
+        }
+        .n-button--loading{
+            opacity: .6;
+        }
+    }
+}
+.productIframe{
+    position: fixed;
+    width: 1000px;
+    height: 80vh;
+    border: none;
+    z-index: -100;
+    pointer-events: none;
+    opacity: 0;
+}

+ 304 - 0
src/pc/component/upload-to-resources/index.tsx

@@ -0,0 +1,304 @@
+import { defineComponent, nextTick, onMounted, onUnmounted, reactive, ref, watch } from "vue";
+import {
+	api_musicSheetCreationWav2mp3,
+	api_musicSheetCreationSaveMusic,
+	api_subjectList,
+} from "../../api";
+import {
+	NButton,
+	NForm,
+	NFormItem,
+	NIcon,
+	NModal,
+	NProgress,
+	NSelect,
+	NSpace,
+	useMessage,
+} from "naive-ui";
+import styles from "./index.module.less";
+import { Close } from "@vicons/ionicons5";
+import { SelectMixedOption } from "naive-ui/es/select/src/interface";
+import { api_uploadFile } from "/src/utils/uploadFile";
+
+export default defineComponent({
+	name: "UploadToResources",
+	props: {
+		show: {
+			type: Boolean,
+			default: false,
+		},
+		item: {
+			type: Object,
+			default: () => ({}),
+		},
+	},
+	emits: ["update:show", "success"],
+	setup(props, { emit }) {
+		const message = useMessage();
+		const model = reactive({
+			subjects: [] as SelectMixedOption[],
+			saveLoading: false,
+			saveProgress: 0,
+			productOpen: false,
+			productIfameSrc: "",
+		});
+		const froms = reactive({
+			subjectId: null,
+			isPublic: 0,
+			mp3: "",
+			musicImg: "",
+			musicSvg: "",
+			musicJianSvg: "",
+		});
+		const getSubjects = async () => {
+			const { data } = await api_subjectList();
+			model.subjects = data.map((item: any) => {
+				return {
+					label: item.name,
+					value: item.id,
+				};
+			});
+		};
+
+		const handleProductResult = (res: MessageEvent) => {
+			const data = res.data;
+			if (data?.api === "webApi_renderSvg") {
+				let imgs: any = [];
+				try {
+					imgs = JSON.parse(data.product);
+				} catch (error) {
+					console.log("🚀 ~ error:", error);
+				}
+				imgs = imgs.filter((item: any) => item.base64);
+				if (imgs.length === 3) {
+					handleUploadImg(imgs);
+				}
+				console.log("🚀 ~ 上传之前", [...imgs]);
+			}
+		};
+		const handleUploadImg = async (imgs: any[]) => {
+			if (!props.show) return;
+			for (let i = 0; i < imgs.length; i++) {
+				const fileName = `${Date.now()}p${i}.png`;
+				const file = dataURLtoFile(imgs[i].base64, fileName);
+				imgs[i].url = await api_uploadFile(file, fileName, () => {});
+				model.saveProgress = (i + 1) * 20;
+			}
+			froms.musicImg = imgs[0]?.url || "";
+			froms.musicSvg = imgs[1]?.url || "";
+			froms.musicJianSvg = imgs[2]?.url || "";
+			model.productOpen = false;
+			imgs = [];
+			if (!props.show) return;
+			handleSubmit();
+		};
+		/** base64转file */
+		const dataURLtoFile = (dataurl: string, filename: string) => {
+			let arr = dataurl.split(",") || [],
+				mime = arr[0].match(/:(.*?);/)?.[1],
+				bstr = atob(arr[1]),
+				n = bstr.length,
+				u8arr = new Uint8Array(n);
+			while (n--) {
+				u8arr[n] = bstr.charCodeAt(n);
+			}
+			return new File([u8arr], filename, { type: mime });
+		};
+		onMounted(() => {
+			getSubjects();
+			window.addEventListener("message", handleProductResult);
+		});
+		onUnmounted(() => {
+			window.removeEventListener("message", handleProductResult);
+		});
+		watch(
+			() => props.item,
+			() => {
+				console.log(props.item);
+				froms.subjectId = props.item.subjectId ?? null;
+			}
+		);
+
+		const createMusic = async () => {
+			console.log()
+			await api_musicSheetCreationSaveMusic({
+				musicSheetCreationId: props.item.id,
+				musicSheetName: props.item.name || "曲谱名称",
+				musicSheetCategoriesId: "",
+				audioType: "MP3",
+				mp3Type: "MP3",
+				xmlFileUrl: props.item.xml,
+				musicSubject: froms.subjectId,
+				showFingering: 1,
+				canEvaluate: 1,
+				notation: 1,
+				playSpeed: props.item?.visualObj?.metaText?.tempo?.bpm || "",
+				background: [
+					{
+						audioFileUrl: froms.mp3,
+						track: "P1",
+					},
+				],
+				musicImg: froms.musicImg,
+				musicSvg: froms.musicSvg,
+				musicJianSvg: froms.musicJianSvg,
+				extConfigJson: "",
+			});
+		};
+		const wav2mp3 = async () => {
+			const { data } = await api_musicSheetCreationWav2mp3(props.item.filePath);
+			froms.mp3 = data;
+		};
+
+		const handleClose = () => {
+			model.saveLoading = false;
+			model.saveProgress = 0;
+		};
+
+		/** 自动生成图片 */
+		const handleAutoProduct = async () => {
+			model.saveProgress = 0;
+			const xml = props.item.xml;
+			const res = await fetch(xml);
+			if (res.status > 299 || res.status < 200) {
+				message.error("xml文件不存在");
+				handleClose();
+				return;
+			}
+			const origin = /(localhost|192)/.test(location.host)
+				? "https://test.lexiaoya.cn"
+				: location.origin;
+			model.productIfameSrc = `${origin}/instrument/#/product-img?xmlUrl=${xml}&productXmlImg=1`;
+			model.productOpen = true;
+			setTimeout(() => {
+				model.saveProgress = 10;
+			}, 800);
+		};
+		const fromRef = ref();
+		const handleUpload = () => {
+			fromRef.value.validate((err: any) => {
+				if (err) {
+					return;
+				}
+				if (!props.item.xml) {
+					message.error("没有生成xml文件");
+					handleClose();
+					return;
+				}
+				if (!props.item.filePath) {
+					message.error("没有生成wav文件");
+					handleClose();
+					return;
+				}
+				model.saveLoading = true;
+				handleAutoProduct();
+			});
+		};
+		const handleSubmit = async () => {
+			await wav2mp3();
+			model.saveProgress = 70;
+			if (!props.show) return;
+			await createMusic();
+			model.saveProgress = 100;
+			emit("success");
+			if (!props.show) return;
+			message.success("上传成功");
+			setTimeout(() => {
+				model.saveLoading = false;
+				emit("update:show", false);
+			}, 300);
+		};
+		return () => (
+			<>
+				<NModal
+					autoFocus={false}
+					show={props.show}
+					onUpdate:show={(val) => {
+						model.productOpen = false;
+						emit("update:show", val);
+					}}
+				>
+					<div class={styles.setbox}>
+						<div class={styles.head}>
+							<div>上传到我的资源</div>
+							<NButton
+								class={styles.close}
+								quaternary
+								circle
+								size="small"
+								onClick={() => {
+									model.productOpen = false;
+									emit("update:show", false);
+								}}
+							>
+								<NIcon component={Close} size={18} />
+							</NButton>
+						</div>
+						<NForm
+							ref={fromRef}
+							model={froms}
+							class={styles.form}
+							labelPlacement="left"
+							showRequireMark={false}
+						>
+							<NFormItem
+								label="可用声部"
+								path="subjectId"
+								rule={{
+									required: true,
+									type: "number",
+									message: "请选择素材可用乐器",
+									trigger: "change",
+								}}
+							>
+								<NSelect
+									to="body"
+									placeholder="请选择素材可用乐器"
+									options={model.subjects}
+									v-model:value={froms.subjectId}
+								></NSelect>
+							</NFormItem>
+							{/* <NFormItem label="是否公开">
+								<NSpace class={styles.checkbox} wrapItem={false}>
+									<NButton
+										secondary
+										bordered={false}
+										type={froms.isPublic === 1 ? "primary" : "default"}
+										onClick={() => (froms.isPublic = 1)}
+									>
+										公开
+									</NButton>
+									<NButton
+										secondary
+										bordered={false}
+										type={froms.isPublic === 0 ? "primary" : "default"}
+										onClick={() => (froms.isPublic = 0)}
+									>
+										不公开
+									</NButton>
+								</NSpace>
+							</NFormItem> */}
+							<NFormItem label="上传进度" style={{ display: model.saveLoading ? "" : "none" }}>
+								<NProgress percentage={model.saveProgress} />
+							</NFormItem>
+						</NForm>
+						<div class={styles.btns}>
+							<NButton
+								onClick={() => {
+									model.productOpen = false;
+									emit("update:show", false);
+								}}
+							>
+								取消
+							</NButton>
+							<NButton type="primary" loading={model.saveLoading} onClick={() => handleUpload()}>
+								确定
+							</NButton>
+						</div>
+					</div>
+				</NModal>
+				{model.productOpen && <iframe class={styles.productIframe} src={model.productIfameSrc}></iframe>}
+			</>
+		);
+	},
+});

+ 7 - 5
src/pc/create/component/the-create/index.tsx

@@ -21,14 +21,11 @@ import {
 	useMessage,
 } from "naive-ui";
 import { defineComponent, onMounted, reactive, watch } from "vue";
-import { CheckmarkCircle, Close } from "@vicons/ionicons5";
+import { Close } from "@vicons/ionicons5";
 import styles from "./index.module.less";
 import { getImage } from "/src/pc/home/images";
 import { ABC_DATA } from "/src/pc/home/runtime";
 import TheIcon from "/src/components/The-icon";
-import TheSpeed from "/src/pc/home/component/the-speed";
-import { api_musicSheetCreationSave, api_subjectList } from "/src/pc/api";
-import { initMusic } from "/src/pc/home";
 import { encodeUrl } from "/src/utils";
 const instruments = [
 	{
@@ -104,6 +101,7 @@ export default defineComponent({
 			speed: 80,
 			measure: 30,
 			subjectCode: "recorder",
+			subjectId: 4,
 		});
 
 		const handleCreate = async () => {
@@ -122,13 +120,15 @@ export default defineComponent({
 				key: froms.key.value,
 				subjectCode: froms.subjectCode,
 				measure: froms.measure,
+				subjectId: froms.subjectId
 			});
 			emit("create");
 			formsOptions.loading = false;
 		};
 
 		const handleOpenNotaion = (data: any) => {
-			const url = `${location.origin}/notation/#/?v=1.0.3&config=${encodeUrl(data)}`;
+			const url = `${location.origin}/notation/#/?v=1.0.4&config=${encodeUrl(data)}`;
+			// console.log("🚀 ~ url:", url);
 			window.parent.postMessage(
 				{
 					api: "notation_open",
@@ -141,6 +141,7 @@ export default defineComponent({
 
 		return () => (
 			<NModal
+				unstableShowMask={false}
 				transformOrigin="center"
 				autoFocus={false}
 				show={props.show}
@@ -167,6 +168,7 @@ export default defineComponent({
 									class={[styles.item, froms.subjectCode === item.key && styles.itemActive]}
 									onClick={() => {
 										froms.subjectCode = item.key;
+										froms.subjectId = item.id;
 									}}
 								>
 									<div class={styles.itemImg}>

+ 95 - 20
src/pc/create/index.module.less

@@ -1,7 +1,7 @@
 .createItem {
     position: relative;
-    width: 304px;
-    height: 233px;
+    width: 100%;
+    height: 100%;
     background: #F9FAFD;
     border: 2px solid rgba(209, 216, 235, 1);
     border-radius: 12px;
@@ -12,7 +12,8 @@
     font-size: 15px;
     font-weight: 600;
     cursor: pointer;
-    img{
+
+    img {
         width: 45px;
         height: 45px;
         margin-bottom: 23px;
@@ -20,14 +21,79 @@
 }
 
 .wrap {
-    padding: 24px;
+    padding: 24px 14px;
     border-radius: 15px;
     background-color: #fff;
 }
+
+.wrapBox {
+    display: flex;
+    flex-wrap: wrap;
+}
+
+.itemWrap {
+    position: relative;
+    width: calc(100% / 6);
+    padding-bottom: calc(100% / 6 * 0.76644);
+}
+
+@media screen and (max-width: 2400px) {
+    .wrapBox {
+        .itemWrap {
+            width: calc(100% / 5);
+            padding-bottom: calc(100% / 5 * 0.76644);
+        }
+    }
+}
+
+@media screen and (max-width: 1600px) {
+    .wrapBox {
+        .itemWrap {
+            width: calc(100% / 4);
+            padding-bottom: calc(100% / 4 * 0.76644);
+        }
+    }
+}
+
+@media screen and (max-width: 1200px) {
+    .wrapBox {
+        .itemWrap {
+            width: calc(100% / 3);
+            padding-bottom: calc(100% / 3 * 0.76644);
+        }
+    }
+}
+
+@media screen and (max-width: 820px) {
+    .wrapBox {
+        .itemWrap {
+            width: calc(100% / 2);
+            padding-bottom: calc(100% / 2 * 0.76644);
+        }
+    }
+}
+@media screen and (max-width: 600px) {
+    .wrapBox {
+        .itemWrap {
+            width: calc(100%);
+            padding-bottom: calc(100% * 0.76644);
+        }
+    }
+}
+
+.itemWrapBox {
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    padding: 10px;
+}
+
 .item {
     position: relative;
-    width: 304px;
-    height: 233px;
+    width: 100%;
+    height: 100%;
     background: #F9FAFD;
     border: 2px solid rgba(209, 216, 235, 1);
     border-radius: 12px;
@@ -35,59 +101,68 @@
     display: flex;
     flex-direction: column;
     overflow: hidden;
-    .icon_29{
+
+    .icon_29 {
         height: 14px;
         vertical-align: middle;
         margin-right: 6px;
         transform: translateY(-1px);
     }
-    .bottomBtn{
+
+    .bottomBtn {
         width: 30px;
         height: 30px;
         margin: 0 6px;
-        &:hover{
+
+        &:hover {
             opacity: .8;
         }
     }
-    .btn{
+
+    .btn {
         position: absolute;
         right: 10px;
         top: 16px;
         display: block;
         height: 30px;
-        &:hover{
-            opacity: .8;
-        }
     }
 
 }
-.imgBox{
+
+.imgBox {
     flex: 1;
     overflow: hidden;
     background-color: #fff;
+
     img {
         width: 100%;
     }
 }
-.itemBottom{
+
+.itemBottom {
     flex-shrink: 0;
     padding: 8px 12px;
     background: linear-gradient(to left, rgb(219, 241, 255) 0%, rgb(231, 249, 255) 100%);
     overflow: hidden;
-    .bottombox{
+
+    .bottombox {
         width: 100%;
         display: flex;
         align-items: center;
     }
-    .bottomLeft{
+
+    .bottomLeft {
         flex: 1;
         margin-right: 6px;
     }
-    .itemtitle{
+
+    .itemtitle {
         font-weight: 600;
     }
-    .time{
+
+    .time {
         font-size: 12px;
         color: #777;
     }
-}
+}
+

+ 200 - 171
src/pc/create/index.tsx

@@ -1,6 +1,6 @@
 import { defineComponent, nextTick, onMounted, onUnmounted, reactive, ref, watch } from "vue";
 import styles from "./index.module.less";
-import { NButton, NCheckbox, NRadio, NSpace, NSpin, useDialog } from "naive-ui";
+import { NButton, NCheckbox, NModal, NRadio, NSpace, NSpin, useDialog } from "naive-ui";
 import { getImage } from "../home/images";
 import TheCreate from "./component/the-create";
 import { storeData } from "/src/store";
@@ -8,182 +8,211 @@ import { api_musicSheetCreationPage, api_musicSheetCreationRemove } from "../api
 import { useRouter } from "vue-router";
 import ABCJS from "abcjs";
 import { usePageVisibility } from "@vant/use";
+import UploadToResources from "../component/upload-to-resources";
+import { getQuery } from "/src/utils/queryString";
 
 export default defineComponent({
-  name: "Create",
-  setup() {
-    const router = useRouter();
-    const dialog = useDialog();
-    console.log(storeData.user);
-    const forms = reactive({
-      teacherId: storeData.user.id,
-      page: 1,
-      keyword: "",
-      rows: 20,
-    });
-    const data = reactive({
-      list: [] as any[],
-      addShow: false,
-      loading: false,
-      finish: false,
-      isCreated: false,
-    });
-    const getList = async () => {
-      data.loading = true;
-      const res = await api_musicSheetCreationPage({ ...forms });
-      if (res?.code == 200) {
-        if (data.isCreated) {
-          data.isCreated = false;
-          handleOpenNotaion(res.data.rows[0]);
-        }
-        data.list = data.list.concat(res.data.rows);
-        data.finish = res.data.rows.length < forms.rows;
-      }
-      data.loading = false;
-    };
-    const handleReset = () => {
-      forms.page = 1;
-      data.finish = false;
-      data.list = [];
-      getList();
-    };
-    const pageVisibility = usePageVisibility();
-    watch(pageVisibility, (val) => {
-      if (val === "visible") {
-        handleReset();
-      }
-    });
+	name: "Create",
+	setup() {
+		const query = getQuery();
+		const dialog = useDialog();
+		console.log(storeData.user);
+		const forms = reactive({
+			teacherId: storeData.user.id,
+			page: 1,
+			keyword: "",
+			rows: 20,
+		});
+		const data = reactive({
+			list: [] as any[],
+			addShow: query.addShow ? true : false,
+			loading: false,
+			finish: false,
+			isCreated: false,
+			uploadShow: false,
+			item: {} as any,
+		});
+		const getList = async () => {
+			data.loading = true;
+			const res = await api_musicSheetCreationPage({ ...forms });
+			if (res?.code == 200) {
+				if (data.isCreated) {
+					data.isCreated = false;
+					handleOpenNotaion(res.data.rows[0]);
+				}
+				data.list = data.list.concat(res.data.rows);
+				data.finish = res.data.rows.length < forms.rows;
+			}
+			data.loading = false;
+		};
+		const handleReset = () => {
+			forms.page = 1;
+			data.finish = false;
+			data.list = [];
+			getList();
+		};
+		const pageVisibility = usePageVisibility();
+		watch(pageVisibility, (val) => {
+			if (val === "visible") {
+				handleReset();
+			}
+		});
 
-    const handleDelte = (item: any) => {
-      const checked = ref(true);
-      dialog.warning({
-        autoFocus: false,
-        class: "deleteDialog",
-        title: "删除曲谱",
-        // content: () => (
-        // 	<div onClick={() => checked.value = !checked.value}>
-        // 		<NRadio checked={checked.value}>同步删除我的资源中的该曲目</NRadio>
-        // 	</div>
-        // ),
-        content: () => <div style={{ paddingRight: "calc(var(--n-close-size) + 6px)" }}>确认删除当前曲谱?</div>,
-        positiveText: "取消",
-        positiveButtonProps: {
-          type: "default",
-        },
-        negativeText: "删除",
-        negativeButtonProps: {
-          type: "primary",
-          ghost: false,
-        },
-        onPositiveClick: () => {},
-        onNegativeClick: async () => {
-          await api_musicSheetCreationRemove(item.id);
-          handleReset();
-        },
-      });
-    };
-    const loadingRef = ref();
+		const handleDelte = (item: any) => {
+			const checked = ref(true);
+			dialog.warning({
+				autoFocus: false,
+				class: "deleteDialog",
+				title: "删除曲谱",
+				content: () => (
+					<div onClick={() => checked.value = !checked.value}>
+						<NRadio checked={checked.value}>同步删除我的资源中的该曲目</NRadio>
+					</div>
+				),
+				// content: () => <div>确认删除当前曲谱?</div>,
+				positiveText: "取消",
+				positiveButtonProps: {
+					type: "default",
+				},
+				negativeText: "删除",
+				negativeButtonProps: {
+					type: "primary",
+					ghost: false,
+				},
+				onPositiveClick: () => {},
+				onNegativeClick: async () => {
+					await api_musicSheetCreationRemove(item.id, checked.value ? 1 : 0);
+					handleReset();
+				},
+			});
+		};
+		const loadingRef = ref();
 
-    const messageEvent = (params?: any) => {
-      // 在老师端里面关闭要刷新
-      if (params.data?.api == "reload") {
-        handleReset();
-      }
-    };
-    onMounted(() => {
-      getList();
-      if (loadingRef.value) {
-        const obv = new IntersectionObserver((entries) => {
-          if (entries[0].isIntersecting) {
-            if (data.finish || data.loading) return;
-            forms.page++;
-            getList();
-          }
-        });
-        obv.observe(loadingRef.value?.$el);
-      }
+		const messageEvent = (params?: any) => {
+			// 在老师端里面关闭要刷新
+			if (params.data?.api == "reload") {
+				handleReset();
+			}
+		};
+		onMounted(() => {
+			getList();
+			if (loadingRef.value) {
+				const obv = new IntersectionObserver((entries) => {
+					if (entries[0].isIntersecting) {
+						if (data.finish || data.loading) return;
+						forms.page++;
+						getList();
+					}
+				});
+				obv.observe(loadingRef.value?.$el);
+			}
 
-      window.addEventListener("message", (params?: any) => {
-        messageEvent(params);
-      });
-    });
+			window.addEventListener("message", (params?: any) => {
+				messageEvent(params);
+			});
+		});
 
-    onUnmounted(() => {
-      window.removeEventListener("message", messageEvent);
-    });
-    const handleOpenNotaion = (item: any) => {
-      window.parent.postMessage(
-        {
-          api: "notation_open",
-          url: `${location.origin}/notation/#/?v=1.0.3&id=${item.id}`,
-        },
-        "*"
-      );
-    };
-    const productSvg = (abc: string, id: string) => {
-      if (abc) {
-        const a = ABCJS.renderAbc(id, abc, { selectTypes: false, add_classes: true });
-      }
-    };
-    return () => (
-      <div class={styles.wrap}>
-        <NSpace size={18}>
-          <div class={styles.createItem} onClick={() => (data.addShow = true)}>
-            <img src={getImage("icon_29.png")} />
-            <div>新建乐谱</div>
-          </div>
+		onUnmounted(() => {
+			window.removeEventListener("message", messageEvent);
+		});
+		const handleOpenNotaion = (item: any) => {
+			window.parent.postMessage(
+				{
+					api: "notation_open",
+					url: `${location.origin}/notation/#/?v=1.0.3&id=${item.id}`,
+				},
+				"*"
+			);
+		};
+		const productSvg = (abc: string, id: string) => {
+			const a = ABCJS.renderAbc(id, abc, { selectTypes: false, add_classes: true })[0];
+			return a;
+		};
 
-          {data.list.map((item, index: number) => (
-            <div class={styles.item} onClick={() => handleOpenNotaion(item)}>
-              <div class={styles.imgBox} id={"item_" + index}>
-                <img
-                  src={getImage("icon_staff.png")}
-                  onLoad={() => {
-                    productSvg(item.creationConfig, "item_" + index);
-                  }}
-                />
-              </div>
-              <div class={styles.itemBottom}>
-                <div class={styles.bottombox}>
-                  <div class={styles.bottomLeft}>
-                    <div class={styles.itemtitle}>
-                      <img class={styles.icon_29} src={getImage("icon_29_1.png")} />
-                      <span>{item.name}</span>
-                    </div>
-                    <div class={styles.time}>{item.updateTime}</div>
-                  </div>
-                  {/* <img
-										style={{ pointerEvents: "none", opacity: 0.3 }}
-										class={styles.bottomBtn}
-										src={getImage("icon_29_2.png")}
-									/> */}
-                  <img
-                    class={styles.bottomBtn}
-                    src={getImage("icon_29_3.png")}
-                    onClick={(e: Event) => {
-                      e.stopPropagation();
-                      handleDelte(item);
-                    }}
-                  />
-                </div>
-              </div>
-              {/* <img class={styles.btn} src={getImage("icon_29_4.png")} /> */}
-            </div>
-          ))}
-        </NSpace>
-        {!data.finish && (
-          <NSpace ref={loadingRef} justify="center" style={{ padding: "30px" }}>
-            <NSpin size="large" />
-          </NSpace>
-        )}
+    const handleSuccess = () => {
+      data.list.find((item: any) => item.id === data.item.id).uploadStatus = "YES";
+    }
+		return () => (
+			<div class={styles.wrap}>
+				<div class={styles.wrapBox}>
+					<div class={styles.itemWrap}>
+						<div class={styles.itemWrapBox}>
+							<div class={styles.createItem} onClick={() => (data.addShow = true)}>
+								<img src={getImage("icon_29.png")} />
+								<div>新建乐谱</div>
+							</div>
+						</div>
+					</div>
 
-        <TheCreate
-          v-model:show={data.addShow}
-          onCreate={() => {
-            data.addShow = false;
-          }}
-        />
-      </div>
-    );
-  },
+					{data.list.map((item, index: number) => (
+						<div class={styles.itemWrap}>
+							<div class={styles.itemWrapBox}>
+								<div class={styles.item} onClick={() => handleOpenNotaion(item)}>
+									<div class={styles.imgBox} id={"item_" + index}>
+										<img
+											src={getImage("icon_staff.png")}
+											onLoad={() => {
+												item.visualObj = productSvg(item.creationConfig, "item_" + index);
+											}}
+										/>
+									</div>
+									<div class={styles.itemBottom}>
+										<div class={styles.bottombox}>
+											<div class={styles.bottomLeft}>
+												<div class={styles.itemtitle}>
+													<span>{item.name || `未命名乐谱-${index + 1}`}</span>
+												</div>
+												<div class={styles.time}>{item.updateTime}</div>
+											</div>
+											{item.uploadStatus !== "YES" && (
+												<img
+													class={styles.bottomBtn}
+													src={getImage("icon_29_2.png")}
+													onClick={(e: Event) => {
+                            e.stopPropagation();
+														data.item = { ...item };
+														nextTick(() => {
+															data.uploadShow = true;
+														});
+													}}
+												/>
+											)}
+											<img
+												class={styles.bottomBtn}
+												src={getImage("icon_29_3.png")}
+												onClick={(e: Event) => {
+													e.stopPropagation();
+													handleDelte(item);
+												}}
+											/>
+										</div>
+									</div>
+									{item.uploadStatus === "YES" && (
+										<img class={styles.btn} src={getImage("icon_29_4.png")} />
+									)}
+									{item.uploadStatus === "UPDATE" && (
+										<img class={styles.btn} src={getImage("icon_29_5.png")} />
+									)}
+								</div>
+							</div>
+						</div>
+					))}
+				</div>
+				{!data.finish && (
+					<NSpace ref={loadingRef} justify="center" style={{ padding: "30px" }}>
+						<NSpin size="large" />
+					</NSpace>
+				)}
+
+				<TheCreate
+					v-model:show={data.addShow}
+					onCreate={() => {
+						data.addShow = false;
+					}}
+				/>
+
+				<UploadToResources v-model:show={data.uploadShow} item={data.item} onSuccess={() => handleSuccess()} />
+			</div>
+		);
+	},
 });

+ 97 - 87
src/pc/home/component/file-btn/index.tsx

@@ -1,5 +1,5 @@
-import { NDropdown } from "naive-ui";
-import { defineComponent } from "vue";
+import { NDropdown, NSpin } from "naive-ui";
+import { computed, defineComponent, ref, watch } from "vue";
 import styles from "./index.module.less";
 import { getImage } from "../../images";
 import { DropdownMixedOption } from "naive-ui/es/dropdown/src/interface";
@@ -20,95 +20,105 @@ export type IFileBtnType =
 export default defineComponent({
 	name: "FileBtn",
 	emits: ["select"],
+	props: {
+		saveLoading: {
+			type: Boolean,
+			default: false,
+		},
+	},
 	setup(props, { emit }) {
-		const options: DropdownMixedOption[] = [
-			{
-				label: () => (
-					<div class={styles.dropItem}>
-						<img class={styles.dropIcon} src={getImage("icon_26_4.png")} />
-						<span>新建曲谱</span>
-					</div>
-				),
-				key: "newMusic",
-			},
-			{
-				label: () => (
-					<div class={styles.dropItem}>
-						<img class={styles.dropIcon} src={getImage("icon_26_0.png")} />
-						<span>保存</span>
-					</div>
-				),
-				key: "save",
-			},
-			{
-				label: () => (
-					<div class={styles.dropItem}>
-						<img class={styles.dropIcon} src={getImage("icon_26_0.png")} />
-						<span>导入</span>
-					</div>
-				),
-				key: "import",
-				// disabled: true,
-				children: [
-					{
-						label: "XML",
-						key: "xml",
-						// disabled: true,
-					},
-				],
-			},
-			{
-				label: () => (
-					<div class={styles.dropItem}>
-						<img class={styles.dropIcon} src={getImage("icon_26_1.png")} />
-						<span>上传到我的资源</span>
-					</div>
-				),
-				key: "upload",
-				disabled: true,
-			},
-			{
-				label: () => (
-					<div class={styles.dropItem}>
-						<img class={styles.dropIcon} src={getImage("icon_26_2.png")} />
-						<span>导出</span>
-					</div>
-				),
-				key: "export",
-				children: [
-					{
-						label: "XML",
-						key: "down-xml",
-					},
-					{
-						label: "PNG",
-						key: "png",
-					},
-					{
-						label: "WAV",
-						key: "wav",
-					},
-					{
-						label: "MIDI",
-						key: "midi",
-					},
-				],
-			},
-			{
-				label: () => (
-					<div class={styles.dropItem}>
-						<img class={styles.dropIcon} src={getImage("icon_26_3.png")} />
-						<span>打印</span>
-					</div>
-				),
-				key: "print",
-				disabled: true,
-			},
-		];
+		const options = computed(() => {
+			return [
+				{
+					label: () => (
+						<div class={styles.dropItem}>
+							<img class={styles.dropIcon} src={getImage("icon_26_4.png")} />
+							<span>新建曲谱</span>
+						</div>
+					),
+					key: "newMusic",
+				},
+				{
+					label: () => (
+						<div class={styles.dropItem}>
+							<img class={styles.dropIcon} src={getImage("icon_26_0.png")} />
+							<span>保存</span>
+							{props.saveLoading && <NSpin style={{ marginLeft: "auto" }} size={14}></NSpin>}
+						</div>
+					),
+					key: "save",
+					disabled: props.saveLoading,
+				},
+				{
+					label: () => (
+						<div class={styles.dropItem}>
+							<img class={styles.dropIcon} src={getImage("icon_26_0.png")} />
+							<span>导入</span>
+						</div>
+					),
+					key: "import",
+					// disabled: true,
+					children: [
+						{
+							label: "XML",
+							key: "xml",
+							// disabled: true,
+						},
+					],
+				},
+				{
+					label: () => (
+						<div class={styles.dropItem}>
+							<img class={styles.dropIcon} src={getImage("icon_26_1.png")} />
+							<span>上传到我的资源</span>
+						</div>
+					),
+					key: "upload",
+					// disabled: true,
+				},
+				{
+					label: () => (
+						<div class={styles.dropItem}>
+							<img class={styles.dropIcon} src={getImage("icon_26_2.png")} />
+							<span>导出</span>
+						</div>
+					),
+					key: "export",
+					children: [
+						{
+							label: "XML",
+							key: "down-xml",
+						},
+						{
+							label: "PNG",
+							key: "png",
+						},
+						{
+							label: "WAV",
+							key: "wav",
+						},
+						{
+							label: "MIDI",
+							key: "midi",
+						},
+					],
+				},
+				{
+					label: () => (
+						<div class={styles.dropItem}>
+							<img class={styles.dropIcon} src={getImage("icon_26_3.png")} />
+							<span>打印</span>
+						</div>
+					),
+					key: "print",
+					disabled: true,
+				},
+			];
+		});
 		return () => (
 			<NDropdown
 				class={styles.dropWrap}
-				options={options}
+				options={options.value}
 				trigger="click"
 				onSelect={(val) => {
 					console.log("🚀 ~ val:", val);

+ 6 - 2
src/pc/home/component/the-setting/index.tsx

@@ -92,8 +92,12 @@ export default defineComponent({
 				value: "B",
 			},
 			{
-				label: "BackSpace",
-				value: "删除音符",
+				label: "删除音符",
+				value: "BackSpace",
+			},
+			{
+				label: "撤回",
+				value: "Ctrl + z",
 			},
 		];
 		return () => (

二進制
src/pc/home/images/icon_29_4.png


二進制
src/pc/home/images/icon_29_5.png


+ 11 - 14
src/pc/home/index.module.less

@@ -168,18 +168,18 @@
 }
 
 :global {
-    .n-modal-scroll-content .n-modal-mask{
-        background-color: rgba(0, 0, 0, .6);
-    }
-    .deleteDialog.saveDialog{
+    .deleteDialog.saveDialog {
         width: 338px;
-        .n-dialog__title{
+
+        .n-dialog__title {
             padding-right: 0 !important;
         }
-        .n-dialog__content{
+
+        .n-dialog__content {
             color: #777;
         }
     }
+
     .abcjs-note-hover {
         &:hover {
             fill: #ffe65948;
@@ -187,13 +187,9 @@
         }
     }
 
-    .abcjs-bar {
-        .abcjs-annotation {
-            display: block;
-            font-size: 12px;
-            font-style: italic;
-            transform: translateX(10px);
-        }
+    .abcjs-bar-number {
+        font-size: 12px;
+        transform: translateY(-5px);
     }
 
 
@@ -338,7 +334,8 @@
     align-items: center;
     z-index: 100;
 }
-.exportPng{
+
+.exportPng {
     position: fixed;
     left: 0;
     top: 0;

+ 268 - 94
src/pc/home/index.tsx

@@ -65,6 +65,9 @@ import { saveAs } from "file-saver";
 import qs from "query-string";
 import { useDocumentVisibility } from "@vueuse/core";
 import request from "/src/utils/request";
+import { api_uploadFile } from "/src/utils/uploadFile";
+import { bufferToWave } from "/src/helpers/parseABC";
+import UploadToResources from "../component/upload-to-resources";
 
 export const initMusic = (total: number): IMeasure[] => {
 	return new Array(total).fill(0).map((item, index) => {
@@ -132,10 +135,12 @@ export default defineComponent({
 			selectMearesShow: false, // 选择小节弹窗
 		});
 		const data = reactive({
+			uploadStatus: "",
+			saveLoading: false,
 			loading: true,
 			drawCount: 0,
 			isSave: true,
-			musicId: "",
+			musicId: Date.now().toString(),
 			musicName: "", // 曲谱名称
 			creator: "", // 创建者
 			subjectId: "", // 声部
@@ -175,6 +180,13 @@ export default defineComponent({
 			moveKeyType: "inset" as "inset" | "up" | "down", // 移调类型
 			activePlayNote: null as any, // 当前演奏音符
 			times: [] as any[], // 节拍器数据
+
+			undoList: [] as any[], // 撤销列表
+			redoList: [] as any[], // 重做列表
+
+			uploadShow: false, // 上传弹窗
+			item: {} as any, // 上传数据
+			uploadClick: false, // 上传点击
 		});
 		const noteTypes = ABC_DATA.types.map((item) => item.value).filter(Boolean);
 		const accidentals = ABC_DATA.accidentals.map((item) => item.value).filter(Boolean);
@@ -687,6 +699,46 @@ export default defineComponent({
 			}
 		};
 
+		const handleClickExit = async () => {
+			if (data.saveLoading) return;
+			const msg = message.loading("保存中...", { duration: 0 });
+			await handleSaveMusic(false);
+			setTimeout(async () => {
+				msg.type = "success";
+				msg.content = "保存成功";
+				setTimeout(() => {
+					msg.destroy();
+				}, 500);
+			}, 300);
+			if (data.uploadStatus !== "NO") {
+				dialog.warning({
+					maskClosable: true,
+					autoFocus: false,
+					class: "deleteDialog saveDialog",
+					title: "温馨提示",
+					content: "是否更新到我的资源?",
+					positiveText: "更新",
+					positiveButtonProps: {
+						type: "primary",
+					},
+					negativeText: "不更新",
+					negativeButtonProps: {
+						type: "default",
+						ghost: false,
+					},
+					onPositiveClick: async () => {
+						data.uploadClick = true;
+						await handleUpdate();
+					},
+					onNegativeClick: () => {
+						handleClose();
+					},
+				});
+			} else {
+				handleClose();
+			}
+		};
+
 		/**
 		 *
 		 * @param key
@@ -694,6 +746,11 @@ export default defineComponent({
 		 * @returns
 		 */
 		const handleChange = async (params: { type: string; value: any }) => {
+			// 最多记录30步
+			if (data.undoList.length > 30) {
+				data.undoList.shift();
+			}
+			data.undoList.push(cleanDeep(abcData.abc));
 			abcData.synthControl.disable(true);
 			if (data.playState) {
 				data.playState = false;
@@ -706,6 +763,7 @@ export default defineComponent({
 			if (type === "exit") {
 				if (!data.isSave) {
 					clearTimeout(saveTimer);
+					data.uploadClick = false;
 					dialog.warning({
 						maskClosable: true,
 						autoFocus: false,
@@ -721,17 +779,8 @@ export default defineComponent({
 							type: "default",
 							ghost: false,
 						},
-						onPositiveClick: async () => {
-							const msg = message.loading("保存中...");
-							await handleSaveMusic(false);
-							setTimeout(() => {
-								msg.type = "success";
-								msg.content = "保存成功";
-								setTimeout(() => {
-									msg.destroy();
-									handleClose();
-								}, 500);
-							}, 300);
+						onPositiveClick: () => {
+							handleClickExit();
 						},
 						onNegativeClick: () => {
 							handleClose();
@@ -888,9 +937,15 @@ export default defineComponent({
 			// 谱号
 			if (type === "clef") {
 				if (data.active) {
-					if (!activeNote) return;
-					activeNote.clef = `[${value}]`;
-					await handleResetRender();
+					if (data.active.measureIndex === 0 && data.active.noteIndex === 0) {
+						abcData.abc.celf = value;
+						handleResetRender();
+					} else {
+						if (!activeNote) return;
+						activeNote.clef = `[${value}]`;
+						await handleResetRender();
+					}
+					rangeHighlight(data.active.startChar);
 				} else {
 					abcData.abc.celf = value;
 					handleResetRender();
@@ -900,11 +955,21 @@ export default defineComponent({
 			// 调号
 			if (type === "key") {
 				if (data.active) {
-					if (!activeNote) return;
-					activeNote.key = `[${value}]`;
-					await handleResetRender();
+					if (data.active.measureIndex === 0 && data.active.noteIndex === 0) {
+						abcData.abc.key = value;
+						abcData.abc.visualTranspose = 0;
+						abcData.abc.visualKey = "K:C";
+						await handleResetRender();
+					} else {
+						if (!activeNote) return;
+						activeNote.key = `[${value}]`;
+						await handleResetRender();
+					}
+					rangeHighlight(data.active.startChar);
 				} else {
 					abcData.abc.key = value;
+					abcData.abc.visualTranspose = 0;
+					abcData.abc.visualKey = "K:C";
 					await handleResetRender();
 				}
 			}
@@ -1050,9 +1115,9 @@ export default defineComponent({
 				if (value === "|:") {
 					const prevMeasure = abcData.abc.measures[data.active.measureIndex - 1] || null;
 					if (!prevMeasure) return;
-					prevMeasure.barline = value;
+					prevMeasure.barline = prevMeasure.barline === value ? "|" : value;
 				} else {
-					activeMeasure.barline = value;
+					activeMeasure.barline = activeMeasure.barline === value ? "|" : value;
 				}
 				await handleResetRender();
 			}
@@ -1235,6 +1300,7 @@ export default defineComponent({
 			// 			? item.step
 			// 			: item.step + 12
 			// 		: item.step;
+			console.log(item);
 			abcData.abc.visualTranspose = item.step;
 			abcData.abc.visualKey = item.value;
 			popup.moveKeyShow = false;
@@ -1245,10 +1311,37 @@ export default defineComponent({
 			await handleResetRender();
 		};
 
+		const keyDownData = reactive({
+			wait: false,
+			control: false,
+		});
+		const handleKeyDonw = async (e: KeyboardEvent) => {
+			if ((e.target as HTMLElement).nodeName === "INPUT") return;
+			if (e.key === "Control" || e.key === "Meta") {
+				keyDownData.control = true;
+			}
+			if (e.key === "z" && keyDownData.control && !keyDownData.wait) {
+				e.preventDefault();
+				if (data.undoList.length) {
+					abcData.abc = cloneDeep(data.undoList[data.undoList.length - 1]);
+					data.undoList.pop();
+					keyDownData.wait = true;
+					await handleResetRender();
+					nextTick(() => {
+						keyDownData.wait = false;
+					});
+				}
+			}
+		};
+
 		const handleKeyUp = (e: KeyboardEvent) => {
 			if ((e.target as HTMLElement).nodeName === "INPUT") return;
-			if (!data.active) return false;
 			console.log(e.key);
+
+			if (e.key === "Control" || e.key === "Meta") {
+				keyDownData.control = false;
+			}
+			if (!data.active) return false;
 			if (e.key === "Backspace") {
 				handleChange({ type: "delete", value: "" });
 			}
@@ -1284,9 +1377,11 @@ export default defineComponent({
 			data.loading = true;
 			const res = await api_musicSheetCreationDetail(query.id);
 			if (res?.code == 200) {
+				data.uploadStatus = res.data.uploadStatus || "";
 				data.musicId = res.data.id || "";
 				data.musicName = res.data.name || "";
 				data.creator = res.data.creator || "";
+				data.subjectId = res.data.subjectId || "";
 				let abc = "" as any;
 				try {
 					abc = JSON.parse(res.data.creationData);
@@ -1312,56 +1407,79 @@ export default defineComponent({
 			data.loading = false;
 			return res;
 		};
+		const setSaveLoading = (tips: boolean) => {
+			data.saveLoading = true;
+			if (tips) {
+				message.loading("保存中...", { duration: 0 });
+			}
+		};
 		const handleSaveMusic = async (tips = true) => {
 			const query = getQuery();
 			abcData.abc.title = data.musicName;
 			abcData.abc.creator = data.creator;
-			if (query.id) {
-				await api_musicSheetCreationUpdate({
-					name: data.musicName,
-					creator: data.creator,
-					creationConfig: renderMeasures(abcData.abc, {
-						hiddenIndex: true,
-						showTitle: true,
-						showCreator: true,
-					}),
-					creationData: JSON.stringify(cleanDeep(abcData.abc)),
-					id: query.id,
-					subjectId: "",
-				});
-			} else {
-				const res = await api_musicSheetCreationSave({
-					name: data.musicName,
-					creator: data.creator,
-					creationConfig: renderMeasures(abcData.abc, {
-						hiddenIndex: true,
-						showTitle: true,
-						showCreator: true,
-					}),
-					creationData: JSON.stringify(cleanDeep(abcData.abc)),
-					subjectId: "",
-				});
-				if (res?.data) {
-					const hash = location.hash.split("?");
-					const qs_data = qs.parse(hash[1]);
-					qs_data.id = res.data;
-					try {
-						delete qs_data.config;
-					} catch (error) {
-						console.log("🚀 ~ error:", error);
+			setSaveLoading(tips);
+			const wavUrl = await productWav(false);
+			const pngUrl = await productPng(false);
+			console.log("🚀 ~ pngUrl:", pngUrl);
+			try {
+				if (query.id) {
+					await api_musicSheetCreationUpdate({
+						name: data.musicName || "未命名乐谱",
+						creator: data.creator || "未命名乐谱",
+						creationConfig: renderMeasures(abcData.abc, {
+							hiddenIndex: true,
+							showTitle: true,
+							showCreator: true,
+						}),
+						creationData: JSON.stringify(cleanDeep(abcData.abc)),
+						id: query.id,
+						subjectId: data.subjectId,
+						filePath: wavUrl,
+						coverImg: pngUrl,
+					});
+				} else {
+					const res = await api_musicSheetCreationSave({
+						name: data.musicName || "未命名乐谱",
+						creator: data.creator || "未命名乐谱",
+						creationConfig: renderMeasures(abcData.abc, {
+							hiddenIndex: true,
+							showTitle: true,
+							showCreator: true,
+						}),
+						creationData: JSON.stringify(cleanDeep(abcData.abc)),
+						subjectId: data.subjectId,
+						filePath: wavUrl,
+						coverImg: pngUrl,
+					});
+					if (res?.data) {
+						const hash = location.hash.split("?");
+						const qs_data = qs.parse(hash[1]);
+						qs_data.id = res.data;
+						try {
+							delete qs_data.config;
+						} catch (error) {
+							console.log("🚀 ~ error:", error);
+						}
+						location.hash = hash[0] + "?" + qs.stringify(qs_data);
 					}
-					location.hash = hash[0] + "?" + qs.stringify(qs_data);
 				}
+			} catch (error) {
+				console.log(error);
 			}
+
 			if (tips) {
+				message.destroyAll();
 				message.success("保存成功");
 			}
 			data.isSave = true;
+			data.saveLoading = false;
+			data.uploadClick = false;
 		};
 		const hanldeInitCreate = () => {
 			const query = getQuery();
 			const abc = decodeUrl(query.config);
 			console.log("🚀 ~ abc:", abc);
+			data.subjectId = abc.subjectId || "";
 			abcData.abc.celf = abc.celf ?? "K:treble";
 			abcData.abc.key = abc.key ?? "K:C";
 			abcData.abc.meter = abc.meter ?? "M:4/4";
@@ -1406,6 +1524,7 @@ export default defineComponent({
 			await handleResetRender();
 			loadMiniMp3();
 			document.addEventListener("keyup", handleKeyUp);
+			document.addEventListener("keydown", handleKeyDonw);
 			window.onbeforeunload = (e) => {
 				if (!data.isSave) {
 					e.preventDefault();
@@ -1423,6 +1542,7 @@ export default defineComponent({
 		});
 		onUnmounted(() => {
 			document.removeEventListener("keyup", handleKeyUp);
+			document.removeEventListener("keydown", handleKeyDonw);
 		});
 
 		const measureComputed = computed(() => {
@@ -1497,36 +1617,53 @@ export default defineComponent({
 			handleResetRender();
 		};
 
+		const productPng = (isUrl = true) => {
+			return new Promise((resolve) => {
+				const paper = document.getElementById("exportPng");
+				if (!paper) return;
+				const abc = renderMeasures(abcData.abc, {
+					hiddenIndex: true,
+					showTitle: true,
+					showCreator: true,
+				});
+				ABCJS.renderAbc(paper, abc, abcData.abcOptions);
+				const svg: any = paper.children[0]?.cloneNode(true);
+				const svgBox = paper.getBoundingClientRect();
+				svg.setAttribute("width", `${svgBox.width * 3}`);
+				svg.setAttribute("height", `${svgBox.height * 3}`);
+				const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+				rect.setAttribute("x", "0");
+				rect.setAttribute("y", "0");
+				rect.setAttribute("width", `${svgBox.width * 10}`);
+				rect.setAttribute("height", `${svgBox.height * 10}`);
+				rect.setAttribute("fill", "#fff");
+				svg.prepend(rect);
+				if (svg) {
+					const _canvas = svg2canvas(svg.outerHTML);
+					if (isUrl) {
+						// document.body.appendChild(_canvas);
+						let el: any = document.createElement("a");
+						// 设置 href 为图片经过 base64 编码后的字符串,默认为 png 格式
+						el.href = _canvas.toDataURL();
+						el.download = data.musicName + ".png";
+
+						// 创建一个点击事件并对 a 标签进行触发
+						const event = new MouseEvent("click");
+						el.dispatchEvent(event);
+					} else {
+						_canvas.toBlob(async (blob) => {
+							const pngUrl = await api_uploadFile(blob, data.musicId + ".png");
+							resolve(pngUrl);
+						}, "image/png");
+					}
+				}
+			});
+		};
+
 		const downPng = async () => {
 			abcData.abc.title = `T:${data.musicName}`;
 			abcData.abc.creator = `R:${data.creator}`;
-			const paper = document.getElementById("exportPng");
-			if (!paper) return;
-			const abc = renderMeasures(abcData.abc, { hiddenIndex: true, showTitle: true, showCreator: true });
-			ABCJS.renderAbc(paper, abc, abcData.abcOptions);
-			const svg: any = paper.children[0]?.cloneNode(true);
-			const svgBox = paper.getBoundingClientRect();
-			svg.setAttribute("width", `${svgBox.width * 3}`);
-			svg.setAttribute("height", `${svgBox.height * 3}`);
-			const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
-			rect.setAttribute("x", "0");
-			rect.setAttribute("y", "0");
-			rect.setAttribute("width", `${svgBox.width * 10}`);
-			rect.setAttribute("height", `${svgBox.height * 10}`);
-			rect.setAttribute("fill", "#fff");
-			svg.prepend(rect);
-			if (svg) {
-				const _canvas = svg2canvas(svg.outerHTML);
-				// document.body.appendChild(_canvas);
-				let el: any = document.createElement("a");
-				// 设置 href 为图片经过 base64 编码后的字符串,默认为 png 格式
-				el.href = _canvas.toDataURL();
-				el.download = data.musicName + ".png";
-
-				// 创建一个点击事件并对 a 标签进行触发
-				const event = new MouseEvent("click");
-				el.dispatchEvent(event);
-			}
+			productPng();
 		};
 
 		const downRef = ref();
@@ -1540,12 +1677,8 @@ export default defineComponent({
 			downRef.value.innerHTML = midi;
 			downRef.value.querySelector("a").click();
 		};
-		const downWav = () => {
-			try {
-				if (abcData.synthControl) {
-					abcData.synthControl.download("曲谱.wav");
-				}
-			} catch (error) {
+		const productWav = async (isUrl = true) => {
+			return new Promise((resolve) => {
 				const midiBuffer = new ABCJS.synth.CreateSynth();
 				midiBuffer
 					.init({
@@ -1553,11 +1686,25 @@ export default defineComponent({
 						options: abcData.synthOptions,
 					})
 					.then(() => {
-						midiBuffer.prime().then(() => {
-							// console.log(midiBuffer.download());
-							downloadFile(midiBuffer.download(), "曲谱.wav");
+						midiBuffer.prime().then(async () => {
+							if (isUrl) {
+								downloadFile(midiBuffer.download(), (data.musicName || "曲谱") + ".wav");
+							} else {
+								const blob = bufferToWave((midiBuffer as any).getAudioBuffer());
+								const wavurl = await api_uploadFile(blob, data.musicId + ".wav");
+								resolve(wavurl);
+							}
 						});
 					});
+			});
+		};
+		const downWav = () => {
+			try {
+				if (abcData.synthControl) {
+					abcData.synthControl.download((data.musicName || "曲谱") + ".wav");
+				}
+			} catch (error) {
+				productWav();
 			}
 		};
 
@@ -1658,6 +1805,20 @@ export default defineComponent({
 			}
 		};
 
+		const handleUpdate = async () => {
+			if (!data.isSave) {
+				await handleSaveMusic();
+			}
+			const query = getQuery();
+			const res = await api_musicSheetCreationDetail(query.id);
+			if (res.data) {
+				if (res.data.uploadStatus !== "YES") {
+					data.item = { ...res.data, visualObj: abcData.visualObj };
+					data.uploadShow = true;
+				}
+			}
+		};
+
 		return () => (
 			<>
 				<div class={styles.container}>
@@ -1671,6 +1832,7 @@ export default defineComponent({
 							</div>
 							<div class={styles.topBtn}>
 								<FileBtn
+									saveLoading={data.saveLoading}
 									onSelect={(val: IFileBtnType) => {
 										if (val === "newMusic") {
 											handleCreateMusic();
@@ -1679,13 +1841,11 @@ export default defineComponent({
 										} else if (["xml"].includes(val)) {
 											handleExport();
 										} else if (val === "upload") {
+											handleUpdate();
 										} else if (["png", "midi", "wav", "down-xml"].includes(val)) {
 											handleDownFile(val);
 										} else if (val === "print") {
 										}
-										//  else if (val === "exit") {
-
-										// }
 									}}
 								/>
 								<div>文件</div>
@@ -2587,6 +2747,20 @@ export default defineComponent({
 				<div class={styles.exportPng}>
 					<div id="exportPng"></div>
 				</div>
+
+				<UploadToResources
+					v-model:show={data.uploadShow}
+					item={data.item}
+					onSuccess={() => {
+						data.uploadStatus = "YES";
+						message.success("上传成功");
+						if (data.uploadClick) {
+							setTimeout(() => {
+								handleClose();
+							}, 300);
+						}
+					}}
+				/>
 			</>
 		);
 	},

+ 20 - 6
src/pc/home/runtime.ts

@@ -34,13 +34,13 @@ export const ABC_DATA = {
 	],
 	/** 调号 */
 	key: [
-		{ name: "C大调", value: "K:C", step: 0, icon: "icon-a-diaohao-cdadiaoaxiaodiao1" },
 		{ name: "F#大调", value: "K:F#", step: 6, icon: "icon-a-diaohao-fdadiaodxiaodiao" },
 		{ name: "F大调", value: "K:F", step: 5, icon: "icon-a-diaohao-fdadiaodxiaodiao1" },
 		{ name: "E大调", value: "K:E", step: 4, icon: "icon-a-diaohao-edadiaocxiaodiao" },
 		{ name: "Eb大调", value: "K:Eb", step: 3, icon: "icon-a-diaohao-ebdadiaocxiaodiao" },
 		{ name: "D大调", value: "K:D", step: 2, icon: "icon-a-diaohao-Ddaxiaoexiaodiao" },
 		{ name: "C#大调", value: "K:C#", step: 1, icon: "icon-a-diaohao-cdadiaoaxiaodiao" },
+		{ name: "C大调", value: "K:C", step: 0, icon: "icon-a-diaohao-cdadiaoaxiaodiao1" },
 		{ name: "B大调", value: "K:B", step: -1, icon: "icon-a-diaohao-bdadiaogxiaodiao" },
 		{ name: "Cb大调", value: "K:Cb", step: -1, icon: "icon-a-diaohao-cbdadiaoabxiaodiao" },
 		{ name: "Db大调", value: "K:Db", step: -1, icon: "icon-a-diaohao-dbdadiaobbxiaodiao" },
@@ -202,11 +202,25 @@ export const renderMeasures = (abc: IAbc, option?: IRenderMeasuresOption) => {
 	if (option?.showCreator) {
 		abc.creator && (text += `C:${abc.creator}` + "\n");
 	}
+	if (!option?.hiddenIndex) {
+		text += "%%barnumbers 1" + "\n";
+	}
+
 	abc.celf && (text += abc.celf + "\n");
 	abc.meter && (text += abc.meter + "\n");
 	abc.minUnit && (text += abc.minUnit + "\n");
 	abc.speed && (text += abc.speed + "\n");
-	abc.key && (text += abc.key + "\n");
+	if (abc.key) {
+		text += abc.key + " ";
+		
+		// text += "style=rhythm";
+		// text += "style=harmonic";
+		// text += "style=x";
+		// text += "style=triangle";
+		
+		text += "\n";
+	}
+	text += "V:1 style=jianpu" + "\n";
 
 	const measures = abc.measures;
 	for (let i = 0; i < measures.length; i++) {
@@ -243,10 +257,10 @@ export const renderMeasures = (abc: IAbc, option?: IRenderMeasuresOption) => {
 			}
 			text += note.segno ?? ""; // 分割
 		}
-		let _i = i + 1;
-		if (!option?.hiddenIndex) {
-			text += `"<${_i}"`;
-		}
+		// let _i = i + 1;
+		// if (!option?.hiddenIndex) {
+		// 	text += `"<${_i}"`;
+		// }
 		text += measure.barline ?? "";
 		if (wrap % 4 === 0) {
 			text += "\n";

+ 3 - 1
src/pc/theme.css

@@ -31,16 +31,18 @@ path {
 }
 
 .deleteDialog {
+    width: 400px !important;
     display: flex;
     flex-direction: column;
     border-radius: 12px;
-    min-height   : 178px;
+    min-height   : 190px;
 }
 
 .deleteDialog.n-dialog .n-dialog__title {
     justify-content: center;
     font-weight: 600;
     font-size: 16px;
+    padding-right: 0 !important;
 }
 
 .deleteDialog.n-dialog .n-dialog__icon {

+ 59 - 0
src/utils/uploadFile.ts

@@ -0,0 +1,59 @@
+import axios from "axios";
+import request from "./request";
+const bucketName = "gyt";
+const ACL = "public-read";
+const ossUploadUrl = `https://${bucketName}.ks3-cn-beijing.ksyuncs.com/`;
+
+const policy = (params: object) => {
+	return request.post('/open/getUploadSign', {
+		data: params,
+    requestType: 'json',
+	});
+};
+
+const getPolicy = async (fileName: string) => {
+	const obj = {
+		filename: fileName,
+		bucketName: bucketName,
+		postData: {
+			filename: fileName,
+			acl: ACL,
+			key: fileName,
+		},
+	};
+	// console.log("🚀 ~ obj:", obj)
+	const { data } = await policy(obj);
+	// console.log(data);
+	return data;
+};
+const upload = async (policy: any, fileName: string, file: any, progress?: Function) => {
+	const url = ossUploadUrl + fileName;
+	const formData = new FormData();
+	formData.append("policy", policy.policy);
+	formData.append("signature", policy.signature);
+	formData.append("key", fileName);
+	formData.append("KSSAccessKeyId", policy.kssAccessKeyId);
+	formData.append("acl", ACL);
+	formData.append("name", fileName);
+	formData.append("file", file);
+	const res = await axios({
+		url: ossUploadUrl,
+		method: "post",
+		headers: {
+			"Content-Type": "multipart/form-data",
+		},
+		data: formData,
+		onUploadProgress: (progressEvent: any) => {
+			const complete = ((progressEvent.loaded / progressEvent.total) * 100) | 0;
+			progress && progress(complete);
+		},
+	} as any);
+	return url;
+};
+
+export const api_uploadFile = async (file: any, fileName: string, progress?: Function) => {
+	const policy = await getPolicy(fileName);
+	const url = await upload(policy, fileName, file, progress && progress);
+	console.log("🚀 ~ url:", url);
+	return url;
+};