index.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. import { defineComponent, nextTick, reactive, watch } from "vue";
  2. import styles from "./index.module.less";
  3. import iconClose from "../image/close2.svg";
  4. import {
  5. Cell,
  6. Field,
  7. NoticeBar,
  8. Popup,
  9. Radio,
  10. RadioGroup,
  11. Slider,
  12. Switch,
  13. Tab,
  14. Tabs,
  15. closeToast,
  16. showLoadingToast,
  17. showToast,
  18. } from "vant";
  19. import state, { IPlatform } from "/src/state";
  20. import { api_closeCamera, api_openCamera, api_savePicture } from "/src/helpers/communication";
  21. import iconInfo from "../image/info.svg";
  22. import iconDown from "../image/down.svg";
  23. import iconTv from "../image/tv.svg";
  24. import iconYijian from "../image/yijian.svg";
  25. import iconSubtract from "../image/subtract.png";
  26. import iconAdd from "../image/add.png";
  27. import ScreenModel from "../../custom-plugins/helper-model/screen-model";
  28. import Recommendation from "../../custom-plugins/helper-model/recommendation";
  29. import { svg2canvas } from "/src/utils/svg2canvas";
  30. import { getQuery } from "/src/utils/queryString";
  31. import { browser } from "/src/utils";
  32. export default defineComponent({
  33. name: "header-settting",
  34. setup() {
  35. const query = getQuery();
  36. const helperData = reactive({
  37. show: false,
  38. recommendationShow: false, // 建议
  39. });
  40. const downPng = () => {
  41. showLoadingToast({ message: "下载中", duration: 0 });
  42. setTimeout(async () => {
  43. const svg: any = document.getElementById("osmdSvgPage1")?.cloneNode(true);
  44. if (!svg) return showToast({ message: "保存失败", type: "fail" });
  45. const cw = svg.width.animVal.value;
  46. const ch = svg.height.animVal.value;
  47. const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
  48. rect.setAttribute("x", "0");
  49. rect.setAttribute("y", "0");
  50. rect.setAttribute("width", `${cw * 2}`);
  51. rect.setAttribute("height", `${ch * 2}`);
  52. rect.setAttribute("fill", "#fff");
  53. svg.prepend(rect);
  54. if (svg) {
  55. const _canvas = svg2canvas(svg.outerHTML);
  56. const browserInfo = browser();
  57. if (state.platform === IPlatform.PC || !browserInfo.isApp) {
  58. let el: any = document.createElement("a");
  59. // 设置 href 为图片经过 base64 编码后的字符串,默认为 png 格式
  60. el.href = _canvas.toDataURL();
  61. el.download = state.examSongName;
  62. // 创建一个点击事件并对 a 标签进行触发
  63. const event = new MouseEvent("click");
  64. el.dispatchEvent(event);
  65. setTimeout(() => {
  66. showToast({ message: "保存成功", type: "success" });
  67. el = null;
  68. }, 300);
  69. } else {
  70. const base64 = _canvas.toDataURL("image/png", 1);
  71. const res = await api_savePicture({
  72. base64,
  73. });
  74. if (res?.content?.status === "success") {
  75. showToast({ message: "保存成功", type: "success" });
  76. } else {
  77. showToast({ message: "保存失败", type: "fail" });
  78. }
  79. }
  80. }
  81. }, 500);
  82. };
  83. const formatterTimeMs = (value: any) => value = String(Math.min(3000, value));
  84. // 加减评测频率
  85. const operateHz = (type: number) => {
  86. const minFrequency = state.baseFrequency - 10, maxFrequency = state.baseFrequency + 10
  87. let currentFrequency = state.setting.frequency
  88. if (type === 1) {
  89. if (currentFrequency - 1 < minFrequency) return showToast({ message: `最低标准音高${minFrequency}HZ` })
  90. currentFrequency = currentFrequency - 1
  91. } else {
  92. if (currentFrequency + 1 > maxFrequency) return showToast({ message: `最高标准音高${maxFrequency}HZ` })
  93. currentFrequency = currentFrequency + 1
  94. }
  95. state.setting.frequency = currentFrequency >= 0 ? currentFrequency : 0
  96. }
  97. return () => (
  98. <div class={styles["header-settting"]}>
  99. <div class={styles.content}>
  100. <Tabs border animated swipeable>
  101. <Tab title="全局设置">
  102. <NoticeBar
  103. class={styles.noticebar}
  104. left-icon={iconInfo}
  105. text="全局设置会更改所有乐谱练习及评测"
  106. />
  107. <Cell title="护眼模式" center>
  108. {{
  109. extra: () => <Switch v-model={state.setting.eyeProtection}></Switch>,
  110. }}
  111. </Cell>
  112. <Cell
  113. title="节拍器音量"
  114. class={styles.sliderWrap}
  115. center
  116. >
  117. {{
  118. extra: () => (
  119. <Slider
  120. class={[styles.slider, styles.sliderVolume]}
  121. min={0}
  122. max={100}
  123. v-model:modelValue={state.setting.beatVolume}
  124. >
  125. {{
  126. button: () => <div class={styles.sliderBtn}>{state.setting.beatVolume}</div>,
  127. }}
  128. </Slider>
  129. ),
  130. }}
  131. </Cell>
  132. <div class={styles.btnsbar}>
  133. {/* <div class={styles.btn} onClick={downPng}>
  134. <img src={iconDown} />
  135. 下载曲谱
  136. </div> */}
  137. <div class={styles.btn} onClick={() => (helperData.show = true)}>
  138. <img src={iconTv} />
  139. 投屏帮助
  140. </div>
  141. <div class={styles.btn} onClick={() => (helperData.recommendationShow = true)}>
  142. <img src={iconYijian} />
  143. 意见反馈
  144. </div>
  145. </div>
  146. </Tab>
  147. <Tab title="练习设置">
  148. <Cell title="循环播放" center>
  149. {{
  150. extra: () => <Switch v-model={state.setting.repeatAutoPlay}></Switch>,
  151. }}
  152. </Cell>
  153. <Cell class={[state.modeType == "evaluating" && styles.disabled]} title="显示指法" center>
  154. {{
  155. extra: () => <Switch v-model={state.setting.displayFingering} disabled={!state.fingeringInfo.name}></Switch>,
  156. }}
  157. </Cell>
  158. </Tab>
  159. <Tab title="评测设置">
  160. <Cell class={[query.workRecord && styles.disabled]} title="评测难度" center>
  161. {{
  162. extra: () => (
  163. <RadioGroup
  164. iconSize={20}
  165. class={styles.radioGroup}
  166. v-model={state.setting.evaluationDifficulty}
  167. >
  168. <Radio name="BEGINNER">入门</Radio>
  169. <Radio name="ADVANCED">进阶</Radio>
  170. <Radio name="PERFORMER">大师</Radio>
  171. </RadioGroup>
  172. ),
  173. }}
  174. </Cell>
  175. <Cell title="延迟检测" center>
  176. {{
  177. extra: () => <Switch v-model={state.setting.soundEffect}></Switch>,
  178. }}
  179. </Cell>
  180. <Cell title="摄像头" center>
  181. {{
  182. extra: () => (
  183. <Switch
  184. v-model={state.setting.camera}
  185. onChange={ async (value) => {
  186. if (value) {
  187. const res = await api_openCamera();
  188. // 没有授权
  189. if (res?.content?.reson) {
  190. state.setting.camera = false
  191. }
  192. } else {
  193. api_closeCamera();
  194. }
  195. }}
  196. ></Switch>
  197. ),
  198. }}
  199. </Cell>
  200. <Cell
  201. style={{ display: state.setting.camera ? "" : "none" }}
  202. title="透明度"
  203. class={styles.sliderWrap}
  204. center
  205. >
  206. {{
  207. extra: () => (
  208. <Slider
  209. class={styles.slider}
  210. min={0}
  211. max={100}
  212. v-model:modelValue={state.setting.cameraOpacity}
  213. >
  214. {{
  215. button: () => <div class={styles.sliderBtn}>{state.setting.cameraOpacity}</div>,
  216. }}
  217. </Slider>
  218. ),
  219. }}
  220. </Cell>
  221. {/* <Cell title="保存到相册" center>
  222. {{
  223. extra: () => <Switch v-model={state.setting.saveToAlbum}></Switch>,
  224. }}
  225. </Cell> */}
  226. <Cell title="开启伴奏" center>
  227. {{
  228. extra: () => <Switch v-model={state.setting.enableAccompaniment}></Switch>,
  229. }}
  230. </Cell>
  231. <Cell title="标准音高" center>
  232. {/* {{
  233. extra: () => (
  234. <RadioGroup
  235. iconSize={20}
  236. class={styles.radioGroup}
  237. v-model={state.setting.frequency}
  238. >
  239. <Radio name={440}>440Hz</Radio>
  240. <Radio name={442}>442Hz</Radio>
  241. </RadioGroup>
  242. ),
  243. }} */}
  244. {{
  245. extra: () => (
  246. <div class={styles.operateHz}>
  247. <img src={iconSubtract} onClick={() => operateHz(1)} />
  248. <span>{state.setting.frequency}HZ</span>
  249. <img src={iconAdd} onClick={() => operateHz(2)} />
  250. </div>
  251. )
  252. }}
  253. </Cell>
  254. <Field class={styles.reactionTime} label="反应时间(毫秒)" type="digit"
  255. placeholder="最大可输入3000毫秒"
  256. formatter={formatterTimeMs}
  257. v-model:modelValue={state.setting.reactionTimeMs} />
  258. </Tab>
  259. </Tabs>
  260. </div>
  261. <Popup
  262. class={["popup-custom", styles.screen]}
  263. v-model:show={helperData.show}
  264. onClose={() => {
  265. helperData.show = false;
  266. }}
  267. position="right"
  268. teleport="body"
  269. >
  270. <ScreenModel
  271. onClose={(open: Boolean) => {
  272. helperData.show = false;
  273. }}
  274. />
  275. </Popup>
  276. <Popup
  277. v-model:show={helperData.recommendationShow}
  278. class="popup-custom van-scale center-closeBtn"
  279. transition="van-scale"
  280. teleport="body"
  281. closeable
  282. >
  283. <Recommendation
  284. onClose={() => {
  285. helperData.recommendationShow = false;
  286. }}
  287. />
  288. </Popup>
  289. </div>
  290. );
  291. },
  292. });