Compare commits
4 Commits
save-mobli
...
save-mobli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f581d7aa5 | ||
|
|
c372a18ca0 | ||
|
|
6d9fbf234f | ||
|
|
102f5fbc1f |
10
auto-imports.d.ts
vendored
10
auto-imports.d.ts
vendored
@@ -1,10 +0,0 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
export {}
|
||||
declare global {
|
||||
const ElMessage: typeof import('element-plus/es')['ElMessage']
|
||||
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
|
||||
}
|
||||
43
components.d.ts
vendored
43
components.d.ts
vendored
@@ -1,43 +0,0 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
BackIcon: typeof import('./src/components/icon/BackIcon.vue')['default']
|
||||
BaseButton: typeof import('./src/components/BaseButton.vue')['default']
|
||||
BaseIcon: typeof import('./src/components/BaseIcon.vue')['default']
|
||||
Close: typeof import('./src/components/icon/Close.vue')['default']
|
||||
DeleteIcon: typeof import('./src/components/icon/DeleteIcon.vue')['default']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSlider: typeof import('element-plus/es')['ElSlider']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTableV2: typeof import('element-plus/es')['ElTableV2']
|
||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
Empty: typeof import('./src/components/Empty.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SlideHorizontal: typeof import('./src/components/slide/SlideHorizontal.vue')['default']
|
||||
SlideItem: typeof import('./src/components/slide/SlideItem.vue')['default']
|
||||
VolumeIcon: typeof import('./src/components/icon/VolumeIcon.vue')['default']
|
||||
}
|
||||
export interface ComponentCustomProperties {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
}
|
||||
}
|
||||
12
package.json
12
package.json
@@ -24,21 +24,17 @@
|
||||
"element-plus": "^2.10.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"git-last-commit": "^1.0.1",
|
||||
"hover.css": "^2.3.2",
|
||||
"jquery": "^3.7.1",
|
||||
"libarchive-wasm": "^1.2.0",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"nanoid": "^5.1.5",
|
||||
"pinia": "^3.0.3",
|
||||
"sentence-splitter": "^4.4.1",
|
||||
"string-comparison": "^1.3.0",
|
||||
"tesseract.js": "^4.1.4",
|
||||
"vant": "^4.9.20",
|
||||
"unplugin-element-plus": "^0.10.0",
|
||||
"vue": "^3.5.17",
|
||||
"vue-activity-calendar": "^1.2.2",
|
||||
"vue-i18n": "^9.14.4",
|
||||
"vue-router": "^4.5.1",
|
||||
"vue-virtual-scroller": "2.0.0-beta.8"
|
||||
},
|
||||
@@ -46,7 +42,7 @@
|
||||
"@iconify/vue": "^4.3.0",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@unocss/postcss": "^0.60.4",
|
||||
"@unocss/postcss": "^66.4.0",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"@vitejs/plugin-vue-jsx": "^5.0.1",
|
||||
"@vue/compiler-sfc": "^3.5.17",
|
||||
@@ -59,9 +55,7 @@
|
||||
"sass": "^1.89.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.8.3",
|
||||
"unocss": "^66.3.3",
|
||||
"unplugin-auto-import": "^0.16.7",
|
||||
"unplugin-vue-components": "^0.25.2",
|
||||
"unocss": "^66.4.0",
|
||||
"unplugin-vue-macros": "^2.14.5",
|
||||
"vite": "^7.0.3",
|
||||
"vite-plugin-cdn-import": "^1.0.1",
|
||||
|
||||
651
pnpm-lock.yaml
generated
651
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
461
src/Mobile.vue
461
src/Mobile.vue
@@ -1,461 +0,0 @@
|
||||
<script setup lang="js">
|
||||
import {onMounted} from "vue"
|
||||
import {createWorker} from "tesseract.js";
|
||||
import useMobile from "@/hooks/useMobile.ts";
|
||||
|
||||
const isMobile = useMobile()
|
||||
|
||||
onMounted(async () => {
|
||||
Array.prototype.clone = function () {
|
||||
return [].concat(this);
|
||||
//或者 return this.concat();
|
||||
}
|
||||
|
||||
class Point {
|
||||
constructor(x, y, time) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.isControl = false;
|
||||
this.time = Date.now();
|
||||
this.lineWidth = 0;
|
||||
this.isAdd = false;
|
||||
}
|
||||
}
|
||||
|
||||
class Line {
|
||||
constructor() {
|
||||
this.points = new Array();
|
||||
this.changeWidthCount = 0;
|
||||
this.lineWidth = 10;
|
||||
}
|
||||
}
|
||||
|
||||
class HandwritingSelf {
|
||||
|
||||
constructor(canvas) {
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext("2d")
|
||||
let canvasRect = canvas.getBoundingClientRect()
|
||||
let {width, height} = canvasRect
|
||||
let dpr = window.devicePixelRatio
|
||||
if (dpr) {
|
||||
canvas.style.width = width + "px"
|
||||
canvas.style.height = height + "px"
|
||||
canvas.height = height * dpr
|
||||
canvas.width = width * dpr
|
||||
this.ctx.scale(dpr, dpr)
|
||||
}
|
||||
// this.points = new Array();
|
||||
this.line = new Line();
|
||||
this.pointLines = new Array();//Line数组
|
||||
this.k = 0.5;
|
||||
this.begin = null;
|
||||
this.middle = null;
|
||||
this.end = null;
|
||||
this.preTime = null;
|
||||
this.lineWidth = 8;
|
||||
this.isDown = false;
|
||||
}
|
||||
|
||||
down(x, y) {
|
||||
// console.log("down:", x, y)
|
||||
this.isDown = true;
|
||||
this.line = new Line();
|
||||
this.line.lineWidth = this.lineWidth;
|
||||
let currentPoint = new Point(x, y, Date.now());
|
||||
this.addPoint(currentPoint);
|
||||
|
||||
this.preTime = Date.now();
|
||||
}
|
||||
|
||||
move(x, y) {
|
||||
// console.log("move:",x,y,this.isDown)
|
||||
if (this.isDown) {
|
||||
let currentPoint = new Point(x, y, Date.now())
|
||||
this.addPoint(currentPoint);
|
||||
this.draw();
|
||||
}
|
||||
}
|
||||
|
||||
up(x, y) {
|
||||
// if (e.touches.length > 0) {
|
||||
let currentPoint = new Point(x, y, Date.now())
|
||||
this.addPoint(currentPoint);
|
||||
// }
|
||||
this.draw(true);
|
||||
|
||||
this.pointLines.push(this.line);
|
||||
|
||||
this.begin = null;
|
||||
this.middle = null;
|
||||
this.end = null;
|
||||
this.isDown = false;
|
||||
}
|
||||
|
||||
draw(isUp = false) {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.ctx.strokeStyle = "rgba(255,20,87,1)";
|
||||
|
||||
//绘制不包含this.line的线条
|
||||
this.pointLines.forEach((line, index) => {
|
||||
let points = line.points;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.ellipse(points[0].x - 1.5, points[0].y, 6, 3, Math.PI / 4, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(points[0].x, points[0].y);
|
||||
let lastW = line.lineWidth;
|
||||
this.ctx.lineWidth = line.lineWidth;
|
||||
this.ctx.lineJoin = "round";
|
||||
this.ctx.lineCap = "round";
|
||||
let minLineW = line.lineWidth / 4;
|
||||
let isChangeW = false;
|
||||
|
||||
let changeWidthCount = line.changeWidthCount;
|
||||
for (let i = 1; i <= points.length; i++) {
|
||||
if (i == points.length) {
|
||||
this.ctx.stroke();
|
||||
break;
|
||||
}
|
||||
if (i > points.length - changeWidthCount) {
|
||||
if (!isChangeW) {
|
||||
this.ctx.stroke();//将之前的线条不变的path绘制完
|
||||
isChangeW = true;
|
||||
if (i > 1 && points[i - 1].isControl)
|
||||
continue;
|
||||
}
|
||||
let w = (lastW - minLineW) / changeWidthCount * (points.length - i) + minLineW;
|
||||
points[i - 1].lineWidth = w;
|
||||
this.ctx.beginPath();//为了开启新的路径 否则每次stroke 都会把之前的路径在描一遍
|
||||
// this.ctx.strokeStyle = "rgba("+Math.random()*255+","+Math.random()*255+","+Math.random()*255+",1)";
|
||||
this.ctx.lineWidth = w;
|
||||
this.ctx.moveTo(points[i - 1].x, points[i - 1].y);//移动到之前的点
|
||||
this.ctx.lineTo(points[i].x, points[i].y);
|
||||
this.ctx.stroke();//将之前的线条不变的path绘制完
|
||||
} else {
|
||||
if (points[i].isControl && points[i + 1]) {
|
||||
this.ctx.quadraticCurveTo(points[i].x, points[i].y, points[i + 1].x, points[i + 1].y);
|
||||
} else if (i >= 1 && points[i - 1].isControl) {//上一个是控制点 当前点已经被绘制
|
||||
} else
|
||||
this.ctx.lineTo(points[i].x, points[i].y);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
//绘制this.line线条
|
||||
let points;
|
||||
if (isUp)
|
||||
points = this.line.points;
|
||||
else
|
||||
points = this.line.points.clone();
|
||||
//当前绘制的线条最后几个补点 贝塞尔方式增加点
|
||||
let count = 0;
|
||||
let insertCount = 0;
|
||||
let i = points.length - 1;
|
||||
let endPoint = points[i];
|
||||
let controlPoint;
|
||||
let startPoint;
|
||||
while (i >= 0) {
|
||||
if (points[i].isControl == true) {
|
||||
controlPoint = points[i];
|
||||
count++;
|
||||
} else {
|
||||
startPoint = points[i];
|
||||
}
|
||||
if (startPoint && controlPoint && endPoint) {//使用贝塞尔计算补点
|
||||
let dis = this.z_distance(startPoint, controlPoint) + this.z_distance(controlPoint, endPoint);
|
||||
let insertPoints = this.BezierCalculate([startPoint, controlPoint, endPoint], Math.floor(dis / 6) + 1);
|
||||
insertCount += insertPoints.length;
|
||||
var index = i;//插入位置
|
||||
// 把insertPoints 变成一个适合splice的数组(包含splice前2个参数的数组)
|
||||
insertPoints.unshift(index, 1);
|
||||
Array.prototype.splice.apply(points, insertPoints);
|
||||
|
||||
//补完点后
|
||||
endPoint = startPoint;
|
||||
startPoint = null;
|
||||
}
|
||||
if (count >= 6)
|
||||
break;
|
||||
i--;
|
||||
}
|
||||
//确定最后线宽变化的点数
|
||||
let changeWidthCount = count + insertCount;
|
||||
if (isUp)
|
||||
this.line.changeWidthCount = changeWidthCount;
|
||||
|
||||
//制造椭圆头
|
||||
this.ctx.fillStyle = "rgba(255,20,87,1)"
|
||||
this.ctx.beginPath();
|
||||
this.ctx.ellipse(points[0].x - 1.5, points[0].y, 6, 3, Math.PI / 4, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
|
||||
// console.log('points', points)
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(points[0].x, points[0].y);
|
||||
let lastW = this.line.lineWidth;
|
||||
this.ctx.lineWidth = this.line.lineWidth;
|
||||
this.ctx.lineJoin = "round";
|
||||
this.ctx.lineCap = "round";
|
||||
let minLineW = this.line.lineWidth / 4;
|
||||
let isChangeW = false;
|
||||
for (let i = 1; i <= points.length; i++) {
|
||||
if (i == points.length) {
|
||||
this.ctx.stroke();
|
||||
break;
|
||||
}
|
||||
//最后的一些点线宽变细
|
||||
if (i > points.length - changeWidthCount) {
|
||||
if (!isChangeW) {
|
||||
this.ctx.stroke();//将之前的线条不变的path绘制完
|
||||
isChangeW = true;
|
||||
if (i > 1 && points[i - 1].isControl)
|
||||
continue;
|
||||
}
|
||||
|
||||
//计算线宽
|
||||
let w = (lastW - minLineW) / changeWidthCount * (points.length - i) + minLineW;
|
||||
points[i - 1].lineWidth = w;
|
||||
this.ctx.beginPath();//为了开启新的路径 否则每次stroke 都会把之前的路径在描一遍
|
||||
// this.ctx.strokeStyle = "rgba(" + Math.random() * 255 + "," + Math.random() * 255 + "," + Math.random() * 255 + ",0.5)";
|
||||
this.ctx.lineWidth = w;
|
||||
this.ctx.moveTo(points[i - 1].x, points[i - 1].y);//移动到之前的点
|
||||
this.ctx.lineTo(points[i].x, points[i].y);
|
||||
this.ctx.stroke();//将之前的线条不变的path绘制完
|
||||
} else {
|
||||
if (points[i].isControl && points[i + 1]) {
|
||||
this.ctx.quadraticCurveTo(points[i].x, points[i].y, points[i + 1].x, points[i + 1].y);
|
||||
} else if (i >= 1 && points[i - 1].isControl) {//上一个是控制点 当前点已经被绘制
|
||||
} else
|
||||
this.ctx.lineTo(points[i].x, points[i].y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addPoint(p) {
|
||||
if (this.line.points.length >= 1) {
|
||||
let last_point = this.line.points[this.line.points.length - 1]
|
||||
let distance = this.z_distance(p, last_point);
|
||||
if (distance < 10) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.line.points.length === 0) {
|
||||
this.begin = p;
|
||||
p.isControl = true;
|
||||
this.pushPoint(p);
|
||||
} else {
|
||||
this.middle = p;
|
||||
let controlPs = this.computeControlPoints(this.k, this.begin, this.middle, null);
|
||||
this.pushPoint(controlPs.first);
|
||||
this.pushPoint(p);
|
||||
p.isControl = true;
|
||||
|
||||
this.begin = this.middle;
|
||||
}
|
||||
}
|
||||
|
||||
pushPoint(p) {
|
||||
//排除重复点
|
||||
if (this.line.points.length >= 1) {
|
||||
let last = this.line.points[this.line.points.length - 1]
|
||||
if (last.x === p.x && last.y === p.y) return;
|
||||
}
|
||||
this.line.points.push(p);
|
||||
}
|
||||
|
||||
computeControlPoints(k, begin, middle, end) {
|
||||
if (k > 0.5 || k <= 0)
|
||||
return;
|
||||
|
||||
let diff1 = new Point(middle.x - begin.x, middle.y - begin.y)
|
||||
let diff2 = null;
|
||||
if (end)
|
||||
diff2 = new Point(end.x - middle.x, end.y - middle.y)
|
||||
|
||||
// let l1 = (diff1.x ** 2 + diff1.y ** 2) ** (1 / 2)
|
||||
// let l2 = (diff2.x ** 2 + diff2.y ** 2) ** (1 / 2)
|
||||
|
||||
let first = new Point(middle.x - (k * diff1.x), middle.y - (k * diff1.y))
|
||||
let second = null;
|
||||
if (diff2)
|
||||
second = new Point(middle.x + (k * diff2.x), middle.y + (k * diff2.y))
|
||||
return {first: first, second: second}
|
||||
}
|
||||
|
||||
z_distance(b, e) {
|
||||
return Math.sqrt(Math.pow(e.x - b.x, 2) + Math.pow(e.y - b.y, 2));
|
||||
}
|
||||
|
||||
BezierCalculate(poss, precision) {
|
||||
|
||||
//维度,坐标轴数(二维坐标,三维坐标...)
|
||||
let dimersion = 2;
|
||||
|
||||
//贝塞尔曲线控制点数(阶数)
|
||||
let number = poss.length;
|
||||
|
||||
//控制点数不小于 2 ,至少为二维坐标系
|
||||
if (number < 2 || dimersion < 2)
|
||||
return null;
|
||||
|
||||
let result = new Array();
|
||||
|
||||
//计算杨辉三角
|
||||
let mi = new Array();
|
||||
mi[0] = mi[1] = 1;
|
||||
for (let i = 3; i <= number; i++) {
|
||||
|
||||
let t = new Array();
|
||||
for (let j = 0; j < i - 1; j++) {
|
||||
t[j] = mi[j];
|
||||
}
|
||||
|
||||
mi[0] = mi[i - 1] = 1;
|
||||
for (let j = 0; j < i - 2; j++) {
|
||||
mi[j + 1] = t[j] + t[j + 1];
|
||||
}
|
||||
}
|
||||
|
||||
//计算坐标点
|
||||
for (let i = 0; i < precision; i++) {
|
||||
let t = i / precision;
|
||||
let p = new Point(0, 0);
|
||||
p.isAdd = true;
|
||||
result.push(p);
|
||||
for (let j = 0; j < dimersion; j++) {
|
||||
let temp = 0.0;
|
||||
for (let k = 0; k < number; k++) {
|
||||
temp += Math.pow(1 - t, number - k - 1) * (j == 0 ? poss[k].x : poss[k].y) * Math.pow(t, k) * mi[k];
|
||||
}
|
||||
j == 0 ? p.x = temp : p.y = temp;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.line = new Line();
|
||||
this.pointLines = new Array();//Line数组
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
const worker = await createWorker({
|
||||
// logger: m => console.log(m)
|
||||
});
|
||||
await worker.loadLanguage('eng');
|
||||
await worker.initialize('eng');
|
||||
await worker.setParameters({
|
||||
tessedit_char_whitelist: 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM'
|
||||
});
|
||||
|
||||
// alert('好了')
|
||||
let lastTime = Date.now()
|
||||
let timer = -1
|
||||
let checkTime = 400
|
||||
|
||||
let eventMap = {
|
||||
down: '',
|
||||
move: '',
|
||||
up: '',
|
||||
}
|
||||
if (isMobile) {
|
||||
eventMap = {
|
||||
down: 'touchstart',
|
||||
move: 'touchmove',
|
||||
up: 'touchend',
|
||||
}
|
||||
} else {
|
||||
eventMap = {
|
||||
down: 'mousedown',
|
||||
move: 'mousemove',
|
||||
up: 'mouseup',
|
||||
}
|
||||
}
|
||||
let handwriting = new HandwritingSelf(document.getElementById("canvasId"))
|
||||
|
||||
window.addEventListener(eventMap.down, (e) => {
|
||||
if (Date.now() - lastTime > checkTime) {
|
||||
handwriting.clear()
|
||||
} else {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
if (e.type === "touchstart")
|
||||
handwriting.down(e.touches[0].pageX, e.touches[0].pageY);
|
||||
else
|
||||
handwriting.down(e.x, e.y);
|
||||
})
|
||||
|
||||
window.addEventListener(eventMap.move, (e) => {
|
||||
if (e.type === "touchmove")
|
||||
handwriting.move(e.touches[0].pageX, e.touches[0].pageY);
|
||||
else
|
||||
handwriting.move(e.x, e.y);
|
||||
})
|
||||
|
||||
window.addEventListener(eventMap.up, (e) => {
|
||||
if (e.type === "touchend")
|
||||
handwriting.up(e.touches[0].pageX, e.touches[0].pageY);
|
||||
else
|
||||
handwriting.up(e.x, e.y);
|
||||
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
// console.log('识别');
|
||||
// handwriting.canvas.toDataURL()
|
||||
|
||||
// var MIME_TYPE = "image/png";
|
||||
// var imgURL = handwriting.canvas.toDataURL(MIME_TYPE);
|
||||
// var dlLink = document.createElement('a');
|
||||
// dlLink.download = 'fileName.png';
|
||||
// dlLink.href = imgURL;
|
||||
// dlLink.dataset.downloadurl = [MIME_TYPE, dlLink.download, dlLink.href].join(':');
|
||||
//
|
||||
// document.body.appendChild(dlLink);
|
||||
// dlLink.click();
|
||||
// document.body.removeChild(dlLink);
|
||||
|
||||
(async () => {
|
||||
const {data: {text}} = await worker.recognize(handwriting.canvas);
|
||||
console.log(text);
|
||||
if (isMobile){
|
||||
alert(text)
|
||||
}
|
||||
})();
|
||||
}, checkTime)
|
||||
|
||||
lastTime = Date.now()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mobile">
|
||||
<canvas id="canvasId"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
html, body {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="scss">
|
||||
|
||||
.mobile {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: var(--color-background);
|
||||
overflow: hidden;
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
//border: 1px solid gray;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,49 +1,30 @@
|
||||
//@import '/node_modules/element-plus/dist/index.css';
|
||||
//@use "/node_modules/hover.css" as *;
|
||||
@use "anim" as *;
|
||||
@use 'element-plus/theme-chalk/dark/css-vars' as *;
|
||||
|
||||
:root {
|
||||
//修改element-ui的进度条底色
|
||||
--el-border-color-lighter: #d1d1d1 !important;
|
||||
|
||||
--color-background: #E6E8EB;
|
||||
//--color-main-bg: #E6E8EB;
|
||||
--color-main-bg: rgb(238, 240, 244);
|
||||
--color-second-bg: rgb(247, 247, 247);
|
||||
--color-third-bg: rgb(213, 215, 217);
|
||||
|
||||
--color-item-bg: rgb(228, 230, 232);
|
||||
--color-item-hover: white;
|
||||
//--color-item-active: rgb(75, 110, 175);
|
||||
--color-item-active: rgb(253, 246, 236);
|
||||
--color-item-border: rgb(226, 226, 226);
|
||||
|
||||
--color-header-bg: white;
|
||||
--color-tooltip-bg: white;
|
||||
--color-tooltip-shadow: #d9d9d9;
|
||||
--color-font-1: rgb(91, 91, 91);
|
||||
--color-font-2: rgb(46, 46, 46);
|
||||
--color-font-3: rgb(75, 85, 99);
|
||||
--color-font-active-1: white;
|
||||
--color-font-active-2: whitesmoke;
|
||||
--color-main-active: rgb(12, 140, 233);
|
||||
--color-primary: rgb(12, 140, 233);
|
||||
--color-scrollbar: rgb(147, 173, 227);
|
||||
--color-gray: gray;
|
||||
--color-sub-gray: #c0bfbf;
|
||||
|
||||
--practice-wrapper-translateX: 1px;
|
||||
--article-width: 50vw;
|
||||
--toolbar-width: 50rem;
|
||||
--toolbar-height: 3.2rem;
|
||||
--panel-width: 24rem;
|
||||
--space: 1rem;
|
||||
--radius: .5rem;
|
||||
--stat-gap: 2rem;
|
||||
--shadow: rgba(0, 0, 0, 0.08) 0px 4px 12px;
|
||||
--panel-margin-left: calc(50% + var(--toolbar-width) / 2 + 1rem);
|
||||
--article-panel-margin-left: calc(50% + var(--article-width) / 2 + 3.5rem);
|
||||
--anim-time: 0.5s;
|
||||
--article-panel-margin-left: calc(50% + var(--article-width) / 2 + 1rem);
|
||||
--anim-time: 0.3s;
|
||||
|
||||
--color-input-bg: white;
|
||||
--color-input-border: #bfbfbf;
|
||||
@@ -60,89 +41,104 @@
|
||||
--zh-article-family: "Songti SC", "SimSun", "Noto Serif CJK SC", serif;
|
||||
|
||||
--btn-primary: rgb(75, 85, 99);
|
||||
--btn-info: #909399;
|
||||
--btn-info: white;
|
||||
|
||||
--color-primary: #E6E8EB;
|
||||
--color-second: rgb(247, 247, 247);
|
||||
--color-third: rgb(226 232 240 / 1);
|
||||
|
||||
--color-card-active: #FED7AA;
|
||||
--color-list-item-active: rgb(253, 246, 236);
|
||||
--color-icon-hightlight: rgb(12, 140, 233);
|
||||
//--color-icon-hightlight: rgb(12, 140, 233);
|
||||
--color-sub-text: gray;
|
||||
--color-main-text: rgb(91, 91, 91);
|
||||
--color-select-bg: rgb(12, 140, 233);
|
||||
--color-select-text: white;
|
||||
|
||||
--color-notice-bg: rgb(247, 247, 247);
|
||||
|
||||
|
||||
//修改element-ui的进度条底色
|
||||
--el-border-color-lighter: #e2e5ed !important;
|
||||
}
|
||||
|
||||
.footer {
|
||||
&.hide {
|
||||
--el-border-color-lighter: #dbdbdb !important;
|
||||
}
|
||||
}
|
||||
|
||||
html.dark {
|
||||
--color-main-bg: rgba(14, 18, 23, 1);
|
||||
--color-second-bg: rgb(30, 31, 34);
|
||||
--color-third-bg: rgb(43, 45, 48);
|
||||
|
||||
--color-primary: #0E1217;
|
||||
--color-second: rgb(30, 31, 34);
|
||||
--color-third: rgb(43, 45, 48);
|
||||
--color-card-active: rgb(84, 84, 84);
|
||||
--color-list-item-active: rgb(84, 84, 84);
|
||||
--color-icon-hightlight: rgb(147, 173, 227);
|
||||
--color-sub-text: #b8b8b8;
|
||||
--color-main-text: rgba(249, 250, 251, 0.8);
|
||||
--color-select-bg: rgb(147, 173, 227);
|
||||
--color-select-text: black;
|
||||
--color-notice-bg: rgb(43, 45, 48);
|
||||
|
||||
--color-item-bg: rgb(43, 45, 48);
|
||||
--color-item-hover: rgb(67, 69, 74);
|
||||
--color-item-active: rgb(84, 84, 84);
|
||||
--color-item-border: rgb(41, 41, 41);
|
||||
|
||||
--color-header-bg: rgb(51, 51, 51);
|
||||
--color-tooltip-bg: #252525;
|
||||
--color-tooltip-shadow: #3b3b3b;
|
||||
--color-font-1: rgba(249, 250, 251, 0.8);
|
||||
--color-font-2: rgba(255, 255, 255, 0.5);
|
||||
--color-font-3: rgba(255, 255, 255, 0.3);
|
||||
|
||||
--color-gray: #bebebe;
|
||||
--color-sub-gray: #383737;
|
||||
|
||||
--color-main-active: rgb(147, 173, 227);
|
||||
--color-scrollbar: rgb(92, 93, 94);
|
||||
|
||||
--btn-info: transparent;
|
||||
|
||||
--color-input-bg: rgba(14, 18, 23, 1);
|
||||
--color-input-icon: #383737;
|
||||
|
||||
--color-textarea-bg: rgb(43, 45, 48);
|
||||
--color-article: white;
|
||||
|
||||
--el-border-color-lighter: var(--color-third) !important;
|
||||
|
||||
.footer {
|
||||
&.hide {
|
||||
--el-border-color-lighter: var(--color-third) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1680px) {
|
||||
:root {
|
||||
--practice-wrapper-translateX: -12vw;
|
||||
--toolbar-width: 40vw;
|
||||
--article-width: 60vw;
|
||||
--panel-width: 38rem;
|
||||
--toolbar-height: 4.8rem;
|
||||
--panel-margin-left: calc(50vw + var(--practice-wrapper-translateX) + var(--toolbar-width) / 2 + 5vw);
|
||||
--article-panel-margin-left: calc(50% + var(--practice-wrapper-translateX) + var(--article-width) / 2 + 48rem);
|
||||
--toolbar-width: 50vw;
|
||||
--article-width: 50vw;
|
||||
--panel-width: 20vw;
|
||||
--space: 0.5rem;
|
||||
}
|
||||
.footer {
|
||||
.bottom {
|
||||
padding: 1.5rem 1rem 1rem 1rem !important;
|
||||
}
|
||||
|
||||
.stat {
|
||||
margin-top: 0.4rem !important;
|
||||
|
||||
.row {
|
||||
gap: 0.5rem !important;
|
||||
}
|
||||
padding: .5rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1366px) {
|
||||
:root {
|
||||
--space: 1rem;
|
||||
--practice-wrapper-translateX: -22vw;
|
||||
--article-width: 53vw;
|
||||
--panel-width: 30vw;
|
||||
--panel-width: 20vw;
|
||||
--article-width: 50vw;
|
||||
--toolbar-width: 50vw;
|
||||
--toolbar-height: 40rem;
|
||||
--panel-margin-left: calc(50vw + var(--practice-wrapper-translateX) + var(--toolbar-width) / 2 + 14vw);
|
||||
--article-panel-margin-left: calc(50% + var(--practice-wrapper-translateX) + var(--article-width) / 2 + 12vw);
|
||||
--stat-gap: 0.5rem;
|
||||
--space: 0.3rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
.bottom {
|
||||
padding: 1.5rem 0.5rem 0.5rem 0.5rem !important;
|
||||
}
|
||||
|
||||
.stat {
|
||||
margin-top: 0.4rem !important;
|
||||
|
||||
.row {
|
||||
gap: 0.5rem !important;
|
||||
}
|
||||
padding: .5rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,7 +160,7 @@ html, body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
color: var(--color-font-1);
|
||||
color: var(--color-main-text);
|
||||
font-family: var(--font-family);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@@ -226,7 +222,7 @@ a {
|
||||
background: var(--color-textarea-bg);
|
||||
|
||||
&:focus {
|
||||
border: 1px solid var(--color-main-active);
|
||||
border: 1px solid var(--color-select-bg);
|
||||
}
|
||||
|
||||
&[readonly] {
|
||||
@@ -340,7 +336,7 @@ footer {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: var(--color-item-bg);
|
||||
color: var(--color-font-1);
|
||||
color: var(--color-main-text);
|
||||
font-size: 1.1rem;
|
||||
border-radius: .5rem;
|
||||
display: flex;
|
||||
@@ -373,24 +369,23 @@ footer {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-item-hover);
|
||||
|
||||
.volume, .collect, .easy {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--color-item-active);
|
||||
$c: #E6A23C;
|
||||
background: var(--color-list-item-active);
|
||||
|
||||
.phonetic, .item-sub-title {
|
||||
//color: var(--color-gray) !important;
|
||||
.item-sub-title {
|
||||
color: var(--color-sub-text);
|
||||
}
|
||||
|
||||
.volume, .collect, .easy, .fill {
|
||||
color: $c;
|
||||
color: var(--color-icon-hightlight);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@extend .active;
|
||||
|
||||
.volume, .collect, .easy {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,7 +393,7 @@ footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
color: var(--color-font-1);
|
||||
color: var(--color-main-text);
|
||||
|
||||
.word {
|
||||
font-size: 1.2rem;
|
||||
@@ -415,7 +410,6 @@ footer {
|
||||
font-size: 1rem;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.word-shadow {
|
||||
@@ -427,7 +421,7 @@ footer {
|
||||
.common-title {
|
||||
min-height: 2.8rem;
|
||||
font-size: 1.1rem;
|
||||
color: var(--color-font-1);
|
||||
color: var(--color-main-text);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -463,7 +457,8 @@ footer {
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply rounded-xl bg-white p-4 mb-5 box-border relative;
|
||||
@apply rounded-xl p-4 mb-5 box-border relative;
|
||||
background: var(--color-second);
|
||||
}
|
||||
|
||||
.center {
|
||||
@@ -476,7 +471,7 @@ footer {
|
||||
|
||||
.book {
|
||||
@extend .anim;
|
||||
@apply p-4 rounded-md bg-slate-200 relative cursor-pointer h-40 hover:bg-orange-200 flex flex-col justify-between;
|
||||
@apply p-4 rounded-md relative cursor-pointer h-40 bg-third hover:bg-card-active flex flex-col justify-between;
|
||||
}
|
||||
|
||||
.line {
|
||||
|
||||
@@ -30,7 +30,6 @@ defineEmits(['click'])
|
||||
size,
|
||||
type,
|
||||
(disabled||loading) && 'disabled',
|
||||
!disabled && 'hvr-grow'
|
||||
]">
|
||||
<span :style="{opacity:loading?0:1}"><slot></slot></span>
|
||||
<Icon v-if="loading"
|
||||
@@ -51,19 +50,25 @@ defineEmits(['click'])
|
||||
|
||||
.base-button {
|
||||
cursor: pointer;
|
||||
border-radius: .4rem;
|
||||
padding: 0 1rem;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all .3s;
|
||||
//background: #999;
|
||||
//background: rgb(60, 63, 65);
|
||||
//background: var(--color-second-bg);
|
||||
height: 2.5rem;
|
||||
line-height: 1;
|
||||
position: relative;
|
||||
word-break: keep-all;
|
||||
outline: none;
|
||||
text-align: center;
|
||||
transition: .1s;
|
||||
user-select: none;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
border-radius: .3rem;
|
||||
padding: 0 0.9rem;
|
||||
font-size: .9rem;
|
||||
height: 2rem;
|
||||
color: white;
|
||||
|
||||
& + .base-button {
|
||||
margin-left: var(--space);
|
||||
}
|
||||
|
||||
.loading {
|
||||
position: absolute;
|
||||
@@ -76,27 +81,21 @@ defineEmits(['click'])
|
||||
}
|
||||
|
||||
&.small {
|
||||
height: 2.4rem;
|
||||
|
||||
& > span {
|
||||
font-size: .8rem;
|
||||
}
|
||||
border-radius: 0.2rem;
|
||||
padding: 0 0.8rem;
|
||||
height: 1.6rem;
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
&.large {
|
||||
height: 3rem;
|
||||
font-size: 1.1rem;
|
||||
padding: 0 1.4rem;
|
||||
|
||||
& > span {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
padding: 0 1.3rem;
|
||||
height: 2.4rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
|
||||
& > span {
|
||||
font-size: 1rem;
|
||||
color: white;
|
||||
line-height: 1;
|
||||
transform: translateY(-5%);
|
||||
|
||||
:deep(a) {
|
||||
color: white;
|
||||
@@ -104,10 +103,9 @@ defineEmits(['click'])
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: .7;
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
|
||||
&.primary {
|
||||
background: var(--btn-primary);
|
||||
}
|
||||
@@ -117,12 +115,14 @@ defineEmits(['click'])
|
||||
border-bottom: 2px solid transparent;
|
||||
|
||||
&:hover {
|
||||
border-bottom: 2px solid var(--color-font-1);
|
||||
border-bottom: 2px solid var(--color-font-2);
|
||||
}
|
||||
}
|
||||
|
||||
&.info {
|
||||
background: var(--btn-info);
|
||||
border: 1px solid var(--color-main-text);
|
||||
color: var(--color-main-text);
|
||||
}
|
||||
|
||||
&.active {
|
||||
|
||||
@@ -41,10 +41,9 @@ $w: 1.4rem;
|
||||
border-radius: .3rem;
|
||||
background: transparent;
|
||||
transition: all .3s;
|
||||
//color: var(--color-main-active);
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
background: var(--color-primary);
|
||||
background: var(--color-icon-hightlight);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -58,4 +57,4 @@ $w: 1.4rem;
|
||||
height: $w;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
2
src/global.d.ts
vendored
2
src/global.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
import {cloneDeep} from "lodash-es"
|
||||
import {cloneDeep} from "@/utils"
|
||||
|
||||
export {}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Article, ArticleWord, DictType, getDefaultArticleWord, Sentence} from "@/types.ts";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {cloneDeep} from "@/utils";
|
||||
import nlp from "compromise/one";
|
||||
import {usePlayWordAudio} from "@/hooks/sound.ts";
|
||||
import {getSentenceAllText, getSentenceAllTranslateText} from "@/hooks/translate.ts";
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {Article, Dict, Word} from "@/types.ts";
|
||||
import {Article, Word} from "@/types.ts";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {cloneDeep,} from "lodash-es";
|
||||
import {isArticle} from "@/hooks/article.ts";
|
||||
|
||||
|
||||
export function useWordOptions() {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {Article, Sentence, TranslateEngine} from "@/types.ts";
|
||||
import Baidu from "@opentranslate/baidu";
|
||||
import {axiosInstance} from "@/utils/http.ts";
|
||||
import {Translator} from "@opentranslate/translator/src/translator.ts";
|
||||
|
||||
export function getSentenceAllTranslateText(article: Article) {
|
||||
@@ -27,7 +26,6 @@ export async function getNetworkTranslate(
|
||||
let translator: Translator
|
||||
if (translateEngine === TranslateEngine.Baidu) {
|
||||
translator = new Baidu({
|
||||
axios: axiosInstance as any,
|
||||
config: {
|
||||
appid: "20230910001811857",
|
||||
key: "Xxe_yftQR3K3Ue43NQMC"
|
||||
|
||||
15
src/main.ts
15
src/main.ts
@@ -1,32 +1,19 @@
|
||||
import {createApp} from 'vue'
|
||||
import './assets/css/style.scss'
|
||||
import 'virtual:uno.css';
|
||||
import App from './App.vue'
|
||||
// import Mobile from './Mobile.vue'
|
||||
import {createPinia} from "pinia"
|
||||
import ZH from "@/locales/zh-CN.ts";
|
||||
import {createI18n} from 'vue-i18n'
|
||||
import router from "@/router.ts";
|
||||
import VueVirtualScroller from 'vue-virtual-scroller'
|
||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
import 'virtual:uno.css';
|
||||
import './global.d.ts'
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: 'zh-CN',
|
||||
fallbackLocale: 'zh-CN',
|
||||
messages: {
|
||||
'zh-CN': ZH
|
||||
},
|
||||
})
|
||||
|
||||
const pinia = createPinia()
|
||||
// const app = createApp(Mobile)
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(VueVirtualScroller)
|
||||
// app.use(ElementPlus)
|
||||
app.use(pinia)
|
||||
app.use(i18n)
|
||||
app.use(router)
|
||||
|
||||
app.directive('opacity', (el, binding) => {
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import SlideHorizontal from "@/components/slide/SlideHorizontal.vue";
|
||||
import SlideItem from "@/components/slide/SlideItem.vue";
|
||||
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {showConfirmDialog, showToast} from "vant";
|
||||
import 'vant/lib/index.css'
|
||||
import {onMounted} from "vue";
|
||||
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {Dict} from "@/types.ts";
|
||||
import DictPlan from "@/pages/mobile/components/DictPlan.vue";
|
||||
import BackIcon from "@/components/icon/BackIcon.vue";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
const store = useBaseStore()
|
||||
let index = $ref(0)
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
})
|
||||
|
||||
function handleDel(item: Dict, index: number) {
|
||||
if (item.id === store.sdict.id) {
|
||||
//TODO
|
||||
} else {
|
||||
showConfirmDialog({title: '确认删除?', message: '删除后无法撤销,确认删除吗?',})
|
||||
.then(() => {
|
||||
store.word.bookList.splice(index, 1)
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mobile-page">
|
||||
<header>
|
||||
<BackIcon @click="router.back()"/>
|
||||
<div class="tabs">
|
||||
<div class="tab" :class="index === 0 && 'active'" @click="index = 0">修改计划</div>
|
||||
<div class="tab" :class="index === 1 && 'active'" @click="index = 1">更换词书</div>
|
||||
</div>
|
||||
</header>
|
||||
<SlideHorizontal v-model:index="index">
|
||||
<SlideItem>
|
||||
<DictPlan/>
|
||||
</SlideItem>
|
||||
<SlideItem>
|
||||
<div class="my-dcits">
|
||||
<div class="list">
|
||||
<div class="dict" v-for="(item,index) in store.word.bookList">
|
||||
<div class="title">
|
||||
<div class="name">{{ item.name }}</div>
|
||||
<span v-if="item.id === store.sdict.id">当前在学</span>
|
||||
<template v-else>
|
||||
<DeleteIcon
|
||||
v-if="index>=3"
|
||||
@click="handleDel(item,index)"/>
|
||||
</template>
|
||||
</div>
|
||||
<div class="chapter">每日{{ item.chapterWordNumber }}词 剩余100天</div>
|
||||
<el-progress
|
||||
:show-text="false"
|
||||
:percentage="90"
|
||||
/>
|
||||
<div class="progress">
|
||||
<span>已学单词</span>
|
||||
<span>0/{{ item.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BaseButton size="large">添加新书</BaseButton>
|
||||
</div>
|
||||
</SlideItem>
|
||||
</SlideHorizontal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
header {
|
||||
height: 60rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 0 var(--space);
|
||||
|
||||
.back {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
width: 100%;
|
||||
border-top: 1px solid gray;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.tab {
|
||||
width: 100rem;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.active {
|
||||
border-bottom: 2px solid gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.plan {
|
||||
padding: 10rem;
|
||||
|
||||
.dict {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rem;
|
||||
}
|
||||
|
||||
.set-plan {
|
||||
background: white;
|
||||
|
||||
.header {
|
||||
height: 60rem;
|
||||
color: black;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.picker-wrapper {
|
||||
display: flex;
|
||||
|
||||
.van-picker {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.my-dcits {
|
||||
height: 100%;
|
||||
padding: var(--space);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.list {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
margin-bottom: 20rem;
|
||||
}
|
||||
|
||||
.dict {
|
||||
padding: var(--space);
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-second-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rem;
|
||||
margin-bottom: 10rem;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,186 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import SlideHorizontal from "@/components/slide/SlideHorizontal.vue";
|
||||
import SlideItem from "@/components/slide/SlideItem.vue";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {groupBy} from "lodash-es";
|
||||
import {dictionaryResources} from "@/assets/dictionary.ts";
|
||||
import {Dict, DictResource, languageCategoryOptions} from "@/types.ts";
|
||||
import {onMounted} from "vue";
|
||||
import DictGroup from "./components/DictGroup.vue";
|
||||
import router from "@/router.ts";
|
||||
|
||||
let index = $ref(1)
|
||||
|
||||
const store = useBaseStore()
|
||||
|
||||
let currentLanguage = $ref('my')
|
||||
let currentTranslateLanguage = $ref('common')
|
||||
let groupByLanguage = groupBy(dictionaryResources, 'language')
|
||||
let translateLanguageList = $ref([])
|
||||
|
||||
function groupByDictTags(dictList: DictResource[]) {
|
||||
return dictList.reduce<Record<string, DictResource[]>>((result, dict) => {
|
||||
dict.tags.forEach((tag) => {
|
||||
if (Object.prototype.hasOwnProperty.call(result, tag)) {
|
||||
result[tag].push(dict)
|
||||
} else {
|
||||
result[tag] = [dict]
|
||||
}
|
||||
})
|
||||
return result
|
||||
}, {})
|
||||
}
|
||||
|
||||
const groupByTranslateLanguage = $computed(() => {
|
||||
let data: any
|
||||
if (currentLanguage === 'article') {
|
||||
let articleList = dictionaryResources.filter(v => v.type === 'article')
|
||||
data = groupBy(articleList, 'translateLanguage')
|
||||
} else if (currentLanguage === 'my') {
|
||||
data = {
|
||||
common: store.word.bookList.concat([{id: '',} as any])
|
||||
}
|
||||
} else {
|
||||
data = groupBy(groupByLanguage[currentLanguage], 'translateLanguage')
|
||||
}
|
||||
// console.log('groupByTranslateLanguage', data)
|
||||
translateLanguageList = Object.keys(data)
|
||||
currentTranslateLanguage = translateLanguageList[0]
|
||||
return data
|
||||
})
|
||||
|
||||
const groupedByCategoryAndTag = $computed(() => {
|
||||
const currentTranslateLanguageDictList = groupByTranslateLanguage[currentTranslateLanguage]
|
||||
const groupByCategory = groupBy(currentTranslateLanguageDictList, 'category')
|
||||
|
||||
let data = []
|
||||
for (const [key, value] of Object.entries(groupByCategory)) {
|
||||
data.push([key, groupByDictTags(value)])
|
||||
}
|
||||
// console.log('groupedByCategoryAndTag', data)
|
||||
return data
|
||||
})
|
||||
|
||||
let articleData = $ref({
|
||||
translateLanguageList: [],
|
||||
currentTranslateLanguage: '',
|
||||
dictList: []
|
||||
})
|
||||
|
||||
let wordData = $ref({
|
||||
translateLanguageList: [],
|
||||
currentTranslateLanguage: '',
|
||||
dictList: []
|
||||
})
|
||||
|
||||
function getData(type: string) {
|
||||
let articleList = dictionaryResources.filter(v => v.type === type)
|
||||
let data = groupBy(articleList, 'translateLanguage')
|
||||
let translateLanguageList = Object.keys(data)
|
||||
let currentTranslateLanguage = translateLanguageList[0]
|
||||
|
||||
const currentTranslateLanguageDictList = data[currentTranslateLanguage]
|
||||
const groupByCategory = groupBy(currentTranslateLanguageDictList, 'category')
|
||||
|
||||
let dictList = []
|
||||
for (const [key, value] of Object.entries(groupByCategory)) {
|
||||
dictList.push([key, groupByDictTags(value)])
|
||||
}
|
||||
return {
|
||||
translateLanguageList,
|
||||
currentTranslateLanguage,
|
||||
dictList,
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
let temp = getData('article')
|
||||
articleData = temp
|
||||
let temp1 = getData('word')
|
||||
wordData = temp1
|
||||
})
|
||||
|
||||
function selectDict(val: { dict: DictResource | Dict, index: number }) {
|
||||
// console.log('val', val)
|
||||
router.push({
|
||||
path: '/mobile/set-dict-plan', query: {id: val.dict.id}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page dict-manage">
|
||||
<div class="tabs">
|
||||
<div class="tab"
|
||||
:class="currentLanguage === item.id && 'active'"
|
||||
@click="currentLanguage = item.id,index=i"
|
||||
v-for="(item,i) in languageCategoryOptions">
|
||||
<img :src='item.flag' alt=""/>
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<SlideHorizontal
|
||||
v-model:index="index">
|
||||
<SlideItem>
|
||||
<div class="translate">
|
||||
<span>翻译:</span>
|
||||
<el-radio-group v-model="articleData.currentTranslateLanguage">
|
||||
<el-radio-button border v-for="i in articleData.translateLanguageList" :value="i">{{
|
||||
$t(i)
|
||||
}}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<DictGroup
|
||||
v-for="item in articleData.dictList"
|
||||
:select-id="store.sdict.id"
|
||||
:groupByTag="item[1]"
|
||||
:category="item[0]"
|
||||
/>
|
||||
</SlideItem>
|
||||
<SlideItem style="display: flex;">
|
||||
<div class="scroll">
|
||||
<div class="translate">
|
||||
<span>翻译:</span>
|
||||
<el-radio-group v-model="wordData.currentTranslateLanguage">
|
||||
<el-radio-button border v-for="i in wordData.translateLanguageList" :value="i">{{
|
||||
$t(i)
|
||||
}}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<DictGroup
|
||||
@select-dict="selectDict"
|
||||
v-for="item in wordData.dictList"
|
||||
:select-id="store.sdict.id"
|
||||
:groupByTag="item[1]"
|
||||
:category="item[0]"
|
||||
/>
|
||||
</div>
|
||||
</SlideItem>
|
||||
<SlideItem>3</SlideItem>
|
||||
<SlideItem>4</SlideItem>
|
||||
</SlideHorizontal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dict-manage {
|
||||
font-size: 18rem;
|
||||
|
||||
.tabs {
|
||||
height: 60rem;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid gray;
|
||||
|
||||
.tab {
|
||||
width: 80rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,100 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import router from "@/router.ts";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import {APP_NAME} from "../../utils/const.ts";
|
||||
|
||||
const store = useBaseStore()
|
||||
|
||||
function goPractice() {
|
||||
router.push('/mobile/practice')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page home">
|
||||
<div class="header">
|
||||
<div class="welcome">
|
||||
我在{{ APP_NAME }}学习
|
||||
</div>
|
||||
<div class="day">
|
||||
<div class="num">151</div>
|
||||
天
|
||||
</div>
|
||||
</div>
|
||||
<div class="current-dict">
|
||||
<div class="top">
|
||||
<div class="left" @click="router.push('/mobile/dict-detail')">
|
||||
<div class="name">{{ store.sdict.name }}</div>
|
||||
<Icon class="arrow" icon="mingcute:right-line" width="20"/>
|
||||
</div>
|
||||
<span>词表</span>
|
||||
</div>
|
||||
<el-progress
|
||||
:percentage="90"
|
||||
/>
|
||||
</div>
|
||||
<div class="btn">
|
||||
<BaseButton size="large" @click="goPractice">开始背单词吧!</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.home {
|
||||
font-size: 18rem;
|
||||
color: var(--color-font-2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
//align-items: center;
|
||||
//justify-content: center;
|
||||
|
||||
.header {
|
||||
margin-top: 30rem;
|
||||
padding: 20rem;
|
||||
|
||||
.day {
|
||||
margin-top: 20rem;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
.num {
|
||||
font-size: 60rem;
|
||||
transform: translateY(10rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.current-dict {
|
||||
margin: 20rem;
|
||||
padding: 20rem;
|
||||
background: var(--color-third-bg);
|
||||
border-radius: 8rem;
|
||||
|
||||
.top {
|
||||
display: flex;
|
||||
gap: 20rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rem;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rem;
|
||||
}
|
||||
|
||||
|
||||
span {
|
||||
font-size: 14rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin: 20rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,258 +0,0 @@
|
||||
<!--<script setup lang="ts">-->
|
||||
|
||||
<!--import {Icon} from "@iconify/vue";-->
|
||||
<!--import IconWrapper from "@/components/IconWrapper.vue";-->
|
||||
<!--import useTheme from "@/hooks/theme.ts";-->
|
||||
<!--import {useSettingStore} from "@/stores/setting.ts";-->
|
||||
<!---->
|
||||
<!--import SlideItem from "@/components/slide/SlideItem.vue";-->
|
||||
<!--import SlideHorizontal from "@/components/slide/SlideHorizontal.vue";-->
|
||||
<!--import BaseIcon from "@/components/BaseIcon.vue";-->
|
||||
<!--import WordList from "@/components/list/WordList.vue";-->
|
||||
<!--import {useRouter} from "vue-router";-->
|
||||
<!--import {useBaseStore} from "@/stores/base.ts";-->
|
||||
<!--import {useRuntimeStore} from "@/stores/runtime.ts";-->
|
||||
|
||||
<!--const {toggleTheme} = useTheme()-->
|
||||
<!--const router = useRouter()-->
|
||||
<!--const store = useBaseStore()-->
|
||||
<!--const runtimeStore = useRuntimeStore()-->
|
||||
<!--const settingStore = useSettingStore()-->
|
||||
<!--let index = $ref(0)-->
|
||||
<!--let isShowStarCount = $ref(false)-->
|
||||
|
||||
<!--function $nav() {-->
|
||||
<!--}-->
|
||||
|
||||
<!--function $no() {-->
|
||||
<!--}-->
|
||||
<!--</script>-->
|
||||
|
||||
<!--<template>-->
|
||||
<!-- <div class="page setting">-->
|
||||
<!-- <div ref="float" class="float">-->
|
||||
<!-- <div class="right">-->
|
||||
<!-- <IconWrapper>-->
|
||||
<!-- <Icon icon="fluent:search-24-regular"/>-->
|
||||
<!-- </IconWrapper>-->
|
||||
<!-- <IconWrapper>-->
|
||||
<!-- <Icon icon="ep:moon"-->
|
||||
<!-- v-if="settingStore.theme === 'dark'"-->
|
||||
<!-- @click="toggleTheme"/>-->
|
||||
<!-- <Icon icon="tabler:sun" v-else @click="toggleTheme"/>-->
|
||||
<!-- </IconWrapper>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div ref="desc" class="desc">-->
|
||||
<!-- <header ref="header"></header>-->
|
||||
<!-- <div class="detail">-->
|
||||
<!-- <div class="heat">-->
|
||||
<!-- <div class="text" @click="isShowStarCount = true">-->
|
||||
<!-- <span>收藏</span>-->
|
||||
<!-- <span class="num">123</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="text" @click="$nav('/people/follow-and-fans',{type:0})">-->
|
||||
<!-- <span>错误</span>-->
|
||||
<!-- <span class="num">123</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="text" @click="$nav('/people/follow-and-fans',{type:1})">-->
|
||||
<!-- <span>已掌握</span>-->
|
||||
<!-- <span class="num">123</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="description">-->
|
||||
<!-- <span>您已坚持了164天,加油!</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="my-buttons">-->
|
||||
<!-- <div class="button" @click="router.push('/mobile/setting')">-->
|
||||
<!-- <span>设置</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="button" @click="router.push('/mobile/data-manage')">-->
|
||||
<!-- <span>数据同步</span>-->
|
||||
<!-- <div class="not-read"></div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="nav">-->
|
||||
<!-- <div class="tabs">-->
|
||||
<!-- <div class="tab" :class="index === 0 && 'active'" @click="index = 0">当前</div>-->
|
||||
<!-- <div class="tab" :class="index === 1 && 'active'" @click="index = 1">收藏</div>-->
|
||||
<!-- <div class="tab" :class="index === 2 && 'active'" @click="index = 2">错词本</div>-->
|
||||
<!-- <div class="tab" :class="index === 3 && 'active'" @click="index = 3">已掌握</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="indicator" :style="{left:index * 25 + '%'}"></div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <SlideHorizontal-->
|
||||
<!-- v-model:index="index">-->
|
||||
<!-- <SlideItem>-->
|
||||
<!-- </SlideItem>-->
|
||||
<!-- <SlideItem>-->
|
||||
<!-- <div class="panel-page-item">-->
|
||||
<!-- <div class="list-header">-->
|
||||
<!-- <div class="left">-->
|
||||
<!-- <div class="dict-name">总词数:{{ store.collect.words.length }}</div>-->
|
||||
<!-- <BaseIcon icon="fluent:add-12-regular" title="添加" @click="addCollect"/>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <WordList-->
|
||||
<!-- v-if="store.collect.words.length"-->
|
||||
<!-- class="word-list"-->
|
||||
<!-- :list="store.collect.words">-->
|
||||
<!-- <template v-slot:suffix="{item,index}">-->
|
||||
<!-- <BaseIcon-->
|
||||
<!-- class="del"-->
|
||||
<!-- title="移除"-->
|
||||
<!-- icon="solar:trash-bin-minimalistic-linear"/>-->
|
||||
<!-- </template>-->
|
||||
<!-- </WordList>-->
|
||||
<!-- </div>-->
|
||||
<!-- </SlideItem>-->
|
||||
<!-- <SlideItem>4</SlideItem>-->
|
||||
<!-- <SlideItem>4</SlideItem>-->
|
||||
<!-- </SlideHorizontal>-->
|
||||
<!-- </div>-->
|
||||
<!--</template>-->
|
||||
|
||||
<!--<style scoped lang="scss">-->
|
||||
<!--$main-bg: rgb(21, 23, 36);-->
|
||||
<!--$second-btn-color: rgb(58, 58, 70);-->
|
||||
|
||||
<!--.setting {-->
|
||||
<!-- font-size: 18rem;-->
|
||||
<!-- display: flex;-->
|
||||
<!-- flex-direction: column;-->
|
||||
<!-- align-items: center;-->
|
||||
<!-- background: $main-bg;-->
|
||||
|
||||
<!-- .float {-->
|
||||
<!-- position: fixed;-->
|
||||
<!-- box-sizing: border-box;-->
|
||||
<!-- width: 100vw;-->
|
||||
<!-- z-index: 2;-->
|
||||
<!-- display: flex;-->
|
||||
<!-- justify-content: flex-end;-->
|
||||
<!-- align-items: center;-->
|
||||
<!-- height: 46rem;-->
|
||||
<!-- padding: 0 15rem;-->
|
||||
<!-- background: transparent;-->
|
||||
<!-- transition: all .2s;-->
|
||||
|
||||
<!-- .right {-->
|
||||
<!-- }-->
|
||||
<!-- }-->
|
||||
|
||||
<!-- .desc {-->
|
||||
<!-- width: 100%;-->
|
||||
|
||||
<!-- header {-->
|
||||
<!-- color: white;-->
|
||||
<!-- height: 200rem;-->
|
||||
<!-- background-image: url('../../assets/img/a.jpg');-->
|
||||
<!-- background-size: cover;-->
|
||||
<!-- background-position: center;-->
|
||||
<!-- background-repeat: no-repeat;-->
|
||||
<!-- box-sizing: border-box;-->
|
||||
<!-- }-->
|
||||
|
||||
<!-- //消息页面-->
|
||||
<!-- $msg-bg: rgb(22, 22, 22);-->
|
||||
<!-- $msg-subpage-card-bg: rgb(28, 30, 43); //二级页面,卡片背景-->
|
||||
|
||||
<!-- .detail {-->
|
||||
<!-- transform: translateY(-10rem);-->
|
||||
<!-- background: $main-bg;-->
|
||||
<!-- padding: 20rem;-->
|
||||
<!-- padding-bottom: 0;-->
|
||||
<!-- border-radius: 10rem 10rem 0 0;-->
|
||||
<!-- display: flex;-->
|
||||
<!-- flex-direction: column;-->
|
||||
<!-- gap: 20rem;-->
|
||||
|
||||
<!-- .heat {-->
|
||||
<!-- color: white;-->
|
||||
<!-- display: flex;-->
|
||||
<!-- align-items: center;-->
|
||||
<!-- font-size: 16rem;-->
|
||||
<!-- gap: 30rem;-->
|
||||
|
||||
<!-- .num {-->
|
||||
<!-- color: white;-->
|
||||
<!-- font-weight: bold;-->
|
||||
<!-- }-->
|
||||
|
||||
<!-- .text {-->
|
||||
<!-- display: flex;-->
|
||||
<!-- align-items: center;-->
|
||||
<!-- gap: 10rem;-->
|
||||
<!-- }-->
|
||||
<!-- }-->
|
||||
|
||||
<!-- .description {-->
|
||||
<!-- font-size: 16rem;-->
|
||||
<!-- color: white;-->
|
||||
<!-- }-->
|
||||
|
||||
<!-- .my-buttons {-->
|
||||
<!-- display: flex;-->
|
||||
<!-- gap: 20rem;-->
|
||||
<!-- justify-content: space-between;-->
|
||||
|
||||
<!-- .button {-->
|
||||
<!-- position: relative;-->
|
||||
<!-- flex: 1;-->
|
||||
<!-- font-size: 16rem;-->
|
||||
<!-- display: flex;-->
|
||||
<!-- align-items: center;-->
|
||||
<!-- justify-content: center;-->
|
||||
<!-- border-radius: 6rem;-->
|
||||
<!-- background: $second-btn-color;-->
|
||||
<!-- height: 40rem;-->
|
||||
<!-- color: white;-->
|
||||
<!-- }-->
|
||||
<!-- }-->
|
||||
<!-- }-->
|
||||
<!-- }-->
|
||||
|
||||
<!-- .nav {-->
|
||||
<!-- font-size: 16rem;-->
|
||||
<!-- width: 100%;-->
|
||||
<!-- height: 50rem;-->
|
||||
<!-- top: 0;-->
|
||||
<!-- left: 0;-->
|
||||
<!-- right: 0;-->
|
||||
<!-- z-index: 1;-->
|
||||
<!-- background: $main-bg;-->
|
||||
|
||||
<!-- .tabs {-->
|
||||
<!-- width: 100%;-->
|
||||
<!-- display: flex;-->
|
||||
<!-- justify-content: space-between;-->
|
||||
|
||||
<!-- .tab {-->
|
||||
<!-- height: 45rem;-->
|
||||
<!-- width: 45%;-->
|
||||
<!-- display: flex;-->
|
||||
<!-- justify-content: center;-->
|
||||
<!-- align-items: center;-->
|
||||
<!-- color: gray;-->
|
||||
<!-- transition: color .3s;-->
|
||||
|
||||
<!-- &.active {-->
|
||||
<!-- font-weight: bold;-->
|
||||
<!-- color: white;-->
|
||||
<!-- }-->
|
||||
<!-- }-->
|
||||
<!-- }-->
|
||||
|
||||
<!-- .indicator {-->
|
||||
<!-- height: 2px;-->
|
||||
<!-- background: gold;-->
|
||||
<!-- width: 25%;-->
|
||||
<!-- position: relative;-->
|
||||
<!-- transition: all .3s;-->
|
||||
<!-- //left: 50%;-->
|
||||
<!-- }-->
|
||||
<!-- }-->
|
||||
<!--}-->
|
||||
<!--</style>-->
|
||||
@@ -1,48 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import DictPlan from "@/pages/mobile/components/DictPlan.vue";
|
||||
import NavBar from "@/pages/mobile/components/NavBar.vue";
|
||||
import {onMounted} from "vue";
|
||||
import {useRoute} from "vue-router";
|
||||
import {Dict, getDefaultDict} from "@/types.ts";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {nanoid} from "nanoid";
|
||||
import {dictionaryResources} from "@/assets/dictionary.ts";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
|
||||
|
||||
const store = useBaseStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const settingStore = useSettingStore()
|
||||
const route = useRoute()
|
||||
let load = $ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
// console.log('route', route.query.id)
|
||||
let item = dictionaryResources.find(v => v.id === route.query.id)
|
||||
let find: Dict = store.word.bookList.find((v: Dict) => v.id === item.id)
|
||||
if (find) {
|
||||
runtimeStore.editDict = cloneDeep(find)
|
||||
} else {
|
||||
runtimeStore.editDict = cloneDeep({
|
||||
...getDefaultDict(),
|
||||
...item,
|
||||
})
|
||||
runtimeStore.editDict.id = nanoid(6)
|
||||
}
|
||||
load = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mobile-page">
|
||||
<NavBar title="设置任务量"/>
|
||||
<DictPlan v-if="load"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -1,26 +0,0 @@
|
||||
.setting-list {
|
||||
background: var(--color-header-bg);
|
||||
color: var(--color-font-2);
|
||||
border-radius: 8rem;
|
||||
width: 100%;
|
||||
|
||||
.item {
|
||||
height: 60rem;
|
||||
padding-left: 20rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10rem;
|
||||
|
||||
.right {
|
||||
padding-right: 10rem;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #f1f1f1;
|
||||
border-bottom: 1px solid var(--color-item-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import WordList from "@/pages/pc/components/list/WordList.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import PopConfirm from "@/pages/pc/components/PopConfirm.vue";
|
||||
import {Dict, DictType} from "@/types.ts";
|
||||
import {useRouter} from "vue-router";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {useWordOptions} from "@/hooks/dict.ts";
|
||||
|
||||
const router = useRouter()
|
||||
const store = useBaseStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const settingStore = useSettingStore()
|
||||
let practiceType = $ref(DictType.word)
|
||||
|
||||
const showCollectToggleButton = $computed(() => {
|
||||
if (store.sdict.type === DictType.collect) {
|
||||
// if (store.current.practiceType !== practiceType) {
|
||||
// return (practiceType === DictType.word && store.collectWord.words.length) ||
|
||||
// (practiceType === DictType.article && store.collectWord.articles.length)
|
||||
// }
|
||||
} else {
|
||||
return (practiceType === DictType.word && store.collectWord.words.length) ||
|
||||
(practiceType === DictType.article && store.collectWord.articles.length)
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
function changeIndex(dict: Dict) {
|
||||
store.changeDict(dict, practiceType)
|
||||
}
|
||||
|
||||
function addCollect() {
|
||||
runtimeStore.editDict = cloneDeep(store.collect)
|
||||
router.push({path: '/dict', query: {type: 'addWordOrArticle'}})
|
||||
}
|
||||
|
||||
const {
|
||||
delWrongWord,
|
||||
delSimpleWord,
|
||||
toggleWordCollect,
|
||||
} = useWordOptions()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="panel-page-item">
|
||||
<div class="list-header">
|
||||
<div class="left">
|
||||
<div class="dict-name">总词数:{{ store.collectWord.words.length }}</div>
|
||||
<BaseIcon icon="fluent:add-12-regular" title="添加" @click="addCollect"/>
|
||||
</div>
|
||||
<template v-if="showCollectToggleButton">
|
||||
<PopConfirm
|
||||
:title="`确认切换?`"
|
||||
@confirm="changeIndex( store.collect)"
|
||||
>
|
||||
<BaseButton size="small">切换</BaseButton>
|
||||
</PopConfirm>
|
||||
</template>
|
||||
</div>
|
||||
<WordList
|
||||
v-if="store.collectWord.words.length"
|
||||
class="word-list"
|
||||
:list="store.collectWord.words">
|
||||
<template v-slot:suffix="{item,index}">
|
||||
<BaseIcon
|
||||
class="del"
|
||||
@click="toggleWordCollect(item)"
|
||||
title="移除"
|
||||
icon="solar:trash-bin-minimalistic-linear"/>
|
||||
</template>
|
||||
</WordList>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -1,74 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {watch} from "vue";
|
||||
import {DictResource} from "@/types.ts";
|
||||
import DictList from "./DictList.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
category: string,
|
||||
groupByTag: any,
|
||||
selectId: string
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
selectDict: [val: { dict: DictResource, index: number }]
|
||||
detail: [],
|
||||
}>()
|
||||
const tagList = $computed(() => Object.keys(props.groupByTag))
|
||||
let currentTag = $ref(tagList[0])
|
||||
let list = $computed(() => {
|
||||
return props.groupByTag[currentTag]
|
||||
})
|
||||
|
||||
watch(() => props.groupByTag, () => {
|
||||
currentTag = tagList[0]
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dict-group">
|
||||
<div class="category">{{ category }}</div>
|
||||
<div class="tags">
|
||||
<div class="tag" :class="i === currentTag &&'active'"
|
||||
@click="currentTag = i"
|
||||
v-for="i in Object.keys(groupByTag)">{{ i }}
|
||||
</div>
|
||||
</div>
|
||||
<DictList
|
||||
@selectDict="e => emit('selectDict',e)"
|
||||
:list="list"
|
||||
:select-id="selectId"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dict-group {
|
||||
color: var(--color-font-1);
|
||||
margin-bottom: 40rem;
|
||||
//border-bottom: 1px dashed gray;
|
||||
|
||||
.category {
|
||||
font-size: 24rem;
|
||||
padding-bottom: 10rem;
|
||||
border-bottom: 1px dashed gray;
|
||||
}
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 10rem 0;
|
||||
|
||||
.tag {
|
||||
color: var(--color-font-1);
|
||||
cursor: pointer;
|
||||
padding: 5rem 10rem;
|
||||
border-radius: 20rem;
|
||||
|
||||
&.active {
|
||||
color: var(--color-font-active-1);
|
||||
background: gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,138 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {Dict, DictType} from "@/types.ts";
|
||||
import {Icon} from "@iconify/vue";
|
||||
|
||||
const props = defineProps<{
|
||||
dict?: Dict,
|
||||
active?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
selectDict: [val: { dict: Dict, index: number }]
|
||||
add: []
|
||||
}>()
|
||||
|
||||
let length = $computed(() => {
|
||||
let isWord = [DictType.word,DictType.collect,DictType.simple,DictType.wrong].includes(props.dict.type)
|
||||
let len: any = ''
|
||||
if (props.dict.length) {
|
||||
len = props.dict.length
|
||||
len += (isWord ? '词' : '篇')
|
||||
} else {
|
||||
if (isWord) {
|
||||
len = props.dict.originWords.length + '词'
|
||||
} else {
|
||||
len = props.dict.articles.length + '篇'
|
||||
}
|
||||
}
|
||||
return len
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="dict-item anim"
|
||||
:class="active && 'active'"
|
||||
>
|
||||
<template v-if="dict.id">
|
||||
<div class="top">
|
||||
<div class="name">{{ dict.name }}</div>
|
||||
<div class="desc">{{ dict.description }}</div>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div class="num">{{ length }}</div>
|
||||
</div>
|
||||
<div class="pin" v-if="dict.type === DictType.article">文章</div>
|
||||
</template>
|
||||
<div v-else class="add" @click.stop="emit('add')">
|
||||
<Icon icon="fluent:add-20-filled" width="38" color="#929596"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dict-item {
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
padding: 10rem;
|
||||
width: 30%;
|
||||
height: 165rem;
|
||||
border-radius: 10rem;
|
||||
position: relative;
|
||||
background: var(--color-third-bg);
|
||||
border: 1px solid var(--color-item-border);
|
||||
color: var(--color-font-1);
|
||||
font-size: 14rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
|
||||
.name {
|
||||
font-size: 16rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box; //作为弹性伸缩盒子模型显示。
|
||||
-webkit-box-orient: vertical; //设置伸缩盒子的子元素排列方式--从上到下垂直排列
|
||||
-webkit-line-clamp: 2; //显示的行
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: var(--color-font-2);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box; //作为弹性伸缩盒子模型显示。
|
||||
-webkit-box-orient: vertical; //设置伸缩盒子的子元素排列方式--从上到下垂直排列
|
||||
-webkit-line-clamp: 2; //显示的行
|
||||
}
|
||||
|
||||
.num {
|
||||
text-align: right;
|
||||
color: var(--color-font-2);
|
||||
//font-weight: bold;
|
||||
}
|
||||
|
||||
.go {
|
||||
position: absolute;
|
||||
right: 10rem;
|
||||
bottom: 15rem;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--color-item-active);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-item-active);
|
||||
}
|
||||
|
||||
.add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pin {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 55rem;
|
||||
width: 55rem;
|
||||
color: white;
|
||||
//background-color: skyblue;
|
||||
background-color: var(--color-main-active);
|
||||
clip-path: polygon(0 10%, 0% 100%, 100% 100%);
|
||||
font-size: 12rem;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
padding: 4rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,30 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {Dict} from "@/types.ts";
|
||||
|
||||
defineProps<{
|
||||
list?: Dict[],
|
||||
selectId?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
selectDict: [val: { dict: any, index: number }]
|
||||
detail: [],
|
||||
add: []
|
||||
}>()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dict-list">
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dict-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,193 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {Picker, showToast} from "vant";
|
||||
import 'vant/lib/index.css'
|
||||
import {onMounted} from "vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import router from "@/router.ts";
|
||||
|
||||
const store = useBaseStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
let columns = $ref([])
|
||||
let columns2 = $ref([])
|
||||
let chapterWordNumber = $ref([runtimeStore.editDict.chapterWordNumber])
|
||||
let length = $ref(runtimeStore.editDict.length)
|
||||
let completeDay = $ref([Math.ceil(length / chapterWordNumber[0])])
|
||||
|
||||
const onChange = ({selectedValues}) => {
|
||||
chapterWordNumber = selectedValues
|
||||
completeDay = [Math.ceil(length / chapterWordNumber[0])]
|
||||
};
|
||||
|
||||
const onChange2 = ({selectedValues}) => {
|
||||
completeDay = selectedValues
|
||||
for (let i = 0; i < columns.length; i++) {
|
||||
let v = columns[i]
|
||||
let s = Math.ceil(length / v.value)
|
||||
if (s === completeDay[0]) {
|
||||
chapterWordNumber = [v.value]
|
||||
break
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
let list = []
|
||||
if (length < 50) {
|
||||
list = Array.from({length: Math.floor(length / 5)}).map((v, i) => (i + 1) * 5)
|
||||
}
|
||||
if (length > 50) {
|
||||
list = Array.from({length: 10}).map((v, i) => (i + 1) * 5)
|
||||
}
|
||||
if (length > 100) {
|
||||
list = list.concat(Array.from({length: 5}).map((v, i) => 50 + (i + 1) * 10))
|
||||
} else {
|
||||
list = list.concat(Array.from({length: Math.floor((length - 50) / 10)}).map((v, i) => 50 + (i + 1) * 10))
|
||||
}
|
||||
if (length > 200) {
|
||||
list = list.concat(Array.from({length: 4}).map((v, i) => 100 + (i + 1) * 25))
|
||||
} else {
|
||||
list = list.concat(Array.from({length: Math.floor((length - 100) / 25)}).map((v, i) => 100 + (i + 1) * 25))
|
||||
}
|
||||
if (length > 500) {
|
||||
list = list.concat(Array.from({length: 6}).map((v, i) => 200 + (i + 1) * 50))
|
||||
} else {
|
||||
list = list.concat(Array.from({length: Math.floor((length - 200) / 50)}).map((v, i) => 200 + (i + 1) * 50))
|
||||
}
|
||||
if (length > 1000) {
|
||||
list = list.concat(Array.from({length: 5}).map((v, i) => 500 + (i + 1) * 100))
|
||||
} else {
|
||||
list = list.concat(Array.from({length: Math.floor((length - 500) / 100)}).map((v, i) => 500 + (i + 1) * 100))
|
||||
}
|
||||
if (length > 3000) {
|
||||
list = list.concat(Array.from({length: 8}).map((v, i) => 1000 + (i + 1) * 250))
|
||||
} else {
|
||||
list = list.concat(Array.from({length: Math.floor((length - 1000) / 250)}).map((v, i) => 1000 + (i + 1) * 250))
|
||||
}
|
||||
if (length > 10000) {
|
||||
list = list.concat(Array.from({length: 14}).map((v, i) => 3000 + (i + 1) * 500))
|
||||
} else {
|
||||
list = list.concat(Array.from({length: Math.floor((length - 3000) / 500)}).map((v, i) => 3000 + (i + 1) * 500))
|
||||
}
|
||||
let d = Math.floor((length - 10000) / 1000)
|
||||
if (d > 0) {
|
||||
list = list.concat(Array.from({length: d}).map((v, i) => 10000 + (i + 1) * 1000))
|
||||
}
|
||||
|
||||
list.push(length)
|
||||
|
||||
columns = list.map(value => {
|
||||
return {
|
||||
text: value,
|
||||
value,
|
||||
}
|
||||
})
|
||||
|
||||
let days = Array.from(new Set(list.map(v => Math.ceil(length / v)))).sort((a, b) => a - b)
|
||||
columns2 = days.map(value => {
|
||||
return {
|
||||
text: value,
|
||||
value
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function confirm() {
|
||||
runtimeStore.editDict.chapterWordNumber = chapterWordNumber[0]
|
||||
store.changeDict(runtimeStore.editDict)
|
||||
router.back()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="plan">
|
||||
<div class="content">
|
||||
<div class="dict">
|
||||
<div class="name">{{ runtimeStore.editDict.name }}</div>
|
||||
<div class="chapter">每日{{ chapterWordNumber[0] }}词 剩余{{ completeDay[0] }}天</div>
|
||||
<el-progress
|
||||
:show-text="false"
|
||||
:percentage="90"
|
||||
/>
|
||||
<div class="progress">
|
||||
<span>已学单词</span>
|
||||
<span>0/{{ runtimeStore.editDict.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="notice">
|
||||
<span>完成日期:</span>
|
||||
<span class="date">2023年1月1日</span>
|
||||
<span>预计每天11分钟</span>
|
||||
</div>
|
||||
<div class="set-plan">
|
||||
<div class="header">
|
||||
<span>每天背单词</span>
|
||||
<span>完成天数</span>
|
||||
</div>
|
||||
<div class="picker-wrapper">
|
||||
<Picker
|
||||
:show-toolbar="false"
|
||||
:model-value="chapterWordNumber"
|
||||
:columns="columns"
|
||||
@change="onChange"
|
||||
/>
|
||||
<Picker
|
||||
:show-toolbar="false"
|
||||
:model-value="completeDay"
|
||||
:columns="columns2"
|
||||
@change="onChange2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BaseButton size="large" @click="confirm">确认</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.plan {
|
||||
height: 100%;
|
||||
padding: 10rem;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
|
||||
.dict {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rem;
|
||||
}
|
||||
|
||||
.set-plan {
|
||||
background: white;
|
||||
|
||||
.header {
|
||||
height: 60rem;
|
||||
color: black;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.picker-wrapper {
|
||||
display: flex;
|
||||
|
||||
.van-picker {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -1,186 +0,0 @@
|
||||
<script lang="jsx">
|
||||
import {emitter as bus} from "@/utils/eventBus.ts";
|
||||
|
||||
export default {
|
||||
name: "Indicator",
|
||||
props: {
|
||||
activeIndex: {
|
||||
type: Number,
|
||||
default: () => 0
|
||||
},
|
||||
tabStyleWidth: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
},
|
||||
tabTexts: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
tabRender: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
//用于和slidList绑定,因为一个页面可能有多个slidList,但只有一个indicator组件
|
||||
name: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentSlideItemIndex: this.activeIndex,
|
||||
tabIndicatorRelationActiveIndexLefts: [],//指标和slideItem的index的对应left,
|
||||
indicatorSpace: 0,//indicator之间的间距
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
render() {
|
||||
/*
|
||||
* <div class="tabs" ref="tabs">
|
||||
<div class="tab"
|
||||
style="{width : tabStyleWidth}"
|
||||
v-for="(item,index) in tabTexts"
|
||||
:class="currentSlideItemIndex === index?'active':''"
|
||||
@click="changeIndex(index)">
|
||||
<span>{{ item }}</span></div>
|
||||
</div>
|
||||
* */
|
||||
return (
|
||||
<div className='indicator-ctn'>
|
||||
{this.tabRender ?
|
||||
this.tabRender() :
|
||||
<div className="tabs" ref="tabs">
|
||||
{
|
||||
this.tabTexts.map((item, index) => {
|
||||
return (
|
||||
<div className={this.currentSlideItemIndex === index ? 'active tab' : 'tab'}
|
||||
style={{width: this.tabStyleWidth || 100 / this.tabTexts.length + '%'}}
|
||||
onClick={e => this.changeIndex(index)}
|
||||
>
|
||||
< span> {item}</span>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div className="indicator" ref="indicator"
|
||||
style={{width: this.tabStyleWidth || 100 / this.tabTexts.length + '%'}}/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
mounted() {
|
||||
this.initTabs()
|
||||
bus.on(this.name + '-moved', this.move)
|
||||
bus.on(this.name + '-end', this.end)
|
||||
},
|
||||
methods: {
|
||||
changeIndex(index) {
|
||||
this.currentSlideItemIndex = index
|
||||
this.$attrs['onUpdate:active-index'] && this.$emit('update:active-index', this.currentSlideItemIndex)
|
||||
this.$setCss(this.indicatorRef, 'transition-duration', `300ms`)
|
||||
this.$setCss(this.indicatorRef, 'left', this.tabIndicatorRelationActiveIndexLefts[this.currentSlideItemIndex] + 'px')
|
||||
},
|
||||
initTabs() {
|
||||
let tabs = this.$refs.tabs
|
||||
this.indicatorRef = this.$refs.indicator
|
||||
for (let i = 0; i < tabs.children.length; i++) {
|
||||
let item = tabs.children[i]
|
||||
this.tabWidth = this.$getCss(item, 'width')
|
||||
this.tabIndicatorRelationActiveIndexLefts.push(
|
||||
item.getBoundingClientRect().x - tabs.children[0].getBoundingClientRect().x + (this.indicatorType === 'home' ? this.tabWidth * 0.15 : 0))
|
||||
}
|
||||
this.indicatorSpace = this.tabIndicatorRelationActiveIndexLefts[1] - this.tabIndicatorRelationActiveIndexLefts[0]
|
||||
this.$setCss(this.indicatorRef, 'transition-duration', `0ms`)
|
||||
this.$setCss(this.indicatorRef, 'left', this.tabIndicatorRelationActiveIndexLefts[this.currentSlideItemIndex] + 'px')
|
||||
},
|
||||
move(e) {
|
||||
this.$setCss(this.indicatorRef, 'left',
|
||||
this.tabIndicatorRelationActiveIndexLefts[this.currentSlideItemIndex] -
|
||||
e.x.distance / (this.$store.state.bodyWidth / this.indicatorSpace) + 'px')
|
||||
},
|
||||
end(index) {
|
||||
// console.log(index)
|
||||
this.currentSlideItemIndex = index
|
||||
this.$setCss(this.indicatorRef, 'transition-duration', `300ms`)
|
||||
this.$setCss(this.indicatorRef, 'left',
|
||||
this.tabIndicatorRelationActiveIndexLefts[this.currentSlideItemIndex] + 'px')
|
||||
setTimeout(() => {
|
||||
this.$setCss(this.indicatorRef, 'transition-duration', `0ms`)
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
$main-bg: rgb(21, 23, 36);
|
||||
$active-main-bg: rgb(31, 37, 52);
|
||||
$second-text-color: rgb(143, 143, 158);
|
||||
$second-btn-color: rgb(58, 58, 70);
|
||||
$second-btn-color-tran: rgba(58, 58, 70, .4);
|
||||
$line-color: rgb(37, 45, 66);
|
||||
$line-color2: rgb(56, 54, 67);
|
||||
$footer-color: black;
|
||||
|
||||
$primary-btn-color: rgb(252, 47, 86);
|
||||
$disable-primary-btn-color: rgba(252, 47, 86, .5);
|
||||
|
||||
$mask-dark: #000000bb;
|
||||
$mask-light: transparent;
|
||||
$mask-white: transparent;
|
||||
$mask-lightgray: rgba(0, 0, 0, 0.25);
|
||||
|
||||
$footer-height: 50rem;
|
||||
$header-height: 50rem;
|
||||
$indicator-height: 50rem;
|
||||
|
||||
$padding-page: 15rem;
|
||||
.indicator-ctn {
|
||||
font-size: 14rem;
|
||||
width: 100%;
|
||||
height: $indicator-height;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
background: $main-bg;
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-weight: bold;
|
||||
|
||||
.tab {
|
||||
height: 45rem;
|
||||
width: 45%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: gray;
|
||||
transition: color .3s;
|
||||
|
||||
&.active {
|
||||
color: white;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-left: 5rem;
|
||||
$width: 12rem;
|
||||
width: $width;
|
||||
height: $width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.indicator {
|
||||
height: 2px;
|
||||
background: gold;
|
||||
width: 45%;
|
||||
position: relative;
|
||||
transition: all .3s;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,108 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {useBaseStore} from "@/stores/base.ts"
|
||||
|
||||
import {computed, provide, watch} from "vue"
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {EventKey, useEvent} from "@/utils/eventBus.ts";
|
||||
import SlideHorizontal from "@/components/slide/SlideHorizontal.vue";
|
||||
import SlideItem from "@/components/slide/SlideItem.vue";
|
||||
import CollectList from "@/pages/mobile/components/CollectList.vue";
|
||||
import WrongList from "@/pages/mobile/components/WrongList.vue";
|
||||
import SimpleList from "@/pages/mobile/components/SimpleList.vue";
|
||||
|
||||
const store = useBaseStore()
|
||||
const settingStore = useSettingStore()
|
||||
let tabIndex = $ref(0)
|
||||
provide('tabIndex', computed(() => tabIndex))
|
||||
|
||||
watch(() => settingStore.showPanel, n => {
|
||||
if (n) {
|
||||
tabIndex = 0
|
||||
}
|
||||
})
|
||||
|
||||
useEvent(EventKey.changeDict, () => {
|
||||
tabIndex = 0
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div class="panel anim">
|
||||
<header>
|
||||
<div class="tabs">
|
||||
<div class="tab" :class="tabIndex === 0 && 'active'" @click="tabIndex = 0">当前</div>
|
||||
<div class="tab" :class="tabIndex === 1 && 'active'" @click="tabIndex = 1">{{ store.collectWord.name }}</div>
|
||||
<div class="tab" :class="tabIndex === 2 && 'active'" @click="tabIndex = 2">{{ store.wrong.name }}</div>
|
||||
<div class="tab" :class="tabIndex === 3 && 'active'" @click="tabIndex = 3">{{ store.known.name }}</div>
|
||||
</div>
|
||||
</header>
|
||||
<SlideHorizontal v-model:index="tabIndex">
|
||||
<SlideItem>
|
||||
<slot :active="tabIndex === 0 && settingStore.showPanel"></slot>
|
||||
</SlideItem>
|
||||
<SlideItem>
|
||||
<CollectList/>
|
||||
</SlideItem>
|
||||
<SlideItem>
|
||||
<WrongList/>
|
||||
</SlideItem>
|
||||
<SlideItem>
|
||||
<SimpleList/>
|
||||
</SlideItem>
|
||||
</SlideHorizontal>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
|
||||
$header-height: 50rem;
|
||||
.panel {
|
||||
border-radius: 8rem;
|
||||
width: 100%;
|
||||
background: var(--color-second-bg);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all .3s;
|
||||
z-index: 1;
|
||||
border: 1px solid var(--color-item-border);
|
||||
box-shadow: var(--shadow);
|
||||
|
||||
|
||||
& > header {
|
||||
min-height: 50rem;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10rem 15rem;
|
||||
border-bottom: 1px solid #e1e1e1;
|
||||
gap: 15rem;
|
||||
|
||||
.close {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15rem;
|
||||
font-size: 14rem;
|
||||
|
||||
.tab {
|
||||
cursor: pointer;
|
||||
word-break: keep-all;
|
||||
font-size: 16rem;
|
||||
transition: all .3s;
|
||||
color: gray;
|
||||
|
||||
&.active {
|
||||
color: var(--color-main-active);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,36 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import BackIcon from "@/components/icon/BackIcon.vue";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
const router = useRouter()
|
||||
defineProps<{
|
||||
title?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="nav-bar">
|
||||
<BackIcon @click="router.back()"/>
|
||||
<div class="title" v-if="title">{{ title }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.nav-bar {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 50rem;
|
||||
padding: 0 var(--space);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
font-size: 20rem;
|
||||
|
||||
:deep(.back-icon) {
|
||||
left: var(--space);
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,71 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import WordList from "@/pages/pc/components/list/WordList.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import PopConfirm from "@/pages/pc/components/PopConfirm.vue";
|
||||
|
||||
import {Dict, DictType} from "@/types.ts";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {useWordOptions} from "@/hooks/dict.ts";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {useRouter} from "vue-router";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
|
||||
const router = useRouter()
|
||||
const store = useBaseStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const settingStore = useSettingStore()
|
||||
let practiceType = $ref(DictType.word)
|
||||
|
||||
function changeIndex(dict: Dict) {
|
||||
store.changeDict(dict, practiceType)
|
||||
}
|
||||
|
||||
const {
|
||||
delSimpleWord,
|
||||
} = useWordOptions()
|
||||
|
||||
function addSimple() {
|
||||
runtimeStore.editDict = cloneDeep(store.simple)
|
||||
router.push({path: '/dict', query: {type: 'addWordOrArticle'}})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="panel-page-item">
|
||||
<div class="list-header">
|
||||
<div class="left">
|
||||
<div class="dict-name">总词数:{{ store.known.words.length }}</div>
|
||||
<BaseIcon icon="fluent:add-12-regular" title="添加" @click="addSimple"/>
|
||||
</div>
|
||||
<template v-if="store.sdict.type !== DictType.known && store.known.words.length">
|
||||
<PopConfirm
|
||||
:title="`确认切换?`"
|
||||
@confirm="changeIndex( store.known)"
|
||||
>
|
||||
<BaseButton size="small">切换</BaseButton>
|
||||
</PopConfirm>
|
||||
</template>
|
||||
</div>
|
||||
<WordList
|
||||
v-if="store.known.words.length"
|
||||
class="word-list"
|
||||
:list="store.known.words">
|
||||
<template v-slot:suffix="{item,index}">
|
||||
<BaseIcon
|
||||
class="del"
|
||||
@click="delSimpleWord(item)"
|
||||
title="移除"
|
||||
icon="solar:trash-bin-minimalistic-linear"/>
|
||||
</template>
|
||||
</WordList>
|
||||
<Empty v-else/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -1,59 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import WordList from "@/pages/pc/components/list/WordList.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import PopConfirm from "@/pages/pc/components/PopConfirm.vue";
|
||||
|
||||
import {Dict, DictType} from "@/types.ts";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {useWordOptions} from "@/hooks/dict.ts";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
|
||||
const store = useBaseStore()
|
||||
let practiceType = $ref(DictType.word)
|
||||
|
||||
function changeIndex(dict: Dict) {
|
||||
store.changeDict(dict, practiceType)
|
||||
}
|
||||
|
||||
const {
|
||||
delWrongWord,
|
||||
} = useWordOptions()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="panel-page-item">
|
||||
<template v-if="store.wrong.words.length">
|
||||
<div class="list-header">
|
||||
<div class="dict-name">总词数:{{ store.wrong.words.length }}</div>
|
||||
<template
|
||||
v-if="store.sdict.type !== DictType.wrong && store.wrong.words.length">
|
||||
<PopConfirm
|
||||
:title="`确认切换?`"
|
||||
@confirm="changeIndex( store.wrong)"
|
||||
>
|
||||
<BaseButton size="small">切换</BaseButton>
|
||||
</PopConfirm>
|
||||
</template>
|
||||
</div>
|
||||
<WordList
|
||||
class="word-list"
|
||||
:list="store.wrong.words">
|
||||
<template v-slot:suffix="{item,index}">
|
||||
<BaseIcon
|
||||
class="del"
|
||||
@click="delWrongWord(item)"
|
||||
title="移除"
|
||||
icon="solar:trash-bin-minimalistic-linear"/>
|
||||
</template>
|
||||
</WordList>
|
||||
</template>
|
||||
<Empty v-else/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -1,67 +0,0 @@
|
||||
<script setup>
|
||||
import {Icon} from "@iconify/vue";
|
||||
import Home from "@/pages/mobile/Home.vue";
|
||||
import DictListManage from "@/pages/mobile/DictListManage.vue";
|
||||
import My from "@/pages/mobile/my/My.vue";
|
||||
import {onMounted} from "vue";
|
||||
|
||||
defineOptions({
|
||||
name: 'Practice'
|
||||
})
|
||||
|
||||
let index = $ref(2)
|
||||
|
||||
onMounted(() => {
|
||||
console.log('onMounted')
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div class="mobile-page mobile">
|
||||
<div class="content">
|
||||
<Home v-if="index === 0 "/>
|
||||
<DictListManage v-if="index === 1"/>
|
||||
<My v-if="index === 2"/>
|
||||
</div>
|
||||
<div class="tabs">
|
||||
<div class="tab" @click="index = 0">
|
||||
<Icon width="30" icon="ph:exam"/>
|
||||
<span>单词</span>
|
||||
</div>
|
||||
<div class="tab" @click="index = 1">
|
||||
<Icon width="30" icon="iconoir:book"/>
|
||||
<span>词典</span>
|
||||
</div>
|
||||
<div class="tab" @click="index = 2">
|
||||
<Icon width="30" icon="iconoir:user"/>
|
||||
<span>我的</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mobile {
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
border-top: 1px solid var(--color-item-bg);
|
||||
height: 60rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 14rem;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -1,81 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import NavBar from "@/pages/mobile/components/NavBar.vue";
|
||||
import {APP_NAME} from "../../../utils/const.ts";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mobile-page">
|
||||
<NavBar title="关于我们"/>
|
||||
<div class="page-content">
|
||||
<div class="name">{{ APP_NAME }}</div>
|
||||
<div class="desc">可在网页上使用的背单词软件</div>
|
||||
<div class="git">
|
||||
Github地址: <a target="_blank" href="https://github.com/zyronon/typing-word">https://github.com/zyronon/typing-word</a>
|
||||
</div>
|
||||
<div class="features">功能列表</div>
|
||||
<ul>
|
||||
<li>
|
||||
<div class="title">背单词</div>
|
||||
<div class="txt">可以选择记忆或默写单词,提供了音标显示、发音功能(均可选美音、英音)、错误统计</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="title">背文章</div>
|
||||
<div class="txt">
|
||||
内置经典教材书籍,可以练习和背诵文章,逐句输入,自动发音。也可以自行添加、导入文章,提供一键翻译、译文对照功能
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="title">生词本、错词本、已掌握</div>
|
||||
<div class="txt">
|
||||
默写单词时输入错误会自动添加到错词本,以便后续复习。也可以添加到已掌握,之后再遇到这个词便会自动跳过,同时也可以将其添加到生词本中,以便巩固复习
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="title">默写模式</div>
|
||||
<div class="txt">
|
||||
在用户完成一个章节的练习后,如果有错误词,那么会重复练习错误词,直到没有错误词为止。完成之后弹出选项可选择默写本章、重复本章、下一章
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="title">词库</div>
|
||||
<div class="txt">内置了常用的 CET-4 、CET-6 、GMAT 、GRE 、IELTS 、SAT 、TOEFL
|
||||
、考研英语、专业四级英语、专业八级英语,也有程序员常见英语单词以及多种编程语言
|
||||
API 等词库。 尽可能满足大部分用户对背单词的需求,也非常欢迎社区贡献更多的词库。
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mobile-page {
|
||||
.name {
|
||||
padding: 30rem 0;
|
||||
font-size: 46rem;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #c9c9c9;
|
||||
margin-bottom: 20rem;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 22rem;
|
||||
}
|
||||
|
||||
.features {
|
||||
margin-top: 20rem;
|
||||
font-weight: bold;
|
||||
font-size: 22rem;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 10rem 0;
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,16 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import NavBar from "@/pages/mobile/components/NavBar.vue";
|
||||
import CollectList from "@/pages/mobile/components/CollectList.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mobile-page">
|
||||
<NavBar title="收藏"/>
|
||||
<CollectList/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -1,102 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {APP_NAME, EXPORT_DATA_KEY, SAVE_DICT_KEY, SAVE_SETTING_KEY} from "@/utils/const.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, shakeCommonDict} from "@/utils";
|
||||
import {saveAs} from "file-saver";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import NavBar from "@/pages/mobile/components/NavBar.vue";
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const store = useBaseStore()
|
||||
|
||||
function exportData() {
|
||||
let data = {
|
||||
version: EXPORT_DATA_KEY.version,
|
||||
val: {
|
||||
setting: {
|
||||
version: SAVE_SETTING_KEY.version,
|
||||
val: settingStore.$state
|
||||
},
|
||||
dict: {
|
||||
version: SAVE_DICT_KEY.version,
|
||||
val: shakeCommonDict(store.$state)
|
||||
}
|
||||
}
|
||||
}
|
||||
let blob = new Blob([JSON.stringify(data)], {type: "text/plain;charset=utf-8"});
|
||||
let date = new Date()
|
||||
let dateStr = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`
|
||||
saveAs(blob, `${APP_NAME}-User-Data-${dateStr}.json`);
|
||||
ElMessage.success('导出成功!')
|
||||
}
|
||||
|
||||
function importData(e) {
|
||||
let file = e.target.files[0]
|
||||
if (!file) return
|
||||
// no()
|
||||
let reader = new FileReader();
|
||||
reader.onload = function (v) {
|
||||
let str = v.target.result;
|
||||
if (str) {
|
||||
let obj = JSON.parse(str)
|
||||
if (obj.version === EXPORT_DATA_KEY.version) {
|
||||
|
||||
} else {
|
||||
//TODO
|
||||
}
|
||||
let data = obj.val
|
||||
let settingState = checkAndUpgradeSaveSetting(data.setting)
|
||||
settingStore.setState(settingState)
|
||||
let dictState = checkAndUpgradeSaveDict(data.dict)
|
||||
store.init(dictState)
|
||||
ElMessage.success('导入成功!')
|
||||
}
|
||||
}
|
||||
reader.readAsText(file);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mobile-page">
|
||||
<NavBar title="数据管理"/>
|
||||
<div class="page-content">
|
||||
<div class="row">
|
||||
<div class="main-title">数据导出</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">
|
||||
目前用户的所有数据(自定义设置、自定义词典、练习进度等)
|
||||
<b>仅保存在本地</b>
|
||||
。如果您需要在不同的设备、浏览器或者其他非官方部署上使用 {{ APP_NAME }}, 您需要手动进行数据同步和保存。
|
||||
</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<BaseButton @click="exportData">数据导出</BaseButton>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="main-title">数据导入</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">
|
||||
请注意,导入数据将
|
||||
<b style="color: red"> 完全覆盖 </b>
|
||||
当前数据。请谨慎操作。
|
||||
</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="import hvr-grow">
|
||||
<BaseButton>数据导入</BaseButton>
|
||||
<input type="file"
|
||||
accept="application/json"
|
||||
@change="importData">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -1,62 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import NavBar from "@/pages/mobile/components/NavBar.vue";
|
||||
import {GITHUB} from "@/config/ENV.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mobile-page">
|
||||
<NavBar title="反馈问题"/>
|
||||
<div class="page-content">
|
||||
<div>
|
||||
给我发Email:<a href="mailto:zyronon@163.com">zyronon@163.com</a>
|
||||
</div>
|
||||
<p>or</p>
|
||||
<div class="github">
|
||||
<span>在<a :href="GITHUB" target="_blank">Github</a>上给我提一个
|
||||
<a :href="`${GITHUB}/issues`" target="_blank">Issue</a>
|
||||
</span>
|
||||
<div class="options">
|
||||
<BaseButton>
|
||||
<a :href="`${GITHUB}/issues/new?assignees=&labels=&projects=&template=%E5%8D%95%E8%AF%8D%E9%94%99%E8%AF%AF---word-error.md&title=%E5%8D%95%E8%AF%8D%E9%94%99%E8%AF%AF+%7C+Word+error`"
|
||||
target="_blank">词典错误?</a>
|
||||
</BaseButton>
|
||||
<BaseButton>
|
||||
<a :href="`${GITHUB}/issues/new?assignees=&labels=&projects=&template=问题报告---bug-report-.md&title=问题报告+%7C+Bug+report+`"
|
||||
target="_blank">反馈BUG?</a>
|
||||
</BaseButton>
|
||||
<BaseButton>
|
||||
<a :href="`${GITHUB}/issues/new?assignees=&labels=&projects=&template=功能请求---feature-request.md&title=功能请求+%7C+Feature+request`"
|
||||
target="_blank">功能请求?</a>
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 30rem;
|
||||
}
|
||||
|
||||
.github {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space);
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,212 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {Icon} from "@iconify/vue";
|
||||
import IconWrapper from "@/pages/pc/components/IconWrapper.vue";
|
||||
import useTheme from "@/hooks/theme.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
|
||||
import SlideItem from "@/components/slide/SlideItem.vue";
|
||||
import SlideHorizontal from "@/components/slide/SlideHorizontal.vue";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import WordList from "@/pages/pc/components/list/WordList.vue";
|
||||
import {useRouter} from "vue-router";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
|
||||
const {toggleTheme} = useTheme()
|
||||
const router = useRouter()
|
||||
const store = useBaseStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const settingStore = useSettingStore()
|
||||
let index = $ref(0)
|
||||
let isShowStarCount = $ref(false)
|
||||
|
||||
function $nav() {
|
||||
}
|
||||
|
||||
function $no() {
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page my">
|
||||
<div ref="float" class="float">
|
||||
<div class="right">
|
||||
<IconWrapper>
|
||||
<Icon icon="fluent:search-24-regular"/>
|
||||
</IconWrapper>
|
||||
<IconWrapper>
|
||||
<Icon icon="ep:moon"
|
||||
v-if="settingStore.theme === 'dark'"
|
||||
@click="toggleTheme"/>
|
||||
<Icon icon="tabler:sun" v-else @click="toggleTheme"/>
|
||||
</IconWrapper>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="desc" class="desc">
|
||||
<header ref="header"></header>
|
||||
<div class="detail">
|
||||
<div class="heat">
|
||||
<div class="text" @click="router.push('/mobile/collect')">
|
||||
<span>收藏</span>
|
||||
<span class="num">123</span>
|
||||
</div>
|
||||
<div class="text" @click="router.push('/mobile/wrong')">
|
||||
<span>错误</span>
|
||||
<span class="num">123</span>
|
||||
</div>
|
||||
<div class="text" @click="router.push('/mobile/simple')">
|
||||
<span>已掌握</span>
|
||||
<span class="num">123</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="description">
|
||||
<span>您已坚持了164天,加油!</span>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="item" @click="router.push('/mobile/collect')">
|
||||
<img src="../../../assets/img/collect.png" alt="">
|
||||
<span>收藏</span>
|
||||
</div>
|
||||
<div class="item" @click="router.push('/mobile/wrong')">
|
||||
<img src="../../../assets/img/book2.png" alt="">
|
||||
<span>错词本</span>
|
||||
</div>
|
||||
<div class="item" @click="router.push('/mobile/simple')">
|
||||
<img src="../../../assets/img/complete.png" alt="">
|
||||
<span>已掌握</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-list">
|
||||
<div class="item" @click="router.push('/mobile/setting')">
|
||||
<Icon icon="uil:setting" width="22"/>
|
||||
<div class="right">
|
||||
<span>设置</span>
|
||||
<Icon class="arrow" icon="mingcute:right-line" width="20"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item" @click="router.push('/mobile/data-manage')">
|
||||
<Icon icon="mdi:database-cog-outline" width="22"/>
|
||||
<div class="right">
|
||||
<span>数据同步</span>
|
||||
<Icon class="arrow" icon="mingcute:right-line" width="20"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item" @click="router.push('/mobile/feedback')">
|
||||
<Icon icon="pepicons-pencil:letter-open" width="22"/>
|
||||
<div class="right">
|
||||
<span>反馈问题</span>
|
||||
<Icon class="arrow" icon="mingcute:right-line" width="20"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item" @click="router.push('/mobile/about')">
|
||||
<Icon icon="mdi:about-circle-outline" width="22"/>
|
||||
<div class="right" style="border-bottom: none">
|
||||
<span>关于我们</span>
|
||||
<Icon class="arrow" icon="mingcute:right-line" width="20"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '../common' as *;
|
||||
|
||||
.my {
|
||||
font-size: 18rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: var(--color-font-2);
|
||||
background: var(--color-second-bg);
|
||||
|
||||
.float {
|
||||
position: fixed;
|
||||
box-sizing: border-box;
|
||||
width: 100vw;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
height: 46rem;
|
||||
padding: 0 15rem;
|
||||
background: transparent;
|
||||
transition: all .2s;
|
||||
|
||||
.right {
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
width: 100%;
|
||||
|
||||
header {
|
||||
color: white;
|
||||
height: 200rem;
|
||||
background-image: url('../../../assets/img/a.png');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.detail {
|
||||
transform: translateY(-50rem);
|
||||
//padding: 20rem;
|
||||
background: var(--color-second-bg);
|
||||
padding-top: 30rem;
|
||||
border-radius: 20rem 20rem 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rem;
|
||||
padding: 20rem;
|
||||
|
||||
.heat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16rem;
|
||||
gap: 30rem;
|
||||
|
||||
.num {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rem;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 16rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20rem;
|
||||
background: var(--color-header-bg);
|
||||
border-radius: 8rem;
|
||||
|
||||
.item {
|
||||
height: 60rem;
|
||||
gap: 10rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
width: 40rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,17 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import NavBar from "@/pages/mobile/components/NavBar.vue";
|
||||
import WrongList from "@/pages/mobile/components/WrongList.vue";
|
||||
import SimpleList from "@/pages/mobile/components/SimpleList.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mobile-page">
|
||||
<NavBar title="已掌握"/>
|
||||
<SimpleList/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -1,16 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import NavBar from "@/pages/mobile/components/NavBar.vue";
|
||||
import WrongList from "@/pages/mobile/components/WrongList.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mobile-page">
|
||||
<NavBar title="错词本"/>
|
||||
<WrongList/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -1,243 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import NavBar from "@/pages/mobile/components/NavBar.vue";
|
||||
import {getAudioFileUrl, useChangeAllSound, usePlayAudio} from "@/hooks/sound.ts";
|
||||
import {SoundFileOptions} from "@/utils/const.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const store = useBaseStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mobile-page">
|
||||
<NavBar title="音效设置"/>
|
||||
<div class="page-content">
|
||||
<div class="row">
|
||||
<label class="main-title">所有音效</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.allSound"
|
||||
@change="useChangeAllSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">单词/句子自动发音</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.wordSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">单词/句子发音口音</label>
|
||||
<div class="wrapper">
|
||||
<el-select v-model="settingStore.wordSoundType"
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option label="美音" value="us"/>
|
||||
<el-option label="英音" value="uk"/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">音量</label>
|
||||
<div class="wrapper">
|
||||
<el-slider v-model="settingStore.wordSoundVolume"/>
|
||||
<span>{{ settingStore.wordSoundVolume }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">倍速</label>
|
||||
<div class="wrapper">
|
||||
<el-slider v-model="settingStore.wordSoundSpeed" :step="0.1" :min="0.5" :max="3"/>
|
||||
<span>{{ settingStore.wordSoundSpeed }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">按键音</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.keyboardSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="item-title">按键音效</label>
|
||||
<div class="wrapper">
|
||||
<el-select v-model="settingStore.keyboardSoundFile"
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in SoundFileOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
<div class="el-option-row">
|
||||
<span>{{ item.label }}</span>
|
||||
<VolumeIcon
|
||||
:time="100"
|
||||
@click="usePlayAudio(getAudioFileUrl(item.value)[0])"/>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">音量</label>
|
||||
<div class="wrapper">
|
||||
<el-slider v-model="settingStore.keyboardSoundVolume"/>
|
||||
<span>{{ settingStore.keyboardSoundVolume }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<!-- <div class="row">-->
|
||||
<!-- <label class="item-title">释义发音</label>-->
|
||||
<!-- <div class="wrapper">-->
|
||||
<!-- <el-switch v-model="settingStore.translateSound"-->
|
||||
<!-- inline-prompt-->
|
||||
<!-- active-text="开"-->
|
||||
<!-- inactive-text="关"-->
|
||||
<!-- />-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="row">-->
|
||||
<!-- <label class="sub-title">音量</label>-->
|
||||
<!-- <div class="wrapper">-->
|
||||
<!-- <el-slider v-model="settingStore.translateSoundVolume"/>-->
|
||||
<!-- <span>{{ settingStore.translateSoundVolume }}%</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">效果音(章节结算页烟花音效)</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.effectSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">音量</label>
|
||||
<div class="wrapper">
|
||||
<el-slider v-model="settingStore.effectSoundVolume"/>
|
||||
<span>{{ settingStore.effectSoundVolume }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-content {
|
||||
background: var(--color-header-bg);
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 10rem var(--space);
|
||||
|
||||
.row {
|
||||
min-height: 40rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: calc(var(--space) * 5);
|
||||
|
||||
.wrapper {
|
||||
height: 30rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space);
|
||||
|
||||
span {
|
||||
text-align: right;
|
||||
//width: 30rem;
|
||||
font-size: 12rem;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.set-key {
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
width: 150rem;
|
||||
box-sizing: border-box;
|
||||
margin-right: 10rem;
|
||||
height: 28rem;
|
||||
outline: none;
|
||||
font-size: 16rem;
|
||||
border: 1px solid gray;
|
||||
border-radius: 3rem;
|
||||
padding: 0 5rem;
|
||||
background: var(--color-second-bg);
|
||||
color: var(--color-font-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 22rem;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 16rem;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 14rem;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.scroll {
|
||||
flex: 1;
|
||||
padding-right: 10rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-bottom: 20rem;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-bottom: 10rem;
|
||||
font-size: 12rem;
|
||||
}
|
||||
|
||||
.line {
|
||||
border-bottom: 1px solid #c4c3c3;
|
||||
}
|
||||
}
|
||||
|
||||
.el-option-row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.icon-wrapper {
|
||||
transform: translateX(10rem);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,202 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import NavBar from "@/pages/mobile/components/NavBar.vue";
|
||||
import {getAudioFileUrl, useChangeAllSound, usePlayAudio} from "@/hooks/sound.ts";
|
||||
import {SoundFileOptions} from "@/utils/const.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {ShortcutKey} from "@/types.ts";
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const store = useBaseStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mobile-page">
|
||||
<NavBar title="其他设置"/>
|
||||
<div class="page-content">
|
||||
<div class="row">
|
||||
<label class="item-title">显示上一个/下一个单词</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.showNearWord"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="desc">
|
||||
开启后,练习中会在上方显示上一个/下一个单词
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">忽略大小写</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.ignoreCase"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="desc">
|
||||
开启后,输入时不区分大小写,如输入“hello”和“Hello”都会被认为是正确的
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">允许默写模式下显示提示</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.allowWordTip"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="desc">
|
||||
开启后,可以通过鼠标 hover 单词或者按 {{ settingStore.shortcutKeyMap[ShortcutKey.ShowWord] }} 显示正确答案
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">字体设置(仅可调整单词练习)</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">外语字体</label>
|
||||
<div class="wrapper">
|
||||
<el-slider
|
||||
:min="10"
|
||||
:max="100"
|
||||
v-model="settingStore.fontSize.wordForeignFontSize"/>
|
||||
<span>{{ settingStore.fontSize.wordForeignFontSize }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">中文字体</label>
|
||||
<div class="wrapper">
|
||||
<el-slider
|
||||
:min="10"
|
||||
:max="100"
|
||||
v-model="settingStore.fontSize.wordTranslateFontSize"/>
|
||||
<span>{{ settingStore.fontSize.wordTranslateFontSize }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">其他设置</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">切换下一个单词时间</label>
|
||||
<div class="wrapper">
|
||||
<el-input-number v-model="settingStore.waitTimeForChangeWord"
|
||||
:min="6"
|
||||
:max="100"
|
||||
type="number"
|
||||
/>
|
||||
<span>毫秒</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-content {
|
||||
background: var(--color-header-bg);
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 10rem var(--space);
|
||||
|
||||
.row {
|
||||
min-height: 40rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: calc(var(--space) * 5);
|
||||
|
||||
.wrapper {
|
||||
height: 30rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space);
|
||||
|
||||
span {
|
||||
text-align: right;
|
||||
//width: 30rem;
|
||||
font-size: 12rem;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.set-key {
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
width: 150rem;
|
||||
box-sizing: border-box;
|
||||
margin-right: 10rem;
|
||||
height: 28rem;
|
||||
outline: none;
|
||||
font-size: 16rem;
|
||||
border: 1px solid gray;
|
||||
border-radius: 3rem;
|
||||
padding: 0 5rem;
|
||||
background: var(--color-second-bg);
|
||||
color: var(--color-font-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 22rem;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 16rem;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 14rem;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.scroll {
|
||||
flex: 1;
|
||||
padding-right: 10rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-bottom: 20rem;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-bottom: 10rem;
|
||||
font-size: 12rem;
|
||||
}
|
||||
|
||||
.line {
|
||||
border-bottom: 1px solid #c4c3c3;
|
||||
}
|
||||
}
|
||||
|
||||
.el-option-row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.icon-wrapper {
|
||||
transform: translateX(10rem);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,64 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {Icon} from "@iconify/vue";
|
||||
import useTheme from "@/hooks/theme.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import NavBar from "@/pages/mobile/components/NavBar.vue";
|
||||
import {ref} from "vue";
|
||||
import router from "@/router.ts";
|
||||
|
||||
const {toggleTheme} = useTheme()
|
||||
const settingStore = useSettingStore()
|
||||
// @ts-ignore
|
||||
const gitLastCommitHash = ref(LATEST_COMMIT_HASH);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mobile-page setting">
|
||||
<NavBar title="设置"/>
|
||||
<div class="content">
|
||||
<div class="setting-list">
|
||||
<div class="item" @click="router.push('music-setting')">
|
||||
<Icon icon="bx:headphone" width="22"/>
|
||||
<div class="right">
|
||||
<span>音效设置</span>
|
||||
<Icon class="arrow" icon="mingcute:right-line" width="20"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item" @click="router.push('other-setting')">
|
||||
<Icon icon="icon-park-outline:setting-config" width="22"/>
|
||||
<div class="right" style="border-bottom: none">
|
||||
<span>其他设置</span>
|
||||
<Icon class="arrow" icon="mingcute:right-line" width="20"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="git-log">
|
||||
Build {{ gitLastCommitHash }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '../../common' as *;
|
||||
|
||||
.setting {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--color-second-bg);
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -1,113 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onDeactivated, onMounted, onUnmounted, watch} from "vue";
|
||||
import {usePracticeStore} from "@/stores/practice.ts";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {MessageBox} from "@/utils/MessageBox.tsx";
|
||||
import PracticeWord from "@/pages/mobile/practice/practice-word/index.vue";
|
||||
import {ShortcutKey} from "@/types.ts";
|
||||
import {useStartKeyboardEventListener} from "@/hooks/event.ts";
|
||||
import useTheme from "@/hooks/theme.ts";
|
||||
|
||||
|
||||
defineOptions({
|
||||
name: 'PracticeWord'
|
||||
})
|
||||
|
||||
const store = useBaseStore()
|
||||
const settingStore = useSettingStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const {toggleTheme} = useTheme()
|
||||
const practiceRef: any = $ref()
|
||||
|
||||
function write() {
|
||||
// console.log('write')
|
||||
settingStore.dictation = true
|
||||
repeat()
|
||||
}
|
||||
|
||||
//TODO 需要判断是否已忽略
|
||||
function repeat() {
|
||||
// console.log('repeat')
|
||||
emitter.emit(EventKey.resetWord)
|
||||
practiceRef.getCurrentPractice()
|
||||
}
|
||||
|
||||
function prev() {
|
||||
// console.log('next')
|
||||
if (store.sdict.chapterIndex === 0) {
|
||||
ElMessage.warning('已经在第一章了~')
|
||||
} else {
|
||||
store.sdict.chapterIndex--
|
||||
repeat()
|
||||
}
|
||||
}
|
||||
|
||||
function toggleShowTranslate() {
|
||||
settingStore.translate = !settingStore.translate
|
||||
}
|
||||
|
||||
function toggleDictation() {
|
||||
settingStore.dictation = !settingStore.dictation
|
||||
}
|
||||
|
||||
function toggleConciseMode() {
|
||||
settingStore.showToolbar = !settingStore.showToolbar
|
||||
settingStore.showPanel = settingStore.showToolbar
|
||||
}
|
||||
|
||||
function togglePanel() {
|
||||
settingStore.showPanel = !settingStore.showPanel
|
||||
}
|
||||
|
||||
function jumpSpecifiedChapter(val: number) {
|
||||
store.sdict.chapterIndex = val
|
||||
repeat()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
emitter.on(EventKey.write, write)
|
||||
emitter.on(EventKey.repeatStudy, repeat)
|
||||
emitter.on(EventKey.jumpSpecifiedChapter, jumpSpecifiedChapter)
|
||||
|
||||
emitter.on(ShortcutKey.PreviousChapter, prev)
|
||||
emitter.on(ShortcutKey.RepeatChapter, repeat)
|
||||
emitter.on(ShortcutKey.DictationChapter, write)
|
||||
emitter.on(ShortcutKey.ToggleShowTranslate, toggleShowTranslate)
|
||||
emitter.on(ShortcutKey.ToggleDictation, toggleDictation)
|
||||
emitter.on(ShortcutKey.ToggleTheme, toggleTheme)
|
||||
emitter.on(ShortcutKey.ToggleConciseMode, toggleConciseMode)
|
||||
emitter.on(ShortcutKey.TogglePanel, togglePanel)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
emitter.off(EventKey.write, write)
|
||||
emitter.off(EventKey.repeatStudy, repeat)
|
||||
emitter.off(EventKey.jumpSpecifiedChapter, jumpSpecifiedChapter)
|
||||
|
||||
emitter.off(ShortcutKey.PreviousChapter, prev)
|
||||
emitter.off(ShortcutKey.RepeatChapter, repeat)
|
||||
emitter.off(ShortcutKey.DictationChapter, write)
|
||||
emitter.off(ShortcutKey.ToggleShowTranslate, toggleShowTranslate)
|
||||
emitter.off(ShortcutKey.ToggleDictation, toggleDictation)
|
||||
emitter.off(ShortcutKey.ToggleTheme, toggleTheme)
|
||||
emitter.off(ShortcutKey.ToggleConciseMode, toggleConciseMode)
|
||||
emitter.off(ShortcutKey.TogglePanel, togglePanel)
|
||||
})
|
||||
|
||||
useStartKeyboardEventListener()
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div class="mobile-page">
|
||||
<PracticeWord ref="practiceRef"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -1,352 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {getDefaultWord, ShortcutKey, Word} from "@/types.ts";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {usePlayBeep, usePlayCorrect, usePlayKeyboardAudio, usePlayWordAudio} from "@/hooks/sound.ts";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import {nextTick, onMounted, onUnmounted, watch} from "vue";
|
||||
import Tooltip from "@/pages/pc/components/Tooltip.vue";
|
||||
|
||||
interface IProps {
|
||||
word: Word,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
word: () => getDefaultWord(),
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
complete: [],
|
||||
wrong: []
|
||||
}>()
|
||||
|
||||
let input = $ref('')
|
||||
let wrong = $ref('')
|
||||
let showFullWord = $ref(false)
|
||||
//输入锁定,因为跳转到下一个单词有延时,如果重复在延时期间内重复输入,导致会跳转N次
|
||||
let inputLock = false
|
||||
let wordRepeatCount = 0
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const playBeep = usePlayBeep()
|
||||
const playCorrect = usePlayCorrect()
|
||||
const playKeyboardAudio = usePlayKeyboardAudio()
|
||||
const playWordAudio = usePlayWordAudio()
|
||||
const volumeIconRef: any = $ref()
|
||||
|
||||
let displayWord = $computed(() => {
|
||||
return props.word.word.slice(input.length + wrong.length)
|
||||
})
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
emitter.on(EventKey.resetWord, () => {
|
||||
wrong = input = ''
|
||||
})
|
||||
|
||||
emitter.on(EventKey.onTyping, onTyping)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
emitter.off(EventKey.resetWord)
|
||||
emitter.off(EventKey.onTyping, onTyping)
|
||||
})
|
||||
|
||||
function repeat() {
|
||||
setTimeout(() => {
|
||||
wrong = input = ''
|
||||
wordRepeatCount++
|
||||
inputLock = false
|
||||
|
||||
if (settingStore.wordSound) {
|
||||
volumeIconRef?.play()
|
||||
}
|
||||
}, settingStore.waitTimeForChangeWord)
|
||||
}
|
||||
|
||||
async function onTyping(e: KeyboardEvent) {
|
||||
if (inputLock) return
|
||||
inputLock = true
|
||||
let letter = e.key
|
||||
let isTypingRight = false
|
||||
let isWordRight = false
|
||||
if (settingStore.ignoreCase) {
|
||||
isTypingRight = letter.toLowerCase() === props.word.word[input.length].toLowerCase()
|
||||
isWordRight = (input + letter).toLowerCase() === props.word.word.toLowerCase()
|
||||
} else {
|
||||
isTypingRight = letter === props.word.word[input.length]
|
||||
isWordRight = (input + letter) === props.word.word
|
||||
}
|
||||
if (isTypingRight) {
|
||||
input += letter
|
||||
wrong = ''
|
||||
playKeyboardAudio()
|
||||
} else {
|
||||
emit('wrong')
|
||||
wrong = letter
|
||||
playKeyboardAudio()
|
||||
playBeep()
|
||||
volumeIconRef?.play()
|
||||
setTimeout(() => {
|
||||
wrong = ''
|
||||
}, 500)
|
||||
}
|
||||
|
||||
if (isWordRight) {
|
||||
playCorrect()
|
||||
if (settingStore.repeatCount == 100) {
|
||||
if (settingStore.repeatCustomCount <= wordRepeatCount + 1) {
|
||||
setTimeout(() => emit('complete'), settingStore.waitTimeForChangeWord)
|
||||
} else {
|
||||
repeat()
|
||||
}
|
||||
} else {
|
||||
if (settingStore.repeatCount <= wordRepeatCount + 1) {
|
||||
setTimeout(() => emit('complete'), settingStore.waitTimeForChangeWord)
|
||||
} else {
|
||||
repeat()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
inputLock = false
|
||||
}
|
||||
}
|
||||
|
||||
function del() {
|
||||
console.log('del')
|
||||
playKeyboardAudio()
|
||||
|
||||
if (wrong) {
|
||||
wrong = ''
|
||||
} else {
|
||||
input = input.slice(0, -1)
|
||||
}
|
||||
console.log(input)
|
||||
}
|
||||
|
||||
function showWord() {
|
||||
if (settingStore.allowWordTip) {
|
||||
showFullWord = true
|
||||
}
|
||||
}
|
||||
|
||||
function hideWord() {
|
||||
showFullWord = false
|
||||
}
|
||||
|
||||
function play() {
|
||||
volumeIconRef?.play()
|
||||
}
|
||||
|
||||
defineExpose({del, showWord, hideWord, play})
|
||||
|
||||
let transHeight = $ref(150)
|
||||
let transWrapperRef = $ref<HTMLDivElement>()
|
||||
let showEnd = $ref(true)
|
||||
|
||||
const transStyle = $computed(() => {
|
||||
return {
|
||||
'justify-content': showEnd ? 'flex-end' : 'unset',
|
||||
height: transHeight + 'px',
|
||||
opacity: settingStore.translate ? 1 : 0
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.word, () => {
|
||||
wrong = input = ''
|
||||
wordRepeatCount = 0
|
||||
inputLock = false
|
||||
if (settingStore.wordSound) {
|
||||
volumeIconRef?.play(400, true)
|
||||
}
|
||||
transHeight = 150
|
||||
nextTick(() => {
|
||||
console.log('transWrapperRef.scrollHeight', transWrapperRef.scrollHeight)
|
||||
let scrollHeight = transWrapperRef.scrollHeight
|
||||
if (scrollHeight <= 240) {
|
||||
showEnd = true
|
||||
if (scrollHeight > transHeight) {
|
||||
transHeight = scrollHeight
|
||||
}
|
||||
} else {
|
||||
showEnd = scrollHeight <= transHeight
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="typing-word">
|
||||
<div class="translate"
|
||||
:style="transStyle"
|
||||
>
|
||||
<div class="wrapper" ref="transWrapperRef">
|
||||
<div class="translate-item" v-for="(v,i) in word.trans">
|
||||
<span>{{ (v.pos ? v.pos + '.' : '') + v.cn }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="word-wrapper"
|
||||
:style="{marginTop: transHeight + 6 + 'px'}"
|
||||
>
|
||||
<div class="word"
|
||||
:class="wrong && 'is-wrong'"
|
||||
:style="{fontSize: settingStore.fontSize.wordForeignFontSize +'rem'}"
|
||||
>
|
||||
<span class="input" v-if="input">{{ input }}</span>
|
||||
<span class="wrong" v-if="wrong">{{ wrong }}</span>
|
||||
<template v-if="settingStore.dictation">
|
||||
<span class="letter" v-if="!showFullWord"
|
||||
@mouseenter="settingStore.allowWordTip && (showFullWord = true)">{{
|
||||
displayWord.split('').map(() => '_').join('')
|
||||
}}</span>
|
||||
<span class="letter" v-else @mouseleave="showFullWord = false">{{ displayWord }}</span>
|
||||
</template>
|
||||
<span class="letter" v-else>{{ displayWord }}</span>
|
||||
</div>
|
||||
<Tooltip
|
||||
:title="`发音(${settingStore.shortcutKeyMap[ShortcutKey.PlayWordPronunciation]})`"
|
||||
>
|
||||
<VolumeIcon ref="volumeIconRef" :simple="true" :cb="() => playWordAudio(word.word)"/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="phonetic" v-if="settingStore.wordSoundType === 'us' && word.phonetic0">[{{ word.phonetic0 }}]</div>
|
||||
<div class="phonetic" v-if="settingStore.wordSoundType === 'uk' && word.phonetic1">[{{ word.phonetic1 }}]</div>
|
||||
<transition name="fade">
|
||||
<div class="other" v-if="settingStore.detail">
|
||||
<div class="sentences" v-if="word.sentences && word.sentences.length">
|
||||
<div class="title">例句</div>
|
||||
<div class="sentence" v-for="item in word.sentences">
|
||||
<div class="tran">{{ item.tran }}</div>
|
||||
<div class="v">{{ item.v }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sentences" v-if="word.phrases && word.phrases.length">
|
||||
<div class="title">短语</div>
|
||||
<div class="sentence" v-for="item in word.phrases">
|
||||
<div class="tran">{{ item.tran }}</div>
|
||||
<div class="v">{{ item.v }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.typing-word {
|
||||
width: 95%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
word-break: break-word;
|
||||
position: relative;
|
||||
color: var(--color-font-2);
|
||||
overflow: auto;
|
||||
padding-bottom: 20rem;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none; /* Chrome Safari */
|
||||
}
|
||||
|
||||
.other {
|
||||
margin-top: 10rem;
|
||||
width: 100%;
|
||||
font-size: 18rem;
|
||||
|
||||
.sentences {
|
||||
margin-bottom: 20rem;
|
||||
|
||||
.title {
|
||||
}
|
||||
|
||||
.sentence {
|
||||
margin-bottom: 8rem;
|
||||
|
||||
.tran {
|
||||
color: white;
|
||||
font-size: 18rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.v {
|
||||
color: var(--color-font-1);
|
||||
font-size: 14rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.phonetic, .translate {
|
||||
transition: opacity .3s;
|
||||
}
|
||||
|
||||
.phonetic {
|
||||
font-size: 14rem;
|
||||
margin-top: 5rem;
|
||||
font-family: var(--word-font-family);
|
||||
}
|
||||
|
||||
.translate {
|
||||
font-size: 18rem;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
height: 150px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
|
||||
.wrapper {
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.volumeIcon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.translate-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rem;
|
||||
}
|
||||
|
||||
.volumeIcon {
|
||||
transition: opacity .3s;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.word-wrapper {
|
||||
margin-top: 150px;
|
||||
margin-left: 30rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rem;
|
||||
color: var(--color-font-1);
|
||||
|
||||
.word {
|
||||
font-size: 48rem;
|
||||
line-height: 1;
|
||||
font-family: var(--word-font-family);
|
||||
letter-spacing: 5rem;
|
||||
|
||||
.input {
|
||||
color: rgb(22, 163, 74);
|
||||
}
|
||||
|
||||
.wrong {
|
||||
color: rgba(red, 0.6);
|
||||
}
|
||||
|
||||
&.is-wrong {
|
||||
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,454 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted, onUnmounted, watch} from "vue"
|
||||
import {useBaseStore} from "@/stores/base.ts"
|
||||
import {DefaultDisplayStatistics, DictType, getDefaultWord, ShortcutKey, Sort, Word} from "@/types.ts";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts"
|
||||
import {cloneDeep, reverse, shuffle} from "lodash-es"
|
||||
import {usePracticeStore} from "@/stores/practice.ts"
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {useOnKeyboardEventListener, useWindowClick} from "@/hooks/event.ts";
|
||||
import Typing from "@/pages/mobile/practice/practice-word/Typing.vue";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {useWordOptions} from "@/hooks/dict.ts";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import WordList from "@/pages/pc/components/list/WordList.vue";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
import MiniDialog from "@/pages/pc/components/dialog/MiniDialog.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import SlideHorizontal from "@/components/slide/SlideHorizontal.vue";
|
||||
import SlideItem from "@/components/slide/SlideItem.vue";
|
||||
import MobilePanel from "@/pages/mobile/components/MobilePanel.vue";
|
||||
import router from "@/router.ts";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import useTheme from "@/hooks/theme.ts";
|
||||
|
||||
interface IProps {
|
||||
words: Word[],
|
||||
index: number,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
words: [],
|
||||
index: -1
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:words': [val: Word[]],
|
||||
sort: [val: Word[]]
|
||||
}>()
|
||||
|
||||
const typingRef: any = $ref()
|
||||
const store = useBaseStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const statisticsStore = usePracticeStore()
|
||||
const settingStore = useSettingStore()
|
||||
const {toggleTheme} = useTheme()
|
||||
|
||||
const {
|
||||
isWordCollect,
|
||||
toggleWordCollect,
|
||||
isWordSimple,
|
||||
toggleWordSimple
|
||||
} = useWordOptions()
|
||||
|
||||
let data = $ref({
|
||||
index: props.index,
|
||||
words: props.words,
|
||||
wrongWords: [],
|
||||
})
|
||||
|
||||
let stat = cloneDeep(DefaultDisplayStatistics)
|
||||
let showSortOption = $ref(false)
|
||||
useWindowClick(() => showSortOption = false)
|
||||
|
||||
watch(() => props.words, () => {
|
||||
data.words = props.words
|
||||
data.index = props.index
|
||||
data.wrongWords = []
|
||||
|
||||
statisticsStore.startDate = Date.now()
|
||||
statisticsStore.inputWordNumber = 0
|
||||
statisticsStore.wrong = 0
|
||||
stat = cloneDeep(DefaultDisplayStatistics)
|
||||
|
||||
}, {immediate: true})
|
||||
|
||||
watch(data, () => {
|
||||
statisticsStore.total = data.words.length
|
||||
statisticsStore.index = data.index
|
||||
})
|
||||
|
||||
const word: Word = $computed(() => {
|
||||
return data.words[data.index] ?? getDefaultWord()
|
||||
})
|
||||
|
||||
function next(isTyping: boolean = true) {
|
||||
if (data.index === data.words.length - 1) {
|
||||
|
||||
//复制当前错词,因为第一遍错词是最多的,后续的练习都是从错词中练习
|
||||
if (stat.total === -1) {
|
||||
let now = Date.now()
|
||||
stat = {
|
||||
startDate: statisticsStore.startDate,
|
||||
endDate: now,
|
||||
spend: now - statisticsStore.startDate,
|
||||
total: props.words.length,
|
||||
correctRate: -1,
|
||||
inputWordNumber: statisticsStore.inputWordNumber,
|
||||
wrong: data.wrongWords.length,
|
||||
wrongWords: data.wrongWords,
|
||||
}
|
||||
stat.correctRate = 100 - Math.trunc(((stat.wrong) / (stat.total)) * 100)
|
||||
}
|
||||
|
||||
if (data.wrongWords.length) {
|
||||
console.log('当前背完了,但还有错词')
|
||||
data.words = cloneDeep(data.wrongWords)
|
||||
|
||||
statisticsStore.total = data.words.length
|
||||
statisticsStore.index = data.index = 0
|
||||
statisticsStore.inputWordNumber = 0
|
||||
statisticsStore.wrong = 0
|
||||
data.wrongWords = []
|
||||
} else {
|
||||
console.log('这章节完了')
|
||||
isTyping && statisticsStore.inputWordNumber++
|
||||
|
||||
let now = Date.now()
|
||||
stat.endDate = now
|
||||
stat.spend = now - stat.startDate
|
||||
}
|
||||
} else {
|
||||
data.index++
|
||||
isTyping && statisticsStore.inputWordNumber++
|
||||
console.log('这个词完了')
|
||||
if ([DictType.word].includes(store.sdict.type)
|
||||
&& store.knownWords.includes(word.word.toLowerCase())) {
|
||||
next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function wordWrong() {
|
||||
if (!store.wrong.originWords.find((v: Word) => v.word.toLowerCase() === word.word.toLowerCase())) {
|
||||
store.wrong.originWords.push(word)
|
||||
}
|
||||
if (!data.wrongWords.find((v: Word) => v.word.toLowerCase() === word.word.toLowerCase())) {
|
||||
data.wrongWords.push(word)
|
||||
statisticsStore.wrong++
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyUp(e: KeyboardEvent) {
|
||||
typingRef.hideWord()
|
||||
}
|
||||
|
||||
async function onKeyDown(e: KeyboardEvent) {
|
||||
// console.log('e', e)
|
||||
switch (e.key) {
|
||||
case 'Backspace':
|
||||
typingRef.del()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
useOnKeyboardEventListener(onKeyDown, onKeyUp)
|
||||
|
||||
//TODO 略过忽略的单词上
|
||||
function prev() {
|
||||
if (data.index === 0) {
|
||||
ElMessage.warning('已经是第一个了~')
|
||||
} else {
|
||||
data.index--
|
||||
}
|
||||
}
|
||||
|
||||
function skip(e: KeyboardEvent) {
|
||||
next(false)
|
||||
// e.preventDefault()
|
||||
}
|
||||
|
||||
function show(e: KeyboardEvent) {
|
||||
typingRef.showWord()
|
||||
}
|
||||
|
||||
function collect(e: KeyboardEvent) {
|
||||
toggleWordCollect(word)
|
||||
}
|
||||
|
||||
function toggleWordSimpleWrapper() {
|
||||
if (!isWordSimple(word)) {
|
||||
toggleWordSimple(word)
|
||||
//延迟一下,不知道为什么不延迟会导致当前条目不自动定位到列表中间
|
||||
setTimeout(() => next(false))
|
||||
} else {
|
||||
toggleWordSimple(word)
|
||||
}
|
||||
}
|
||||
|
||||
function play() {
|
||||
typingRef.play()
|
||||
}
|
||||
|
||||
function sort(type: Sort) {
|
||||
if (type === Sort.reverse) {
|
||||
ElMessage.success('已翻转排序')
|
||||
emit('sort', reverse(cloneDeep(data.words)))
|
||||
}
|
||||
if (type === Sort.random) {
|
||||
ElMessage.success('已随机排序')
|
||||
emit('sort', shuffle(data.words))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
emitter.on(ShortcutKey.ShowWord, show)
|
||||
emitter.on(ShortcutKey.Previous, prev)
|
||||
emitter.on(ShortcutKey.Next, skip)
|
||||
emitter.on(ShortcutKey.ToggleCollect, collect)
|
||||
emitter.on(ShortcutKey.ToggleSimple, toggleWordSimpleWrapper)
|
||||
emitter.on(ShortcutKey.PlayWordPronunciation, play)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
emitter.off(ShortcutKey.ShowWord, show)
|
||||
emitter.off(ShortcutKey.Previous, prev)
|
||||
emitter.off(ShortcutKey.Next, skip)
|
||||
emitter.off(ShortcutKey.ToggleCollect, collect)
|
||||
emitter.off(ShortcutKey.ToggleSimple, toggleWordSimpleWrapper)
|
||||
emitter.off(ShortcutKey.PlayWordPronunciation, play)
|
||||
})
|
||||
|
||||
let index = $ref(0)
|
||||
watch(() => index, n => {
|
||||
settingStore.showPanel = index === 1
|
||||
})
|
||||
|
||||
let inputRef = $ref<HTMLInputElement>()
|
||||
|
||||
function change(e) {
|
||||
console.log('e', e)
|
||||
e.key = e.data
|
||||
emitter.emit(EventKey.onTyping, e)
|
||||
inputRef.value = ''
|
||||
}
|
||||
|
||||
function nextWord() {
|
||||
settingStore.translate = false
|
||||
settingStore.detail = false
|
||||
setTimeout(() => {
|
||||
next(true)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function complete() {
|
||||
inputRef.blur()
|
||||
settingStore.detail = true
|
||||
}
|
||||
|
||||
function unknow() {
|
||||
settingStore.translate = true
|
||||
inputRef.focus()
|
||||
}
|
||||
|
||||
let bodyHeight = $ref('100vh')
|
||||
onMounted(() => {
|
||||
bodyHeight = document.body.clientHeight + 'px'
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="practice-center" :style="{height:bodyHeight}">
|
||||
<SlideHorizontal v-model:index="index">
|
||||
<SlideItem>
|
||||
<div class="practice-body" @click.stop="index = 0">
|
||||
<div class="tool-bar">
|
||||
<div class="left">
|
||||
<Icon icon="octicon:arrow-left-24" width="22"
|
||||
@click="router.back()"
|
||||
/>
|
||||
</div>
|
||||
<div class="right">
|
||||
<BaseIcon
|
||||
v-if="!isWordCollect(word)"
|
||||
class="collect"
|
||||
@click="toggleWordCollect(word)"
|
||||
icon="ph:star"/>
|
||||
<BaseIcon
|
||||
v-else
|
||||
class="fill"
|
||||
@click="toggleWordCollect(word)"
|
||||
icon="ph:star-fill"/>
|
||||
<BaseIcon
|
||||
@click="index = 1"
|
||||
icon="tdesign:menu-unfold"/>
|
||||
</div>
|
||||
</div>
|
||||
<input ref="inputRef"
|
||||
style="position:fixed;top:-200vh;"
|
||||
@input="change"
|
||||
type="text">
|
||||
<Typing
|
||||
v-loading="!store.load"
|
||||
ref="typingRef"
|
||||
:word="word"
|
||||
@complete="complete"
|
||||
/>
|
||||
<div class="options">
|
||||
<div class="wrapper">
|
||||
<BaseButton size="large" v-if="settingStore.detail" @click="nextWord">下一个</BaseButton>
|
||||
<template v-else>
|
||||
<BaseButton size="large" @click="unknow">不认识</BaseButton>
|
||||
<BaseButton size="large" @click="nextWord">认识</BaseButton>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SlideItem>
|
||||
<SlideItem style="width: 80vw;">
|
||||
<MobilePanel>
|
||||
<template v-slot="{active}">
|
||||
<div class="panel-page-item"
|
||||
v-loading="!store.load"
|
||||
>
|
||||
<div class="list-header">
|
||||
<div class="left">
|
||||
<div class="title">
|
||||
</div>
|
||||
<BaseIcon title="切换词典"
|
||||
icon="carbon:change-catalog"/>
|
||||
<div style="position:relative;"
|
||||
@click.stop="null">
|
||||
<BaseIcon
|
||||
title="改变顺序"
|
||||
icon="icon-park-outline:sort-two"
|
||||
@click="showSortOption = !showSortOption"
|
||||
/>
|
||||
<MiniDialog
|
||||
v-model="showSortOption"
|
||||
style="width: 130rem;"
|
||||
>
|
||||
<div class="mini-row-title">
|
||||
列表循环设置
|
||||
</div>
|
||||
<div class="mini-row">
|
||||
<BaseButton size="small" @click="sort(Sort.reverse)">翻转</BaseButton>
|
||||
<BaseButton size="small" @click="sort(Sort.random)">随机</BaseButton>
|
||||
</div>
|
||||
</MiniDialog>
|
||||
</div>
|
||||
<BaseIcon icon="bi:arrow-right"
|
||||
@click="next"
|
||||
v-if="store.sdict.chapterIndex < store.sdict.chapterWords.length - 1"/>
|
||||
</div>
|
||||
<div class="right">
|
||||
{{ data.words.length }}个单词
|
||||
</div>
|
||||
</div>
|
||||
<WordList
|
||||
v-if="data.words.length"
|
||||
:is-active="active"
|
||||
:static="false"
|
||||
:show-word="!settingStore.dictation"
|
||||
:show-translate="settingStore.translate"
|
||||
:list="data.words"
|
||||
:activeIndex="data.index"
|
||||
@click="(val:any) => data.index = val.index"
|
||||
>
|
||||
<template v-slot:suffix="{item,index}">
|
||||
<BaseIcon
|
||||
v-if="!isWordCollect(item)"
|
||||
class="collect"
|
||||
@click="toggleWordCollect(item)"
|
||||
title="收藏" icon="ph:star"/>
|
||||
<BaseIcon
|
||||
v-else
|
||||
class="fill"
|
||||
@click="toggleWordCollect(item)"
|
||||
title="取消收藏" icon="ph:star-fill"/>
|
||||
<BaseIcon
|
||||
v-if="!isWordSimple(item)"
|
||||
class="easy"
|
||||
@click="toggleWordSimple(item)"
|
||||
title="标记为已掌握"
|
||||
icon="material-symbols:check-circle-outline-rounded"/>
|
||||
<BaseIcon
|
||||
v-else
|
||||
class="fill"
|
||||
@click="toggleWordSimple(item)"
|
||||
title="取消标记已掌握"
|
||||
icon="material-symbols:check-circle-rounded"/>
|
||||
</template>
|
||||
</WordList>
|
||||
<Empty v-else/>
|
||||
</div>
|
||||
</template>
|
||||
</MobilePanel>
|
||||
</SlideItem>
|
||||
</SlideHorizontal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.practice-center {
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
font-size: 14rem;
|
||||
color: black;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
|
||||
.practice-body {
|
||||
width: 100vw;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 10rem;
|
||||
box-sizing: border-box;
|
||||
|
||||
.tool-bar {
|
||||
width: 100%;
|
||||
height: 50rem;
|
||||
display: flex;
|
||||
padding: 0 10rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
gap: 10rem;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.word) {
|
||||
letter-spacing: 0;
|
||||
font-size: 36rem !important;
|
||||
}
|
||||
|
||||
.options {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: 20rem;
|
||||
|
||||
.wrapper {
|
||||
width: 80%;
|
||||
display: flex;
|
||||
gap: 20rem;
|
||||
}
|
||||
|
||||
:deep(.base-button) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -1,58 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import TypingWord from "@/pages/mobile/practice/practice-word/TypingWord.vue";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {onMounted} from "vue";
|
||||
import {ShortcutKey, Word} from "@/types.ts";
|
||||
import {EventKey, useEvents} from "@/utils/eventBus.ts";
|
||||
|
||||
const store = useBaseStore()
|
||||
|
||||
let wordData = $ref({
|
||||
words: [],
|
||||
index: -1
|
||||
})
|
||||
|
||||
function getCurrentPractice() {
|
||||
|
||||
}
|
||||
|
||||
function sort(list: Word[]) {
|
||||
wordData.index = 0
|
||||
// syncMyDictList(store.currentDict)
|
||||
}
|
||||
|
||||
function next() {
|
||||
getCurrentPractice()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getCurrentPractice()
|
||||
|
||||
})
|
||||
|
||||
useEvents([
|
||||
[EventKey.changeDict, getCurrentPractice],
|
||||
[EventKey.continueStudy, next],
|
||||
[ShortcutKey.NextChapter, next],
|
||||
])
|
||||
|
||||
defineExpose({getCurrentPractice})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="practice">
|
||||
<TypingWord
|
||||
@sort="sort"
|
||||
v-model:words="wordData.words"
|
||||
:index="wordData.index"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.practice {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
@@ -4,7 +4,7 @@ import {ref, watch} from "vue";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {getAudioFileUrl, useChangeAllSound, usePlayAudio, useWatchAllSound} from "@/hooks/sound.ts";
|
||||
import {getShortcutKey, useDisableEventListener, useEventListener} from "@/hooks/event.ts";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {cloneDeep} from "@/utils";
|
||||
import {DefaultShortcutKeyMap, ShortcutKey} from "@/types.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {APP_NAME, EXPORT_DATA_KEY, SAVE_DICT_KEY, SAVE_SETTING_KEY, SoundFileOptions} from "@/utils/const.ts";
|
||||
@@ -15,7 +15,7 @@ import {checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, shakeCommonDict} fr
|
||||
import {GITHUB} from "@/config/ENV.ts";
|
||||
import dayjs from "dayjs";
|
||||
import BasePage from "@/pages/pc/components/BasePage.vue";
|
||||
|
||||
import {ElSwitch, ElSelect, ElOption, ElSlider, ElRadioGroup, ElRadio, ElInputNumber} from 'element-plus'
|
||||
|
||||
const emit = defineEmits<{
|
||||
toggleDisabledDialogEscKey: [val: boolean]
|
||||
@@ -162,11 +162,11 @@ function importData(e) {
|
||||
<div class="row">
|
||||
<label class="main-title">所有音效</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.allSound"
|
||||
@change="useChangeAllSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
<ElSwitch v-model="settingStore.allSound"
|
||||
@change="useChangeAllSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -174,35 +174,35 @@ function importData(e) {
|
||||
<div class="row">
|
||||
<label class="item-title">单词/句子自动发音</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.wordSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
<ElSwitch v-model="settingStore.wordSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">单词/句子发音口音</label>
|
||||
<div class="wrapper">
|
||||
<el-select v-model="settingStore.wordSoundType"
|
||||
placeholder="请选择"
|
||||
<ElSelect v-model="settingStore.wordSoundType"
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option label="美音" value="us"/>
|
||||
<el-option label="英音" value="uk"/>
|
||||
</el-select>
|
||||
<ElOption label="美音" value="us"/>
|
||||
<ElOption label="英音" value="uk"/>
|
||||
</ElSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">音量</label>
|
||||
<div class="wrapper">
|
||||
<el-slider v-model="settingStore.wordSoundVolume"/>
|
||||
<ElSlider v-model="settingStore.wordSoundVolume"/>
|
||||
<span>{{ settingStore.wordSoundVolume }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">倍速</label>
|
||||
<div class="wrapper">
|
||||
<el-slider v-model="settingStore.wordSoundSpeed" :step="0.1" :min="0.5" :max="3"/>
|
||||
<ElSlider v-model="settingStore.wordSoundSpeed" :step="0.1" :min="0.5" :max="3"/>
|
||||
<span>{{ settingStore.wordSoundSpeed }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,20 +210,20 @@ function importData(e) {
|
||||
<div class="row">
|
||||
<label class="item-title">按键音</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.keyboardSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
<ElSwitch v-model="settingStore.keyboardSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="item-title">按键音效</label>
|
||||
<div class="wrapper">
|
||||
<el-select v-model="settingStore.keyboardSoundFile"
|
||||
placeholder="请选择"
|
||||
<ElSelect v-model="settingStore.keyboardSoundFile"
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
<ElOption
|
||||
v-for="item in SoundFileOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
@@ -235,14 +235,14 @@ function importData(e) {
|
||||
:time="100"
|
||||
@click="usePlayAudio(getAudioFileUrl(item.value)[0])"/>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">音量</label>
|
||||
<div class="wrapper">
|
||||
<el-slider v-model="settingStore.keyboardSoundVolume"/>
|
||||
<ElSlider v-model="settingStore.keyboardSoundVolume"/>
|
||||
<span>{{ settingStore.keyboardSoundVolume }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -250,17 +250,17 @@ function importData(e) {
|
||||
<div class="row">
|
||||
<label class="item-title">效果音(输入错误、完成时的音效)</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.effectSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
<ElSwitch v-model="settingStore.effectSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">音量</label>
|
||||
<div class="wrapper">
|
||||
<el-slider v-model="settingStore.effectSoundVolume"/>
|
||||
<ElSlider v-model="settingStore.effectSoundVolume"/>
|
||||
<span>{{ settingStore.effectSoundVolume }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -269,19 +269,19 @@ function importData(e) {
|
||||
<div class="row">
|
||||
<label class="item-title">单词循环设置</label>
|
||||
<div class="wrapper">
|
||||
<el-radio-group v-model="settingStore.repeatCount">
|
||||
<el-radio :value="1" size="default">1</el-radio>
|
||||
<el-radio :value="2" size="default">2</el-radio>
|
||||
<el-radio :value="3" size="default">3</el-radio>
|
||||
<el-radio :value="5" size="default">5</el-radio>
|
||||
<el-radio :value="100" size="default">自定义</el-radio>
|
||||
</el-radio-group>
|
||||
<ElRadioGroup v-model="settingStore.repeatCount">
|
||||
<ElRadio :value="1" size="default">1</ElRadio>
|
||||
<ElRadio :value="2" size="default">2</ElRadio>
|
||||
<ElRadio :value="3" size="default">3</ElRadio>
|
||||
<ElRadio :value="5" size="default">5</ElRadio>
|
||||
<ElRadio :value="100" size="default">自定义</ElRadio>
|
||||
</ElRadioGroup>
|
||||
<div class="mini-row" v-if="settingStore.repeatCount === 100">
|
||||
<label class="item-title">循环次数</label>
|
||||
<el-input-number v-model="settingStore.repeatCustomCount"
|
||||
:min="6"
|
||||
:max="15"
|
||||
type="number"
|
||||
<ElInputNumber v-model="settingStore.repeatCustomCount"
|
||||
:min="6"
|
||||
:max="15"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -289,10 +289,10 @@ function importData(e) {
|
||||
<div class="row">
|
||||
<label class="item-title">显示上一个/下一个单词</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.showNearWord"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
<ElSwitch v-model="settingStore.showNearWord"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -303,10 +303,10 @@ function importData(e) {
|
||||
<div class="row">
|
||||
<label class="item-title">忽略大小写</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.ignoreCase"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
<ElSwitch v-model="settingStore.ignoreCase"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -317,10 +317,10 @@ function importData(e) {
|
||||
<div class="row">
|
||||
<label class="item-title">允许默写模式下显示提示</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.allowWordTip"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
<ElSwitch v-model="settingStore.allowWordTip"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -334,7 +334,7 @@ function importData(e) {
|
||||
<div class="row">
|
||||
<label class="sub-title">外语字体</label>
|
||||
<div class="wrapper">
|
||||
<el-slider
|
||||
<ElSlider
|
||||
:min="10"
|
||||
:max="100"
|
||||
v-model="settingStore.fontSize.wordForeignFontSize"/>
|
||||
@@ -344,7 +344,7 @@ function importData(e) {
|
||||
<div class="row">
|
||||
<label class="sub-title">中文字体</label>
|
||||
<div class="wrapper">
|
||||
<el-slider
|
||||
<ElSlider
|
||||
:min="10"
|
||||
:max="100"
|
||||
v-model="settingStore.fontSize.wordTranslateFontSize"/>
|
||||
@@ -359,10 +359,10 @@ function importData(e) {
|
||||
<div class="row">
|
||||
<label class="sub-title">切换下一个单词时间</label>
|
||||
<div class="wrapper">
|
||||
<el-input-number v-model="settingStore.waitTimeForChangeWord"
|
||||
:min="6"
|
||||
:max="100"
|
||||
type="number"
|
||||
<ElInputNumber v-model="settingStore.waitTimeForChangeWord"
|
||||
:min="6"
|
||||
:max="100"
|
||||
type="number"
|
||||
/>
|
||||
<span>毫秒</span>
|
||||
</div>
|
||||
@@ -453,17 +453,17 @@ function importData(e) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tabIndex === 5" class="about">
|
||||
<div v-if="tabIndex === 5" class="center flex-col">
|
||||
<h1>Type Words</h1>
|
||||
<p>
|
||||
本项目完全开源!好用请大家多多点Star!
|
||||
<p class="w-100 text-xl">
|
||||
感谢使用本项目!本项目是开源项目,如果觉得有帮助,请在 GitHub 点个 Star,您的支持是我持续改进的动力。
|
||||
</p>
|
||||
<p>
|
||||
GitHub地址:<a href="https://github.com/zyronon/typing-word">https://github.com/zyronon/typing-word</a>
|
||||
GitHub地址:<a href="https://github.com/zyronon/TypeWords" target="_blank">https://github.com/zyronon/TypeWords</a>
|
||||
</p>
|
||||
<p>
|
||||
反馈:<a
|
||||
href="https://github.com/zyronon/typing-word/issues">https://github.com/zyronon/typing-word/issues</a>
|
||||
href="https://github.com/zyronon/TypeWords/issues" target="_blank">https://github.com/zyronon/TypeWords/issues</a>
|
||||
</p>
|
||||
<div class="text-md color-gray">
|
||||
Build {{ gitLastCommitHash }}
|
||||
@@ -503,8 +503,8 @@ function importData(e) {
|
||||
gap: .6rem;
|
||||
|
||||
&.active {
|
||||
background: var(--color-main-active);
|
||||
color: var(--color-input-bg);
|
||||
background: var(--color-select-bg);
|
||||
color: var(--color-select-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -551,7 +551,7 @@ function importData(e) {
|
||||
border: 1px solid gray;
|
||||
border-radius: .2rem;
|
||||
padding: 0 .3rem;
|
||||
background: var(--color-second-bg);
|
||||
background: var(--color-second);
|
||||
color: var(--color-font-1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {enArticle} from "@/assets/dictionary.ts";
|
||||
import BasePage from "@/pages/pc/components/BasePage.vue";
|
||||
import {useNav} from "@/utils";
|
||||
import {Dict, DictResource, getDefaultDict} from "@/types.ts";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {cloneDeep} from "@/utils";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {getArticleBookDataByUrl} from "@/utils/article.ts";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
@@ -15,6 +15,7 @@ import Dialog from "@/pages/pc/components/dialog/Dialog.vue";
|
||||
import Input from "@/pages/pc/components/Input.vue";
|
||||
import {computed} from "vue";
|
||||
import Book from "@/pages/pc/components/Book.vue";
|
||||
import {ElProgress} from 'element-plus';
|
||||
|
||||
const {nav} = useNav()
|
||||
const base = useBaseStore()
|
||||
@@ -71,7 +72,7 @@ function startStudy() {
|
||||
<BasePage>
|
||||
<div class="card ">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="bg-slate-200 p-3 gap-4 rounded-md cursor-pointer flex items-center">
|
||||
<div class="bg-third p-3 gap-4 rounded-md cursor-pointer flex items-center">
|
||||
<span class="text-lg font-bold"
|
||||
@click="getBookDetail2(base.currentBook)">{{
|
||||
base.currentBook.name || '请选择书籍开始学习'
|
||||
@@ -86,7 +87,7 @@ function startStudy() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 text-sm">已学习{{ base.currentBook.lastLearnIndex }}篇文章</div>
|
||||
<el-progress class="mt-1" :percentage="base.currentBookProgress" :show-text="false"></el-progress>
|
||||
<ElProgress class="mt-1" :percentage="base.currentBookProgress" :show-text="false"></ElProgress>
|
||||
</div>
|
||||
|
||||
<div class="card flex flex-col">
|
||||
@@ -119,7 +120,7 @@ function startStudy() {
|
||||
</div>
|
||||
|
||||
<Dialog v-model="showAddChooseDialog" title="选项">
|
||||
<div class="color-black px-6 w-100">
|
||||
<div class="color-main px-6 w-100">
|
||||
<div class="cursor-pointer hover:bg-black/10 p-2 rounded"
|
||||
@click="showAddChooseDialog = false,showSearchDialog = true">选择一本书籍
|
||||
</div>
|
||||
@@ -131,7 +132,7 @@ function startStudy() {
|
||||
:show-close="false"
|
||||
@close="searchKey = ''"
|
||||
:header="false">
|
||||
<div class="color-black w-140">
|
||||
<div class="color-main w-140">
|
||||
<div class="p-4">
|
||||
<Input v-if="showSearchDialog" :autofocus="true" v-model="searchKey"/>
|
||||
</div>
|
||||
@@ -146,7 +147,7 @@ function startStudy() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="h-40 center flex-col text-xl color-black/60">
|
||||
<div v-else class="h-40 center flex-col text-xl color-main">
|
||||
<div> 请输入书籍名称搜索</div>
|
||||
<div>或直接在书籍列表选中</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import {onMounted, onUnmounted} from "vue";
|
||||
import {Article, getDefaultArticle} from "@/types.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {cloneDeep} from "@/utils";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
|
||||
import List from "@/pages/pc/components/list/List.vue";
|
||||
@@ -227,7 +227,7 @@ useWindowClick(() => showExport = false)
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
color: var(--color-font-1);
|
||||
background: var(--color-second-bg);
|
||||
background: var(--color-second);
|
||||
display: flex;
|
||||
|
||||
.close {
|
||||
|
||||
@@ -11,7 +11,6 @@ import BaseButton from "@/components/BaseButton.vue";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import EditBook from "@/pages/pc/article/components/EditBook.vue";
|
||||
import {computed, onMounted} from "vue";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const base = useBaseStore()
|
||||
@@ -73,7 +72,7 @@ function formClose() {
|
||||
<div class="flex justify-between items-center relative">
|
||||
<BackIcon class="z-2" @click="$router.back"/>
|
||||
<div class="absolute text-2xl text-align-center w-full">{{ runtimeStore.editDict.name }}</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex">
|
||||
<BaseButton type="info" @click="isEdit = true">编辑</BaseButton>
|
||||
<BaseButton type="info" @click="router.push('batch-edit-article')">文章管理</BaseButton>
|
||||
<BaseButton @click="addMyStudyList">学习</BaseButton>
|
||||
|
||||
@@ -1,661 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {Article, DefaultArticle, Sentence, TranslateEngine} from "@/types.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import EditAbleText from "@/pages/pc/components/EditAbleText.vue";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import {
|
||||
getNetworkTranslate,
|
||||
getSentenceAllText,
|
||||
getSentenceAllTranslateText,
|
||||
renewSectionTexts,
|
||||
renewSectionTranslates
|
||||
} from "@/hooks/translate.ts";
|
||||
|
||||
import {MessageBox} from "@/utils/MessageBox.tsx";
|
||||
import {getSplitTranslateText, usePlaySentenceAudio} from "@/hooks/article.ts";
|
||||
import {cloneDeep, last} from "lodash-es";
|
||||
import {watch} from "vue";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
import {UploadProps} from "element-plus";
|
||||
import {_nextTick, _parseLRC} from "@/utils";
|
||||
import * as Comparison from "string-comparison"
|
||||
import audio from '/public/sound/article/nce2-1/1.mp3'
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import Dialog from "@/pages/pc/components/dialog/Dialog.vue";
|
||||
|
||||
interface IProps {
|
||||
article?: Article,
|
||||
type?: 'single' | 'batch'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
article: () => getDefaultArticle(),
|
||||
type: 'single'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
save: [val: Article],
|
||||
saveAndNext: [val: Article]
|
||||
}>()
|
||||
|
||||
let networkTranslateEngine = $ref('baidu')
|
||||
let progress = $ref(0)
|
||||
let failCount = $ref(0)
|
||||
let textareaRef = $ref<HTMLTextAreaElement>()
|
||||
const TranslateEngineOptions = [
|
||||
{value: 'baidu', label: '百度'},
|
||||
{value: 'youdao', label: '有道'},
|
||||
]
|
||||
|
||||
let editArticle = $ref<Article>(getDefaultArticle())
|
||||
|
||||
watch(() => props.article, val => {
|
||||
editArticle = cloneDeep(val)
|
||||
progress = 0
|
||||
failCount = 0
|
||||
// let r = getSplitTranslateText(editArticle.textTranslate)
|
||||
// if (r) {
|
||||
// editArticle.textTranslate = r
|
||||
// ElMessage({
|
||||
// message: '检测到本地翻译未格式化,已自动格式化',
|
||||
// type: 'success',
|
||||
// duration: 3000
|
||||
// })
|
||||
// }
|
||||
renewSections()
|
||||
console.log('ar', editArticle)
|
||||
}, {immediate: true})
|
||||
|
||||
watch(() => editArticle.text, (s) => {
|
||||
if (!s.trim()) {
|
||||
editArticle.sections = []
|
||||
}
|
||||
})
|
||||
|
||||
function renewSections() {
|
||||
if (editArticle.text.trim()) {
|
||||
renewSectionTexts(editArticle)
|
||||
failCount = renewSectionTranslates(editArticle, editArticle.textTranslate)
|
||||
} else {
|
||||
editArticle.sections = []
|
||||
}
|
||||
}
|
||||
|
||||
function appendTranslate(str: string) {
|
||||
let selectionStart = textareaRef.selectionStart;
|
||||
let selectionEnd = textareaRef.selectionEnd;
|
||||
editArticle.textTranslate = editArticle.textTranslate.slice(0, selectionStart) + str + editArticle.textTranslate.slice(selectionEnd)
|
||||
}
|
||||
|
||||
function splitTranslateText() {
|
||||
if (editArticle.textTranslate.trim()){
|
||||
editArticle.textTranslate = getSplitTranslateText(editArticle.textTranslate.trim())
|
||||
renewSections()
|
||||
}
|
||||
}
|
||||
|
||||
function onPaste(event: ClipboardEvent) {
|
||||
event.preventDefault()
|
||||
// @ts-ignore
|
||||
let paste = (event.clipboardData || window.clipboardData).getData("text");
|
||||
return MessageBox.confirm(
|
||||
'是否需要自动分句',
|
||||
'提示',
|
||||
() => {
|
||||
let r = getSplitTranslateText(paste)
|
||||
if (r) {
|
||||
appendTranslate(r)
|
||||
renewSections()
|
||||
}
|
||||
},
|
||||
() => {
|
||||
appendTranslate(paste)
|
||||
renewSections()
|
||||
}, null,
|
||||
{
|
||||
confirmButtonText: '需要',
|
||||
cancelButtonText: '关闭',
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function onBlur() {
|
||||
document.removeEventListener('paste', onPaste);
|
||||
}
|
||||
|
||||
function onFocus() {
|
||||
document.addEventListener('paste', onPaste);
|
||||
}
|
||||
|
||||
//TODO
|
||||
async function startNetworkTranslate() {
|
||||
if (!editArticle.title.trim()) {
|
||||
return ElMessage.error('请填写标题!')
|
||||
}
|
||||
if (!editArticle.text.trim()) {
|
||||
return ElMessage.error('请填写正文!')
|
||||
}
|
||||
renewSectionTexts(editArticle)
|
||||
//注意!!!
|
||||
//这里需要用异步,因为watch了article.networkTranslate,改变networkTranslate了之后,会重新设置article.sections
|
||||
//导致getNetworkTranslate里面拿到的article.sections是废弃的值
|
||||
setTimeout(async () => {
|
||||
await getNetworkTranslate(editArticle, TranslateEngine.Baidu, true, (v: number) => {
|
||||
progress = v
|
||||
})
|
||||
failCount = 0
|
||||
})
|
||||
}
|
||||
|
||||
function saveSentenceTranslate(sentence: Sentence, val: string) {
|
||||
sentence.translate = val
|
||||
editArticle.textTranslate = getSentenceAllTranslateText(editArticle)
|
||||
renewSections()
|
||||
}
|
||||
|
||||
function saveSentenceText(sentence: Sentence, val: string) {
|
||||
sentence.text = val
|
||||
editArticle.text = getSentenceAllText(editArticle)
|
||||
renewSections()
|
||||
}
|
||||
|
||||
function save(option: 'save' | 'saveAndNext') {
|
||||
// return console.log(cloneDeep(editArticle))
|
||||
return new Promise((resolve: Function) => {
|
||||
// console.log('article', article)
|
||||
// copy(JSON.stringify(article))
|
||||
|
||||
editArticle.title = editArticle.title.trim()
|
||||
editArticle.titleTranslate = editArticle.titleTranslate.trim()
|
||||
editArticle.text = editArticle.text.trim()
|
||||
editArticle.textTranslate = editArticle.textTranslate.trim()
|
||||
|
||||
if (!editArticle.title) {
|
||||
ElMessage.error('请填写标题!')
|
||||
return resolve(false)
|
||||
}
|
||||
if (!editArticle.text) {
|
||||
ElMessage.error('请填写正文!')
|
||||
return resolve(false)
|
||||
}
|
||||
|
||||
const saveTemp = () => {
|
||||
emit(option as any, editArticle)
|
||||
return resolve(true)
|
||||
}
|
||||
|
||||
saveTemp()
|
||||
})
|
||||
}
|
||||
|
||||
//不知道为什么直接用editArticle,取到是空的默认值
|
||||
defineExpose({save, getEditArticle: () => cloneDeep(editArticle)})
|
||||
|
||||
const handleChange: UploadProps['onChange'] = (uploadFile, uploadFiles) => {
|
||||
console.log(uploadFile)
|
||||
let reader = new FileReader();
|
||||
reader.readAsText(uploadFile.raw, 'UTF-8');
|
||||
reader.onload = function (e) {
|
||||
let lrc: string = e.target.result as string;
|
||||
console.log(lrc)
|
||||
if (lrc.trim()) {
|
||||
let lrcList = _parseLRC(lrc)
|
||||
console.log('lrcList', lrcList)
|
||||
if (lrcList.length) {
|
||||
editArticle.lrcPosition = editArticle.sections.map((v, i) => {
|
||||
return v.map((w, j) => {
|
||||
for (let k = 0; k < lrcList.length; k++) {
|
||||
let s = lrcList[k]
|
||||
let d = Comparison.default.cosine.similarity(w.text, s.text)
|
||||
d = Comparison.default.levenshtein.similarity(w.text, s.text)
|
||||
d = Comparison.default.longestCommonSubsequence.similarity(w.text, s.text)
|
||||
// d = Comparison.default.metricLcs.similarity(w.text, s.text)
|
||||
// console.log(w.text, s.text, d)
|
||||
if (d >= 0.8) {
|
||||
w.audioPosition = [s.start, s.end ?? -1]
|
||||
break
|
||||
}
|
||||
}
|
||||
return w.audioPosition ?? []
|
||||
})
|
||||
}).flat()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let currentSentence = $ref<Sentence>({} as any)
|
||||
let editSentence = $ref<Sentence>({} as any)
|
||||
let preSentence = $ref<Sentence>({} as any)
|
||||
let showEditAudioDialog = $ref(false)
|
||||
let sentenceAudioRef = $ref<HTMLAudioElement>()
|
||||
let audioRef = $ref<HTMLAudioElement>()
|
||||
|
||||
function handleShowEditAudioDialog(val: Sentence, i: number, j: number) {
|
||||
showEditAudioDialog = true
|
||||
currentSentence = val
|
||||
editSentence = cloneDeep(val)
|
||||
preSentence = null
|
||||
audioRef.pause()
|
||||
if (j == 0) {
|
||||
if (i != 0) {
|
||||
preSentence = last(editArticle.sections[i - 1])
|
||||
}
|
||||
} else {
|
||||
preSentence = editArticle.sections[i][j - 1]
|
||||
}
|
||||
if (!editSentence.audioPosition?.length) {
|
||||
editSentence.audioPosition = [0, 0]
|
||||
if (preSentence) {
|
||||
editSentence.audioPosition = [preSentence.audioPosition[1] ?? 0, 0]
|
||||
}
|
||||
}
|
||||
_nextTick(() => {
|
||||
sentenceAudioRef.currentTime = editSentence.audioPosition[0]
|
||||
})
|
||||
}
|
||||
|
||||
function recordStart() {
|
||||
if (sentenceAudioRef.paused) {
|
||||
sentenceAudioRef.play()
|
||||
}
|
||||
editSentence.audioPosition[0] = Number(sentenceAudioRef.currentTime.toFixed(2))
|
||||
}
|
||||
|
||||
function recordEnd() {
|
||||
if (!sentenceAudioRef.paused) {
|
||||
sentenceAudioRef.pause()
|
||||
}
|
||||
editSentence.audioPosition[1] = Number(sentenceAudioRef.currentTime.toFixed(2))
|
||||
}
|
||||
|
||||
const {playSentenceAudio} = usePlaySentenceAudio()
|
||||
|
||||
function saveLrcPosition() {
|
||||
// showEditAudioDialog = false
|
||||
currentSentence.audioPosition = cloneDeep(editSentence.audioPosition)
|
||||
editArticle.lrcPosition = editArticle.sections.map((v, i) => v.map((w, j) => (w.audioPosition ?? []))).flat()
|
||||
}
|
||||
|
||||
function jumpAudio(time: number) {
|
||||
sentenceAudioRef.currentTime = time
|
||||
}
|
||||
|
||||
function setPreEndTimeToCurrentStartTime() {
|
||||
if (preSentence) {
|
||||
editSentence.audioPosition[0] = preSentence.audioPosition[1]
|
||||
}
|
||||
}
|
||||
|
||||
function setStartTime(val: Sentence, i: number, j: number) {
|
||||
let preSentence = null
|
||||
if (j == 0) {
|
||||
if (i != 0) {
|
||||
preSentence = last(editArticle.sections[i - 1])
|
||||
}
|
||||
} else {
|
||||
preSentence = editArticle.sections[i][j - 1]
|
||||
}
|
||||
if (preSentence) {
|
||||
val.audioPosition[0] = preSentence.audioPosition[1]
|
||||
} else {
|
||||
val.audioPosition[0] = Number(Number(audioRef.currentTime).toFixed(2))
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="content">
|
||||
<div class="row flex flex-col gap-2">
|
||||
<div class="title">原文</div>
|
||||
<div class="">标题:</div>
|
||||
<input
|
||||
v-model="editArticle.title"
|
||||
type="text"
|
||||
class="base-input"
|
||||
placeholder="请填写原文标题"
|
||||
/>
|
||||
<div class="">正文:</div>
|
||||
<textarea
|
||||
v-model="editArticle.text"
|
||||
:readonly="![100,0].includes(progress)"
|
||||
type="textarea"
|
||||
class="base-textarea"
|
||||
placeholder="请复制原文"
|
||||
>
|
||||
</textarea>
|
||||
<div class="justify-between items-center gap-2 flex">
|
||||
<ol class="py-0 pl-5 my-0 text-base color-black/60">
|
||||
<li>复制原文</li>
|
||||
<li>点击 <span class="color-red font-bold">分句</span> 按钮进行自动分句</li>
|
||||
<li><span class="color-red font-bold">或</span> 手动调整分句,一行一句,段落之间空一行</li>
|
||||
<li>修改完成后点击 <span class="color-red font-bold">应用</span> 按钮同步到左侧结果栏
|
||||
</li>
|
||||
</ol>
|
||||
<el-button type="primary" @click="renewSections">应用</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row flex flex-col gap-2">
|
||||
<div class="title">译文</div>
|
||||
<div class="flex gap-2">
|
||||
标题
|
||||
</div>
|
||||
<input
|
||||
v-model="editArticle.titleTranslate"
|
||||
type="text"
|
||||
class="base-input"
|
||||
placeholder="请填写翻译标题"
|
||||
/>
|
||||
|
||||
<div class="flex">
|
||||
<span>正文:</span>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="editArticle.textTranslate"
|
||||
:readonly="![100,0].includes(progress)"
|
||||
@blur="onBlur"
|
||||
@focus="onFocus"
|
||||
type="textarea"
|
||||
class="base-textarea"
|
||||
placeholder="请填写翻译"
|
||||
ref="textareaRef"
|
||||
>
|
||||
</textarea>
|
||||
<div class="justify-between items-center gap-2 flex">
|
||||
<ol class="py-0 pl-5 my-0 text-base color-black/60">
|
||||
<li>复制译文,如果没有请点击 <span class="color-red font-bold">翻译</span> 按钮</li>
|
||||
<li>点击 <span class="color-red font-bold">分句</span> 按钮进行自动分句</li>
|
||||
<li><span class="color-red font-bold">或</span> 手动调整分句,一行一句,段落之间空一行</li>
|
||||
<li>修改完成后点击 <span class="color-red font-bold">应用</span> 按钮同步到左侧结果栏
|
||||
</li>
|
||||
</ol>
|
||||
<div class="flex flex-col gap-2 items-end">
|
||||
<div class="translate-item">
|
||||
{{ progress }}%
|
||||
<el-select v-model="networkTranslateEngine"
|
||||
class="w-20"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in TranslateEngineOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="startNetworkTranslate"
|
||||
:loading="progress!==0 && progress !== 100"
|
||||
>翻译
|
||||
</el-button>
|
||||
<div>
|
||||
<el-button type="primary" @click="splitTranslateText">分句</el-button>
|
||||
<el-button type="primary" @click="renewSections">应用</el-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row flex flex-col gap-2">
|
||||
<div class="title">结果</div>
|
||||
<div class="center">正文、译文与结果均可编辑,修改一处,另外两处会自动同步变动</div>
|
||||
<div class="flex gap-2">
|
||||
<BaseButton>添加音频</BaseButton>
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
:limit="1"
|
||||
:on-change="handleChange"
|
||||
:auto-upload="false"
|
||||
>
|
||||
<el-button type="primary">添加音频LRC文件</el-button>
|
||||
</el-upload>
|
||||
<audio ref="audioRef" :src="editArticle.audioSrc" controls></audio>
|
||||
</div>
|
||||
<template v-if="editArticle.sections.length">
|
||||
<div class="flex-1 overflow-auto flex flex-col">
|
||||
<div class="flex justify-between bg-black/10 py-2">
|
||||
<div class="center flex-[7]">内容</div>
|
||||
<div>|</div>
|
||||
<div class="center flex-[3]">音频</div>
|
||||
</div>
|
||||
<div class="article-translate">
|
||||
<div class="section " v-for="(item,indexI) in editArticle.sections">
|
||||
<div class="section-title">第{{ indexI + 1 }}段</div>
|
||||
<div class="sentence" v-for="(sentence,indexJ) in item">
|
||||
<div class="flex-[7]">
|
||||
<EditAbleText
|
||||
:value="sentence.text"
|
||||
@save="(e:string) => saveSentenceText(sentence,e)"
|
||||
/>
|
||||
<EditAbleText
|
||||
class="text-lg!"
|
||||
v-if="sentence.translate"
|
||||
:value="sentence.translate"
|
||||
@save="(e:string) => saveSentenceTranslate(sentence,e)"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-[2] flex justify-end gap-1 items-center">
|
||||
<div class="flex justify-end gap-2">
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div>{{ sentence.audioPosition?.[0] ?? 0 }}s</div>
|
||||
<BaseIcon
|
||||
@click="setStartTime(sentence,indexI,indexJ)"
|
||||
:icon="indexI === 0 && indexJ === 0 ?'ic:sharp-my-location':'twemoji:end-arrow'"
|
||||
:title="indexI === 0 && indexJ === 0 ?'设置开始时间':'使用前一句的结束时间'"
|
||||
/>
|
||||
</div>
|
||||
<div>-</div>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div v-if="sentence.audioPosition?.[1] !== -1">{{ sentence.audioPosition?.[1] ?? 0 }}s</div>
|
||||
<div v-else> 结束</div>
|
||||
<BaseIcon
|
||||
@click="sentence.audioPosition[1] = Number(Number(audioRef.currentTime).toFixed(2))"
|
||||
title="设置结束时间"
|
||||
icon="ic:sharp-my-location"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<BaseIcon :icon="sentence.audioPosition?.length ? 'basil:edit-outline' : 'basil:add-outline'"
|
||||
@click="handleShowEditAudioDialog(sentence,indexI,indexJ)"/>
|
||||
<BaseIcon v-if="sentence.audioPosition?.length" icon="hugeicons:play"
|
||||
@click="playSentenceAudio(sentence,audioRef,editArticle)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="options" v-if="editArticle.text.trim()">
|
||||
<div class="status">
|
||||
<span>状态:</span>
|
||||
<div class="warning" v-if="failCount">
|
||||
<Icon icon="typcn:warning-outline"/>
|
||||
共有{{ failCount }}句没有翻译!
|
||||
</div>
|
||||
<div class="success" v-else>
|
||||
<Icon icon="mdi:success-circle-outline"/>
|
||||
翻译完成!
|
||||
</div>
|
||||
</div>
|
||||
<div class="left">
|
||||
<BaseButton @click="save('save')">保存</BaseButton>
|
||||
<BaseButton v-if="type === 'batch'" @click="save('saveAndNext')">保存并添加下一篇</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<Empty v-else text="没有译文对照~"/>
|
||||
</div>
|
||||
<Dialog title="设置音频与句子的对应位置(LRC)"
|
||||
v-model="showEditAudioDialog"
|
||||
:footer="true"
|
||||
@close="showEditAudioDialog = false"
|
||||
@ok="saveLrcPosition"
|
||||
>
|
||||
<div class="p-4 pt-0 color-black w-150 flex flex-col gap-2">
|
||||
<div class="">
|
||||
教程:点击音频播放按钮,当播放到句子开始时,点击开始时间的 <span class="color-red">记录</span>
|
||||
按钮;当播放到句子结束时,点击结束时间的 <span class="color-red">记录</span> 按钮,最后再试听是否正确
|
||||
</div>
|
||||
<audio ref="sentenceAudioRef" :src="editArticle.audioSrc" controls class="w-full"></audio>
|
||||
<div class="flex items-center gap-2 space-between mb-2" v-if="editSentence.audioPosition?.length">
|
||||
<div>{{ editSentence.text }}</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<div>
|
||||
<span>{{ editSentence.audioPosition?.[0] }}s</span>
|
||||
<span v-if="editSentence.audioPosition?.[1] !== -1"> - {{ editSentence.audioPosition?.[1] }}s</span>
|
||||
<span v-else> - 结束</span>
|
||||
</div>
|
||||
<BaseIcon icon="hugeicons:play"
|
||||
title="试听"
|
||||
@click="playSentenceAudio(editSentence,sentenceAudioRef,editArticle)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-2 items-center">
|
||||
<div>开始时间:</div>
|
||||
<div class="flex space-between flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-input-number v-model="editSentence.audioPosition[0]" :precision="2" :step="0.1">
|
||||
<template #suffix>
|
||||
<span>s</span>
|
||||
</template>
|
||||
</el-input-number>
|
||||
<BaseIcon
|
||||
@click="jumpAudio(editSentence.audioPosition[0])"
|
||||
title="跳转"
|
||||
icon="ic:sharp-my-location"
|
||||
/>
|
||||
<BaseIcon
|
||||
@click="setPreEndTimeToCurrentStartTime"
|
||||
title="使用前一句的结束时间"
|
||||
icon="twemoji:end-arrow"
|
||||
/>
|
||||
</div>
|
||||
<BaseButton @click="recordStart">记录</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<div>结束时间:</div>
|
||||
<div class="flex space-between flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-input-number v-model="editSentence.audioPosition[1]" :precision="2" :step="0.1">
|
||||
<template #suffix>
|
||||
<span>s</span>
|
||||
</template>
|
||||
</el-input-number>
|
||||
<span>或</span>
|
||||
<BaseButton size="small" @click="editSentence.audioPosition[1] = -1">结束</BaseButton>
|
||||
</div>
|
||||
<BaseButton @click="recordEnd">记录</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
|
||||
.content {
|
||||
color: var(--color-article);
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: var(--space);
|
||||
padding: var(--space);
|
||||
padding-top: .6rem;
|
||||
}
|
||||
|
||||
.row {
|
||||
flex: 7;
|
||||
width: 33%;
|
||||
//height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
//opacity: 0;
|
||||
|
||||
&:nth-child(3) {
|
||||
flex: 10;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
font-size: 1.4rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.translate-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: calc(var(--space) / 2);
|
||||
}
|
||||
|
||||
.article-translate {
|
||||
flex: 1;
|
||||
overflow-y: overlay;
|
||||
|
||||
.section {
|
||||
background: var(--color-textarea-bg);
|
||||
margin-bottom: 1.2rem;
|
||||
|
||||
.section-title {
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid var(--color-item-border);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.sentence {
|
||||
display: flex;
|
||||
padding: 0.5rem 1.5rem;
|
||||
line-height: 1.2;
|
||||
border-bottom: 1px solid var(--color-item-border);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.2rem;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.success {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.2rem;
|
||||
color: #67C23A;
|
||||
}
|
||||
|
||||
.left {
|
||||
gap: var(--space);
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -6,11 +6,10 @@ import EditAbleText from "@/pages/pc/components/EditAbleText.vue";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import {getNetworkTranslate, getSentenceAllText, getSentenceAllTranslateText} from "@/hooks/translate.ts";
|
||||
import {genArticleSectionData, splitCNArticle2, splitEnArticle2, usePlaySentenceAudio} from "@/hooks/article.ts";
|
||||
import {cloneDeep, last} from "lodash-es";
|
||||
import {_nextTick, _parseLRC, cloneDeep, last} from "@/utils";
|
||||
import {watch} from "vue";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
import {UploadProps} from "element-plus";
|
||||
import {_nextTick, _parseLRC} from "@/utils";
|
||||
import {ElInputNumber, ElOption, ElPopover, ElSelect, ElUpload, UploadProps} from "element-plus";
|
||||
import * as Comparison from "string-comparison"
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import Dialog from "@/pages/pc/components/dialog/Dialog.vue";
|
||||
@@ -286,13 +285,13 @@ function setStartTime(val: Sentence, i: number, j: number) {
|
||||
>
|
||||
</textarea>
|
||||
<div class="justify-end items-center flex">
|
||||
<el-popover
|
||||
<ElPopover
|
||||
class="box-item"
|
||||
title="使用方法"
|
||||
placement="top"
|
||||
:width="400"
|
||||
>
|
||||
<ol class="py-0 pl-5 my-0 text-base color-black/60">
|
||||
<ol class="py-0 pl-5 my-0 text-base color-main">
|
||||
<li>复制原文,然后分句</li>
|
||||
<li>点击 <span class="color-red font-bold">分句</span> 按钮进行自动分句<span
|
||||
class="color-red font-bold"> 或</span> 手动编辑分句
|
||||
@@ -304,9 +303,9 @@ function setStartTime(val: Sentence, i: number, j: number) {
|
||||
<template #reference>
|
||||
<Icon icon="ri:question-line" class="mr-3" width="20"/>
|
||||
</template>
|
||||
</el-popover>
|
||||
<el-button type="primary" @click="splitText">分句</el-button>
|
||||
<el-button type="primary" @click="apply">应用</el-button>
|
||||
</ElPopover>
|
||||
<BaseButton @click="splitText">分句</BaseButton>
|
||||
<BaseButton @click="apply()">应用</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row flex flex-col gap-2">
|
||||
@@ -334,27 +333,23 @@ function setStartTime(val: Sentence, i: number, j: number) {
|
||||
>
|
||||
</textarea>
|
||||
<div class="justify-between items-center flex">
|
||||
<div class="flex gap-2 items-center ">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="startNetworkTranslate"
|
||||
:loading="progress!==0 && progress !== 100"
|
||||
>翻译
|
||||
</el-button>
|
||||
<el-select v-model="networkTranslateEngine"
|
||||
class="w-20"
|
||||
<div class="flex gap-space items-center w-50 ">
|
||||
<BaseButton @click="startNetworkTranslate"
|
||||
:loading="progress!==0 && progress !== 100">翻译
|
||||
</BaseButton>
|
||||
<ElSelect v-model="networkTranslateEngine"
|
||||
>
|
||||
<el-option
|
||||
<ElOption
|
||||
v-for="item in TranslateEngineOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</ElSelect>
|
||||
{{ progress }}%
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<el-popover
|
||||
<ElPopover
|
||||
class="box-item"
|
||||
title="使用方法"
|
||||
placement="top"
|
||||
@@ -372,9 +367,9 @@ function setStartTime(val: Sentence, i: number, j: number) {
|
||||
<template #reference>
|
||||
<Icon icon="ri:question-line" class="mr-3" width="20"/>
|
||||
</template>
|
||||
</el-popover>
|
||||
<el-button type="primary" @click="splitTranslateText">分句</el-button>
|
||||
<el-button type="primary" @click="apply(true)">应用</el-button>
|
||||
</ElPopover>
|
||||
<BaseButton @click="splitTranslateText">分句</BaseButton>
|
||||
<BaseButton @click="apply(true)">应用</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -383,14 +378,14 @@ function setStartTime(val: Sentence, i: number, j: number) {
|
||||
<div class="center">正文、译文与结果均可编辑,编辑后点击应用按钮会自动同步</div>
|
||||
<div class="flex gap-2">
|
||||
<BaseButton>添加音频</BaseButton>
|
||||
<el-upload
|
||||
<ElUpload
|
||||
class="upload-demo"
|
||||
:limit="1"
|
||||
:on-change="handleChange"
|
||||
:auto-upload="false"
|
||||
>
|
||||
<el-button type="primary">添加音频LRC文件</el-button>
|
||||
</el-upload>
|
||||
<BaseButton>添加音频LRC文件</BaseButton>
|
||||
</ElUpload>
|
||||
<audio ref="audioRef" :src="editArticle.audioSrc" controls></audio>
|
||||
</div>
|
||||
<template v-if="editArticle.sections.length">
|
||||
@@ -474,7 +469,7 @@ function setStartTime(val: Sentence, i: number, j: number) {
|
||||
@close="showEditAudioDialog = false"
|
||||
@ok="saveLrcPosition"
|
||||
>
|
||||
<div class="p-4 pt-0 color-black w-150 flex flex-col gap-2">
|
||||
<div class="p-4 pt-0 color-main w-150 flex flex-col gap-2">
|
||||
<div class="">
|
||||
教程:点击音频播放按钮,当播放到句子开始时,点击开始时间的 <span class="color-red">记录</span>
|
||||
按钮;当播放到句子结束时,点击结束时间的 <span class="color-red">记录</span> 按钮,最后再试听是否正确
|
||||
@@ -498,11 +493,11 @@ function setStartTime(val: Sentence, i: number, j: number) {
|
||||
<div>开始时间:</div>
|
||||
<div class="flex justify-between flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-input-number v-model="editSentence.audioPosition[0]" :precision="2" :step="0.1">
|
||||
<ElInputNumber v-model="editSentence.audioPosition[0]" :precision="2" :step="0.1">
|
||||
<template #suffix>
|
||||
<span>s</span>
|
||||
</template>
|
||||
</el-input-number>
|
||||
</ElInputNumber>
|
||||
<BaseIcon
|
||||
@click="jumpAudio(editSentence.audioPosition[0])"
|
||||
title="跳转"
|
||||
@@ -521,11 +516,11 @@ function setStartTime(val: Sentence, i: number, j: number) {
|
||||
<div>结束时间:</div>
|
||||
<div class="flex justify-between flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-input-number v-model="editSentence.audioPosition[1]" :precision="2" :step="0.1">
|
||||
<ElInputNumber v-model="editSentence.audioPosition[1]" :precision="2" :step="0.1">
|
||||
<template #suffix>
|
||||
<span>s</span>
|
||||
</template>
|
||||
</el-input-number>
|
||||
</ElInputNumber>
|
||||
<span>或</span>
|
||||
<BaseButton size="small" @click="editSentence.audioPosition[1] = -1">结束</BaseButton>
|
||||
</div>
|
||||
@@ -539,8 +534,6 @@ function setStartTime(val: Sentence, i: number, j: number) {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
|
||||
.content {
|
||||
color: var(--color-article);
|
||||
height: 100%;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import {onMounted, onUnmounted} from "vue";
|
||||
import {Article, getDefaultArticle} from "@/types.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {cloneDeep} from "@/utils";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
|
||||
import List from "@/pages/pc/components/list/List.vue";
|
||||
@@ -234,7 +234,7 @@ useWindowClick(() => showExport = false)
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
color: var(--color-font-1);
|
||||
background: var(--color-second-bg);
|
||||
background: var(--color-second);
|
||||
display: flex;
|
||||
|
||||
.close {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {Dict, DictType, getDefaultDict} from "@/types.ts";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {cloneDeep} from "@/utils";
|
||||
|
||||
import {FormInstance, FormRules} from "element-plus";
|
||||
import {ElForm,ElFormItem,ElInput,ElSelect,ElOption, FormInstance, FormRules} from "element-plus";
|
||||
import {onMounted, reactive} from "vue";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
isAdd: boolean,
|
||||
@@ -85,38 +86,38 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="w-120 mt-4">
|
||||
<el-form
|
||||
<ElForm
|
||||
ref="dictFormRef"
|
||||
:rules="dictRules"
|
||||
:model="dictForm"
|
||||
label-width="8rem">
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="dictForm.name"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="dictForm.description" type="textarea"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="原文语言">
|
||||
<el-select v-model="dictForm.language" placeholder="请选择选项">
|
||||
<el-option label="英语" value="en"/>
|
||||
<el-option label="德语" value="de"/>
|
||||
<el-option label="日语" value="ja"/>
|
||||
<el-option label="代码" value="code"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="译文语言">
|
||||
<el-select v-model="dictForm.translateLanguage" placeholder="请选择选项">
|
||||
<el-option label="中文" value="zh-CN"/>
|
||||
<el-option label="英语" value="en"/>
|
||||
<el-option label="德语" value="de"/>
|
||||
<el-option label="日语" value="ja"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<ElFormItem label="名称" prop="name">
|
||||
<ElInput v-model="dictForm.name"/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="描述">
|
||||
<ElInput v-model="dictForm.description" type="textarea"/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="原文语言">
|
||||
<ElSelect v-model="dictForm.language" placeholder="请选择选项">
|
||||
<ElOption label="英语" value="en"/>
|
||||
<ElOption label="德语" value="de"/>
|
||||
<ElOption label="日语" value="ja"/>
|
||||
<ElOption label="代码" value="code"/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="译文语言">
|
||||
<ElSelect v-model="dictForm.translateLanguage" placeholder="请选择选项">
|
||||
<ElOption label="中文" value="zh-CN"/>
|
||||
<ElOption label="英语" value="en"/>
|
||||
<ElOption label="德语" value="de"/>
|
||||
<ElOption label="日语" value="ja"/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<div class="center">
|
||||
<el-button @click="emit('close')">关闭</el-button>
|
||||
<el-button type="primary" @click="onSubmit">确定</el-button>
|
||||
<base-button type="info" @click="emit('close')">关闭</base-button>
|
||||
<base-button type="primary" @click="onSubmit">确定</base-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</ElForm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -45,6 +45,6 @@ useDisableEventListener(() => props.modelValue)
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
background: var(--color-main-bg);
|
||||
background: var(--color-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, computed, watch, onMounted, nextTick} from 'vue'
|
||||
import {shuffle} from "lodash-es";
|
||||
import {computed, nextTick, onMounted, ref, watch} from 'vue'
|
||||
import {shuffle} from "@/utils";
|
||||
|
||||
const props = defineProps({
|
||||
stem: String,
|
||||
|
||||
@@ -6,7 +6,6 @@ import {usePracticeStore} from "@/stores/practice.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {usePlayBeep, usePlayCorrect, usePlayKeyboardAudio, usePlayWordAudio} from "@/hooks/sound.ts";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import jq from 'jquery'
|
||||
import {_nextTick} from "@/utils";
|
||||
import '@imengyu/vue3-context-menu/lib/vue3-context-menu.css'
|
||||
import ContextMenu from '@imengyu/vue3-context-menu'
|
||||
@@ -75,7 +74,6 @@ const store = useBaseStore()
|
||||
const statisticsStore = usePracticeStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
window.$ = jq
|
||||
watch([() => sectionIndex, () => sentenceIndex, () => wordIndex, () => stringIndex], ([a, b, c,]) => {
|
||||
checkCursorPosition(a, b, c)
|
||||
})
|
||||
@@ -97,18 +95,20 @@ watch(() => settingStore.translate, () => {
|
||||
function checkCursorPosition(a = sectionIndex, b = sentenceIndex, c = wordIndex) {
|
||||
// console.log('checkCursorPosition')
|
||||
_nextTick(() => {
|
||||
let currentWord = jq(`.section:nth(${a}) .sentence:nth(${b}) .word:nth(${c})`)
|
||||
// console.log(a, b, c, currentWord)
|
||||
if (currentWord.length) {
|
||||
let end = currentWord.find('.word-end')
|
||||
// console.log(end)
|
||||
if (end.length) {
|
||||
let articleRect = articleWrapperRef.getBoundingClientRect()
|
||||
// 选中目标元素
|
||||
const currentWord = document.querySelector(`.section:nth-of-type(${a + 1}) .sentence:nth-of-type(${b + 1}) .word:nth-of-type(${c + 1})`);
|
||||
if (currentWord) {
|
||||
// 在 currentWord 内找 .word-end
|
||||
const end = currentWord.querySelector('.word-end');
|
||||
if (end) {
|
||||
// 获取 articleWrapper 的位置
|
||||
const articleRect = articleWrapperRef.getBoundingClientRect();
|
||||
const endRect = end.getBoundingClientRect();
|
||||
// 计算相对位置
|
||||
cursor = {
|
||||
top: end.offset().top - articleRect.top,
|
||||
left: end.offset().left - articleRect.left,
|
||||
}
|
||||
// console.log(cursor)
|
||||
top: endRect.top - articleRect.top,
|
||||
left: endRect.left - articleRect.left,
|
||||
};
|
||||
}
|
||||
}
|
||||
},)
|
||||
@@ -548,7 +548,7 @@ let showQuestions = $ref(false)
|
||||
}
|
||||
|
||||
.hover-show {
|
||||
background: var(--color-main-active);
|
||||
background: var(--color-select-bg);
|
||||
color: white !important;
|
||||
|
||||
.wrote {
|
||||
@@ -654,7 +654,7 @@ let showQuestions = $ref(false)
|
||||
}
|
||||
|
||||
.word-start {
|
||||
color: var(--color-main-active);
|
||||
color: var(--color-select-bg);
|
||||
}
|
||||
|
||||
.wrong {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import TypingArticle from "./TypingArticle.vue";
|
||||
import {Article, ArticleItem, ArticleWord, DisplayStatistics, getDefaultArticle, ShortcutKey, Word} from "@/types.ts";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {cloneDeep} from "@/utils";
|
||||
import Panel from "../../components/Panel.vue";
|
||||
import {onMounted, onUnmounted} from "vue";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
@@ -19,6 +19,7 @@ import ArticleList from "@/pages/pc/components/list/ArticleList.vue";
|
||||
import {useOnKeyboardEventListener} from "@/hooks/event.ts";
|
||||
import TranslateSetting from "@/pages/pc/components/toolbar/TranslateSetting.vue";
|
||||
import {genArticleSectionData, usePlaySentenceAudio} from "@/hooks/article.ts";
|
||||
import {ElProgress} from 'element-plus';
|
||||
|
||||
const store = useBaseStore()
|
||||
const statisticsStore = usePracticeStore()
|
||||
@@ -53,6 +54,7 @@ function next() {
|
||||
}
|
||||
|
||||
function init() {
|
||||
//todo 这个页面,直接访问白屏
|
||||
if (!store.currentBook.articles.length) return
|
||||
articleData.articles = cloneDeep(store.currentBook.articles)
|
||||
getCurrentPractice()
|
||||
@@ -285,56 +287,52 @@ const {playSentenceAudio} = usePlaySentenceAudio()
|
||||
:article="articleData.article"
|
||||
/>
|
||||
|
||||
<Teleport to="body">
|
||||
<div class="panel-wrapper">
|
||||
<Panel v-if="tabIndex === 0">
|
||||
<template v-slot="{active}">
|
||||
<div class="panel-page-item">
|
||||
<div class="list-header">
|
||||
<div class="left">
|
||||
<BaseIcon title="切换词典"
|
||||
icon="carbon:change-catalog"/>
|
||||
<div class="title">
|
||||
{{ store.currentBook.name }}
|
||||
</div>
|
||||
<Tooltip
|
||||
:title="`下一章(${settingStore.shortcutKeyMap[ShortcutKey.NextChapter]})`"
|
||||
v-if="store.currentBook.lastLearnIndex < articleData.articles.length - 1">
|
||||
<IconWrapper>
|
||||
<Icon @click="emitter.emit(EventKey.continueStudy)" icon="octicon:arrow-right-24"/>
|
||||
</IconWrapper>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="right">
|
||||
{{ articleData.articles.length }}篇文章
|
||||
<div class="panel-wrapper">
|
||||
<Panel>
|
||||
<template v-slot="{active}">
|
||||
<div class="panel-page-item pl-4">
|
||||
<div class="list-header">
|
||||
<div class="left">
|
||||
<div class="title">
|
||||
{{ store.currentBook.name }}
|
||||
</div>
|
||||
<Tooltip
|
||||
:title="`下一篇(${settingStore.shortcutKeyMap[ShortcutKey.NextChapter]})`"
|
||||
v-if="store.currentBook.lastLearnIndex < articleData.articles.length - 1">
|
||||
<IconWrapper>
|
||||
<Icon @click="emitter.emit(EventKey.continueStudy)" icon="octicon:arrow-right-24"/>
|
||||
</IconWrapper>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="right">
|
||||
{{ articleData.articles.length }}篇文章
|
||||
</div>
|
||||
|
||||
<ArticleList
|
||||
:isActive="active"
|
||||
:static="false"
|
||||
:show-translate="settingStore.translate"
|
||||
@click="handleChangeChapterIndex"
|
||||
:active-id="articleData.article.id"
|
||||
:list="articleData.articles ">
|
||||
<template v-slot:suffix="{item,index}">
|
||||
<BaseIcon
|
||||
v-if="!isArticleCollect(item)"
|
||||
class="collect"
|
||||
@click="toggleArticleCollect(item)"
|
||||
title="收藏" icon="ph:star"/>
|
||||
<BaseIcon
|
||||
v-else
|
||||
class="fill"
|
||||
@click="toggleArticleCollect(item)"
|
||||
title="取消收藏" icon="ph:star-fill"/>
|
||||
</template>
|
||||
</ArticleList>
|
||||
</div>
|
||||
</template>
|
||||
</Panel>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<ArticleList
|
||||
:isActive="active"
|
||||
:static="false"
|
||||
:show-translate="settingStore.translate"
|
||||
@click="handleChangeChapterIndex"
|
||||
:active-id="articleData.article.id"
|
||||
:list="articleData.articles ">
|
||||
<template v-slot:suffix="{item,index}">
|
||||
<BaseIcon
|
||||
v-if="!isArticleCollect(item)"
|
||||
class="collect"
|
||||
@click="toggleArticleCollect(item)"
|
||||
title="收藏" icon="ph:star"/>
|
||||
<BaseIcon
|
||||
v-else
|
||||
class="fill"
|
||||
@click="toggleArticleCollect(item)"
|
||||
title="取消收藏" icon="ph:star-fill"/>
|
||||
</template>
|
||||
</ArticleList>
|
||||
</div>
|
||||
</template>
|
||||
</Panel>
|
||||
</div>
|
||||
|
||||
<EditSingleArticleModal
|
||||
v-model="showEditArticle"
|
||||
@@ -344,34 +342,32 @@ const {playSentenceAudio} = usePlaySentenceAudio()
|
||||
</div>
|
||||
<div class="footer " :class="!settingStore.showToolbar && 'hide'">
|
||||
<div class="bottom">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<el-progress
|
||||
class="flex-1"
|
||||
:percentage="progress"
|
||||
:stroke-width="8"
|
||||
:show-text="false"/>
|
||||
<div class="stat">
|
||||
<div class="row">
|
||||
<div class="num">{{ speedMinute }}分钟</div>
|
||||
<div class="line"></div>
|
||||
<div class="name">时间</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="num">{{ statisticsStore.total }}</div>
|
||||
<div class="line"></div>
|
||||
<div class="name">单词总数</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="num">{{ format(statisticsStore.inputWordNumber, '', 0) }}</div>
|
||||
<div class="line"></div>
|
||||
<div class="name">输入数</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="num">{{ format(statisticsStore.wrong, '', 0) }}</div>
|
||||
<div class="line"></div>
|
||||
<div class="name">错误数</div>
|
||||
</div>
|
||||
<ElProgress
|
||||
class="flex-1"
|
||||
:percentage="progress"
|
||||
:stroke-width="8"
|
||||
:show-text="false"/>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="stat">
|
||||
<div class="row">
|
||||
<div class="num">{{ speedMinute }}分钟</div>
|
||||
<div class="line"></div>
|
||||
<div class="name">时间</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="num">{{ statisticsStore.total }}</div>
|
||||
<div class="line"></div>
|
||||
<div class="name">单词总数</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="num">{{ format(statisticsStore.inputWordNumber, '', 0) }}</div>
|
||||
<div class="line"></div>
|
||||
<div class="name">输入数</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="num">{{ format(statisticsStore.wrong, '', 0) }}</div>
|
||||
<div class="line"></div>
|
||||
<div class="name">错误数</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center gap-1">
|
||||
@@ -416,7 +412,7 @@ const {playSentenceAudio} = usePlaySentenceAudio()
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<el-progress :percentage="progress"
|
||||
<ElProgress :percentage="progress"
|
||||
:stroke-width="8"
|
||||
:show-text="false"/>
|
||||
</div>
|
||||
@@ -436,8 +432,6 @@ const {playSentenceAudio} = usePlaySentenceAudio()
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
//padding-right: var(--practice-wrapper-padding-right);
|
||||
transform: translateX(var(--practice-wrapper-translateX));
|
||||
}
|
||||
|
||||
.swiper-wrapper {
|
||||
@@ -472,12 +466,12 @@ const {playSentenceAudio} = usePlaySentenceAudio()
|
||||
}
|
||||
|
||||
.panel-wrapper {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: .6rem;
|
||||
position: absolute;
|
||||
left: var(--article-panel-margin-left);
|
||||
//left: 0;
|
||||
top: .8rem;
|
||||
z-index: 1;
|
||||
margin-left: var(--article-panel-margin-left);
|
||||
height: calc(100% - 1.2rem);
|
||||
height: calc(100% - 1.5rem);
|
||||
}
|
||||
|
||||
.footer {
|
||||
@@ -501,8 +495,8 @@ const {playSentenceAudio} = usePlaySentenceAudio()
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-radius: .6rem;
|
||||
background: var(--color-second-bg);
|
||||
padding: .2rem var(--space) .4rem var(--space);
|
||||
background: var(--color-second);
|
||||
padding: .5rem var(--space);
|
||||
z-index: 2;
|
||||
border: 1px solid var(--color-item-border);
|
||||
box-shadow: var(--shadow);
|
||||
@@ -511,7 +505,7 @@ const {playSentenceAudio} = usePlaySentenceAudio()
|
||||
margin-top: .5rem;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
gap: 2rem;
|
||||
gap: var(--stat-gap);
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
@@ -539,7 +533,7 @@ const {playSentenceAudio} = usePlaySentenceAudio()
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.el-progress-bar__inner) {
|
||||
:deep(.ElProgress-bar__inner) {
|
||||
background: var(--color-scrollbar);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {Word} from "@/types.ts";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import {usePlayWordAudio} from "@/hooks/sound.ts";
|
||||
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
item: Word,
|
||||
showTranslate?: boolean
|
||||
showWord?: boolean
|
||||
border?: boolean
|
||||
}>(), {
|
||||
showTranslate: true,
|
||||
showWord: true,
|
||||
border: true
|
||||
})
|
||||
|
||||
const playWordAudio = usePlayWordAudio()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="word-item"
|
||||
:class="{
|
||||
border,
|
||||
}"
|
||||
>
|
||||
<div class="left">
|
||||
<slot name="prefix" :item="item"></slot>
|
||||
<div class="title-wrapper">
|
||||
<div class="item-title">
|
||||
<span class="word" :class="!showWord && 'word-shadow'">{{ item.word }}</span>
|
||||
<span class="phonetic">{{ item.phonetic0 }}</span>
|
||||
<VolumeIcon class="volume" @click="playWordAudio(item.word)"></VolumeIcon>
|
||||
</div>
|
||||
<div class="item-sub-title" v-if="item.trans.length && showTranslate">
|
||||
<div v-for="v in item.trans">{{ (v.pos ? v.pos + '.' : '') + (v.cn) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<slot name="suffix" :item="item"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
.word-item {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: var(--color-item-bg);
|
||||
color: var(--color-font-1);
|
||||
font-size: 1.1rem;
|
||||
border-radius: .5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
transition: all .3s;
|
||||
padding: .6rem;
|
||||
gap: .6rem;
|
||||
border: 1px solid var(--color-item-border);
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
gap: .6rem;
|
||||
|
||||
.title-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .2rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .3rem;
|
||||
transition: all .3s;
|
||||
}
|
||||
|
||||
.volume, .collect, .easy {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-item-hover);
|
||||
|
||||
.volume, .collect, .easy {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--color-item-active);
|
||||
$c: #E6A23C;
|
||||
|
||||
.phonetic, .item-sub-title {
|
||||
color: var(--color-gray) !important;
|
||||
}
|
||||
|
||||
.volume, .collect, .easy, .fill {
|
||||
color: $c;
|
||||
}
|
||||
}
|
||||
|
||||
&.border {
|
||||
&.active {
|
||||
.item-title {
|
||||
border-bottom: 2px solid gray !important;
|
||||
}
|
||||
}
|
||||
|
||||
.item-title {
|
||||
transition: all .3s;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.item-title {
|
||||
border-bottom: 2px solid gray !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
color: var(--color-font-1);
|
||||
|
||||
.word {
|
||||
font-size: 1.2rem;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.phonetic {
|
||||
font-size: .9rem;
|
||||
color: gray;
|
||||
}
|
||||
}
|
||||
|
||||
.item-sub-title {
|
||||
font-size: 1rem;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<template>
|
||||
<div class="flex justify-center">
|
||||
<div class="w-[70vw] 2xl:w-[50vw] page">
|
||||
<div class="page w-[70vw] 2xl:w-[50vw]">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,11 +5,12 @@ import {Sort} from "@/types.ts";
|
||||
import MiniDialog from "@/pages/pc/components/dialog/MiniDialog.vue";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {cloneDeep, debounce, reverse, shuffle} from "lodash-es";
|
||||
import {cloneDeep, debounce, reverse, shuffle} from "@/utils";
|
||||
import Input from "@/pages/pc/components/Input.vue";
|
||||
import PopConfirm from "@/pages/pc/components/PopConfirm.vue";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import {ElCheckbox, ElPagination} from 'element-plus'
|
||||
|
||||
let list = defineModel('list')
|
||||
|
||||
@@ -117,7 +118,7 @@ const s = useSlots()
|
||||
|
||||
defineRender(
|
||||
() => {
|
||||
const d = (item) => <el-checkbox
|
||||
const d = (item) => <ElCheckbox
|
||||
modelValue={selectIds.includes(item.id)}
|
||||
onChange={() => toggleSelect(item)}
|
||||
size="large"/>
|
||||
@@ -133,14 +134,14 @@ defineRender(
|
||||
<Input
|
||||
modelValue={searchKey}
|
||||
onUpdate:modelValue=
|
||||
{debounce(e => searchKey = e)}
|
||||
{debounce(e => searchKey = e)}
|
||||
class="flex-1"/>
|
||||
<BaseButton onClick={() => (showSearchInput = false, searchKey = '')}>取消</BaseButton>
|
||||
</div>
|
||||
) : (
|
||||
<div class="flex justify-between " v-else>
|
||||
<div class="flex gap-2 items-center">
|
||||
<el-checkbox
|
||||
<ElCheckbox
|
||||
disabled={!currentList.length}
|
||||
onClick={() => toggleSelectAll()}
|
||||
modelValue={selectAll}
|
||||
@@ -219,14 +220,14 @@ defineRender(
|
||||
})}
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<el-pagination background
|
||||
currentPage={pageNo}
|
||||
onUpdate:current-page={handlePageNo}
|
||||
pageSize={pageSize}
|
||||
onUpdate:page-size={(e) => pageSize = e}
|
||||
pageSizes={[20, 50, 100, 200]}
|
||||
layout="prev, pager, next"
|
||||
total={list.value.length}/>
|
||||
<ElPagination background
|
||||
currentPage={pageNo}
|
||||
onUpdate:current-page={handlePageNo}
|
||||
pageSize={pageSize}
|
||||
onUpdate:page-size={(e) => pageSize = e}
|
||||
pageSizes={[20, 50, 100, 200]}
|
||||
layout="prev, pager, next"
|
||||
total={list.value.length}/>
|
||||
</div>
|
||||
</>
|
||||
) : <Empty/>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import {Dict} from "@/types.ts";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import {ElProgress,ElCheckbox} from 'element-plus';
|
||||
|
||||
const props = defineProps<{
|
||||
item?: Dict
|
||||
@@ -36,11 +37,11 @@ const studyProgress = $computed(() => {
|
||||
<div>{{ studyProgress }}{{ item?.length }}{{ quantifier }}</div>
|
||||
</div>
|
||||
<div class="absolute bottom-2 left-4 right-4">
|
||||
<el-progress v-if="item?.lastLearnIndex || item.complete" class="mt-1"
|
||||
<ElProgress v-if="item?.lastLearnIndex || item.complete" class="mt-1"
|
||||
:percentage="progress"
|
||||
:show-text="false"></el-progress>
|
||||
:show-text="false"></ElProgress>
|
||||
</div>
|
||||
<el-checkbox v-if="showCheckbox"
|
||||
<ElCheckbox v-if="showCheckbox"
|
||||
:model-value="checked"
|
||||
@click.stop="$emit('check')"
|
||||
class="absolute left-3 bottom-2"/>
|
||||
|
||||
@@ -103,7 +103,7 @@ watch(() => settingStore.load, (n) => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: var(--color-second-bg);
|
||||
background: var(--color-notice-bg);
|
||||
padding: 1.8rem;
|
||||
border-radius: 0.7rem;
|
||||
width: 30rem;
|
||||
@@ -124,7 +124,7 @@ watch(() => settingStore.load, (n) => {
|
||||
}
|
||||
|
||||
.active {
|
||||
color: var(--color-main-active);
|
||||
color: var(--color-select-bg);
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
@@ -148,7 +148,7 @@ watch(() => settingStore.load, (n) => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--color-main-bg);
|
||||
background: var(--color-primary);
|
||||
|
||||
.href {
|
||||
font-size: 0.9rem;
|
||||
@@ -156,7 +156,7 @@ watch(() => settingStore.load, (n) => {
|
||||
}
|
||||
|
||||
.star {
|
||||
color: var(--color-main-active);
|
||||
color: var(--color-select-bg);
|
||||
}
|
||||
|
||||
.right {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {ElInput} from "element-plus";
|
||||
|
||||
import {watchEffect} from "vue";
|
||||
|
||||
@@ -37,7 +38,7 @@ function toggle() {
|
||||
<div
|
||||
v-if="edit"
|
||||
class="edit-text">
|
||||
<el-input
|
||||
<ElInput
|
||||
v-model="editVal"
|
||||
ref="inputRef"
|
||||
autosize
|
||||
|
||||
@@ -18,10 +18,9 @@ $w: 1.4rem;
|
||||
border-radius: .3rem;
|
||||
background: transparent;
|
||||
transition: all .3s;
|
||||
//color: var(--color-main-active);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-primary);
|
||||
background: var(--color-icon-hightlight);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -30,4 +29,4 @@ $w: 1.4rem;
|
||||
height: $w;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -70,7 +70,7 @@ const vFocus = {
|
||||
}
|
||||
|
||||
&.focus {
|
||||
border: 1px solid var(--color-main-active);
|
||||
border: 1px solid var(--color-select-bg);
|
||||
|
||||
:deep(svg) {
|
||||
color: gray;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import {useBaseStore} from "@/stores/base.ts"
|
||||
|
||||
import {computed, onMounted, onUnmounted, provide, watch} from "vue"
|
||||
import {computed, provide, watch} from "vue"
|
||||
import {Dict, DictType, ShortcutKey} from "@/types.ts"
|
||||
import PopConfirm from "@/pages/pc/components/PopConfirm.vue"
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
@@ -9,18 +9,14 @@ import {useSettingStore} from "@/stores/setting.ts";
|
||||
import Close from "@/components/icon/Close.vue";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
import {useArticleOptions, useWordOptions} from "@/hooks/dict.ts";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import Tooltip from "@/pages/pc/components/Tooltip.vue";
|
||||
import IconWrapper from "@/pages/pc/components/IconWrapper.vue";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import {emitter, EventKey, useEvent} from "@/utils/eventBus.ts";
|
||||
import {useRouter} from "vue-router";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {useNav} from "@/utils";
|
||||
import WordList from "@/pages/pc/components/list/WordList.vue";
|
||||
import ArticleList from "@/pages/pc/components/list/ArticleList.vue";
|
||||
import Slide from "@/pages/pc/components/Slide.vue";
|
||||
import {useNav} from "@/utils";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
type?: DictType
|
||||
@@ -219,7 +215,7 @@ $header-height: 3rem;
|
||||
.panel {
|
||||
border-radius: .5rem;
|
||||
width: var(--panel-width);
|
||||
background: var(--color-second-bg);
|
||||
background: var(--color-second);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -257,7 +253,7 @@ $header-height: 3rem;
|
||||
color: gray;
|
||||
|
||||
&.active {
|
||||
color: var(--color-main-active);
|
||||
color: var(--color-select-bg);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,645 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {Icon} from '@iconify/vue';
|
||||
import {ref, watch} from "vue";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {getAudioFileUrl, useChangeAllSound, usePlayAudio, useWatchAllSound} from "@/hooks/sound.ts";
|
||||
import {getShortcutKey, useDisableEventListener, useEventListener} from "@/hooks/event.ts";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {DefaultShortcutKeyMap, ShortcutKey} from "@/types.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {APP_NAME, EXPORT_DATA_KEY, SAVE_DICT_KEY, SAVE_SETTING_KEY, SoundFileOptions} from "@/utils/const.ts";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {saveAs} from "file-saver";
|
||||
import {checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, shakeCommonDict} from "@/utils";
|
||||
import {GITHUB} from "@/config/ENV.ts";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
|
||||
const emit = defineEmits<{
|
||||
toggleDisabledDialogEscKey: [val: boolean]
|
||||
}>()
|
||||
|
||||
const tabIndex = $ref(0)
|
||||
const settingStore = useSettingStore()
|
||||
const store = useBaseStore()
|
||||
//@ts-ignore
|
||||
const gitLastCommitHash = ref(LATEST_COMMIT_HASH);
|
||||
|
||||
useDisableEventListener(() => undefined)
|
||||
useWatchAllSound()
|
||||
|
||||
let editShortcutKey = $ref('')
|
||||
|
||||
const disabledDefaultKeyboardEvent = $computed(() => {
|
||||
return editShortcutKey && tabIndex === 2
|
||||
})
|
||||
|
||||
watch(() => disabledDefaultKeyboardEvent, v => {
|
||||
emit('toggleDisabledDialogEscKey', !!v)
|
||||
})
|
||||
|
||||
useEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if (!disabledDefaultKeyboardEvent) return
|
||||
e.preventDefault()
|
||||
|
||||
let shortcutKey = getShortcutKey(e)
|
||||
// console.log('e', e, e.keyCode, e.ctrlKey, e.altKey, e.shiftKey)
|
||||
// console.log('key', shortcutKey)
|
||||
|
||||
// if (shortcutKey[shortcutKey.length-1] === '+') {
|
||||
// settingStore.shortcutKeyMap[editShortcutKey] = DefaultShortcutKeyMap[editShortcutKey]
|
||||
// return ElMessage.warning('设备失败!')
|
||||
// }
|
||||
|
||||
if (editShortcutKey) {
|
||||
if (shortcutKey === 'Delete') {
|
||||
settingStore.shortcutKeyMap[editShortcutKey] = ''
|
||||
} else {
|
||||
for (const [k, v] of Object.entries(settingStore.shortcutKeyMap)) {
|
||||
if (v === shortcutKey && k !== editShortcutKey) {
|
||||
settingStore.shortcutKeyMap[editShortcutKey] = DefaultShortcutKeyMap[editShortcutKey]
|
||||
return ElMessage.warning('快捷键重复!')
|
||||
}
|
||||
}
|
||||
settingStore.shortcutKeyMap[editShortcutKey] = shortcutKey
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function resetShortcutKeyMap() {
|
||||
editShortcutKey = ''
|
||||
settingStore.shortcutKeyMap = cloneDeep(DefaultShortcutKeyMap)
|
||||
ElMessage.success('恢复成功')
|
||||
}
|
||||
|
||||
function exportData() {
|
||||
let data = {
|
||||
version: EXPORT_DATA_KEY.version,
|
||||
val: {
|
||||
setting: {
|
||||
version: SAVE_SETTING_KEY.version,
|
||||
val: settingStore.$state
|
||||
},
|
||||
dict: {
|
||||
version: SAVE_DICT_KEY.version,
|
||||
val: shakeCommonDict(store.$state)
|
||||
}
|
||||
}
|
||||
}
|
||||
let blob = new Blob([JSON.stringify(data)], {type: "text/plain;charset=utf-8"});
|
||||
saveAs(blob, `${APP_NAME}-User-Data-${dayjs().format('YYYY-MM-DD HH-mm-ss')}.json`);
|
||||
ElMessage.success('导出成功!')
|
||||
}
|
||||
|
||||
function importData(e) {
|
||||
let file = e.target.files[0]
|
||||
if (!file) return
|
||||
// no()
|
||||
let reader = new FileReader();
|
||||
reader.onload = function (v) {
|
||||
let str: any = v.target.result;
|
||||
if (str) {
|
||||
let obj = {
|
||||
version: -1,
|
||||
val: {
|
||||
setting: {},
|
||||
dict: {},
|
||||
}
|
||||
}
|
||||
try {
|
||||
obj = JSON.parse(str)
|
||||
} catch (err) {
|
||||
ElMessage.error('导入失败!')
|
||||
}
|
||||
if (obj.version === EXPORT_DATA_KEY.version) {
|
||||
|
||||
} else {
|
||||
//TODO
|
||||
}
|
||||
let data = obj.val
|
||||
let settingState = checkAndUpgradeSaveSetting(data.setting)
|
||||
settingStore.setState(settingState)
|
||||
let dictState = checkAndUpgradeSaveDict(data.dict)
|
||||
store.init(dictState)
|
||||
ElMessage.success('导入成功!')
|
||||
}
|
||||
}
|
||||
reader.readAsText(file);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="setting">
|
||||
<div class="left">
|
||||
<div class="tabs">
|
||||
<div class="tab" :class="tabIndex === 0 && 'active'" @click="tabIndex = 0">
|
||||
<Icon icon="bx:headphone" width="20" color="#0C8CE9"/>
|
||||
<span>音效设置</span>
|
||||
</div>
|
||||
<div class="tab" :class="tabIndex === 2 && 'active'" @click="tabIndex = 2">
|
||||
<Icon icon="material-symbols:keyboard-outline" width="20" color="#0C8CE9"/>
|
||||
<span>快捷键设置</span>
|
||||
</div>
|
||||
<div class="tab" :class="tabIndex === 1 && 'active'" @click="tabIndex = 1">
|
||||
<Icon icon="icon-park-outline:setting-config" width="20" color="#0C8CE9"/>
|
||||
<span>其他设置</span>
|
||||
</div>
|
||||
<div class="tab" :class="tabIndex === 3 && 'active'" @click="tabIndex = 3">
|
||||
<Icon icon="mdi:database-cog-outline" width="20" color="#0C8CE9"/>
|
||||
<span>数据管理</span>
|
||||
</div>
|
||||
<div class="tab" :class="tabIndex === 4 && 'active'" @click="tabIndex = 4">
|
||||
<Icon icon="mingcute:service-fill" width="20" color="#0C8CE9"/>
|
||||
<span>反馈</span>
|
||||
</div>
|
||||
<div class="tab" :class="tabIndex === 5 && 'active'" @click="tabIndex = 5">
|
||||
<Icon icon="mdi:about-circle-outline" width="20" color="#0C8CE9"/>
|
||||
<span>关于</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="git-log">
|
||||
Build {{ gitLastCommitHash }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div v-if="tabIndex === 0">
|
||||
<div class="row">
|
||||
<label class="main-title">所有音效</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.allSound"
|
||||
@change="useChangeAllSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">单词/句子自动发音</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.wordSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">单词/句子发音口音</label>
|
||||
<div class="wrapper">
|
||||
<el-select v-model="settingStore.wordSoundType"
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option label="美音" value="us"/>
|
||||
<el-option label="英音" value="uk"/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">音量</label>
|
||||
<div class="wrapper">
|
||||
<el-slider v-model="settingStore.wordSoundVolume"/>
|
||||
<span>{{ settingStore.wordSoundVolume }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">倍速</label>
|
||||
<div class="wrapper">
|
||||
<el-slider v-model="settingStore.wordSoundSpeed" :step="0.1" :min="0.5" :max="3"/>
|
||||
<span>{{ settingStore.wordSoundSpeed }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">按键音</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.keyboardSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="item-title">按键音效</label>
|
||||
<div class="wrapper">
|
||||
<el-select v-model="settingStore.keyboardSoundFile"
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in SoundFileOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
<div class="el-option-row">
|
||||
<span>{{ item.label }}</span>
|
||||
<VolumeIcon
|
||||
:time="100"
|
||||
@click="usePlayAudio(getAudioFileUrl(item.value)[0])"/>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">音量</label>
|
||||
<div class="wrapper">
|
||||
<el-slider v-model="settingStore.keyboardSoundVolume"/>
|
||||
<span>{{ settingStore.keyboardSoundVolume }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">效果音(输入错误、完成时的音效)</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.effectSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">音量</label>
|
||||
<div class="wrapper">
|
||||
<el-slider v-model="settingStore.effectSoundVolume"/>
|
||||
<span>{{ settingStore.effectSoundVolume }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tabIndex === 1">
|
||||
<div class="row">
|
||||
<label class="item-title">显示上一个/下一个单词</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.showNearWord"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="desc">
|
||||
开启后,练习中会在上方显示上一个/下一个单词
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">忽略大小写</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.ignoreCase"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="desc">
|
||||
开启后,输入时不区分大小写,如输入“hello”和“Hello”都会被认为是正确的
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">允许默写模式下显示提示</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.allowWordTip"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="desc">
|
||||
开启后,可以通过鼠标 hover 单词或者按 {{ settingStore.shortcutKeyMap[ShortcutKey.ShowWord] }} 显示正确答案
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">字体设置(仅可调整单词练习)</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">外语字体</label>
|
||||
<div class="wrapper">
|
||||
<el-slider
|
||||
:min="10"
|
||||
:max="100"
|
||||
v-model="settingStore.fontSize.wordForeignFontSize"/>
|
||||
<span>{{ settingStore.fontSize.wordForeignFontSize }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">中文字体</label>
|
||||
<div class="wrapper">
|
||||
<el-slider
|
||||
:min="10"
|
||||
:max="100"
|
||||
v-model="settingStore.fontSize.wordTranslateFontSize"/>
|
||||
<span>{{ settingStore.fontSize.wordTranslateFontSize }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">其他设置</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">切换下一个单词时间</label>
|
||||
<div class="wrapper">
|
||||
<el-input-number v-model="settingStore.waitTimeForChangeWord"
|
||||
:min="6"
|
||||
:max="100"
|
||||
type="number"
|
||||
/>
|
||||
<span>毫秒</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="body" v-if="tabIndex === 2">
|
||||
<div class="row">
|
||||
<label class="main-title">功能</label>
|
||||
<div class="wrapper">快捷键(点击可修改)</div>
|
||||
</div>
|
||||
<div class="scroll">
|
||||
<div class="row" v-for="item of Object.entries(settingStore.shortcutKeyMap)">
|
||||
<label class="item-title">{{ $t(item[0]) }}</label>
|
||||
<div class="wrapper" @click="editShortcutKey = item[0]">
|
||||
<div class="set-key" v-if="editShortcutKey === item[0]">
|
||||
<input :value="item[1]?item[1]:'未设置快捷键'" readonly type="text" @blur="editShortcutKey = ''">
|
||||
<span @click.stop="editShortcutKey = ''">直接按键盘进行设置</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="item[1]">{{ item[1] }}</div>
|
||||
<span v-else>未设置快捷键</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row footer">
|
||||
<label class="item-title"></label>
|
||||
<div class="wrapper">
|
||||
<BaseButton @click="resetShortcutKeyMap">恢复默认</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tabIndex === 3">
|
||||
<div class="row">
|
||||
<div class="main-title">数据导出</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">
|
||||
目前用户的所有数据(自定义设置、自定义词典、练习进度等)
|
||||
<b>仅保存在本地</b>
|
||||
。如果您需要在不同的设备、浏览器或者其他非官方部署上使用 {{ APP_NAME }}, 您需要手动进行数据同步和保存。
|
||||
</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<BaseButton @click="exportData">数据导出</BaseButton>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="main-title">数据导入</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">
|
||||
请注意,导入数据将
|
||||
<b style="color: red"> 完全覆盖 </b>
|
||||
当前数据。请谨慎操作。
|
||||
</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="import hvr-grow">
|
||||
<BaseButton>数据导入</BaseButton>
|
||||
<input type="file"
|
||||
accept="application/json"
|
||||
@change="importData">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tabIndex === 4" class="feedback-modal">
|
||||
<div>
|
||||
给我发Email:<a href="mailto:zyronon@163.com">zyronon@163.com</a>
|
||||
</div>
|
||||
<p>or</p>
|
||||
<div class="github">
|
||||
<span>在<a :href="GITHUB" target="_blank">Github</a>上给我提一个
|
||||
<a :href="`${GITHUB}/issues`" target="_blank">Issue</a>
|
||||
</span>
|
||||
<div class="options">
|
||||
<BaseButton>
|
||||
<a :href="`${GITHUB}/issues/new?assignees=&labels=&projects=&template=%E5%8D%95%E8%AF%8D%E9%94%99%E8%AF%AF---word-error.md&title=%E5%8D%95%E8%AF%8D%E9%94%99%E8%AF%AF+%7C+Word+error`"
|
||||
target="_blank">词典错误?</a>
|
||||
</BaseButton>
|
||||
<BaseButton>
|
||||
<a :href="`${GITHUB}/issues/new?assignees=&labels=&projects=&template=问题报告---bug-report-.md&title=问题报告+%7C+Bug+report+`"
|
||||
target="_blank">反馈BUG?</a>
|
||||
</BaseButton>
|
||||
<BaseButton>
|
||||
<a :href="`${GITHUB}/issues/new?assignees=&labels=&projects=&template=功能请求---feature-request.md&title=功能请求+%7C+Feature+request`"
|
||||
target="_blank">功能请求?</a>
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tabIndex === 5" class="about">
|
||||
<h1>Type Words</h1>
|
||||
<p>
|
||||
本项目完全开源!好用请大家多多点Star!
|
||||
</p>
|
||||
<p>
|
||||
GitHub地址:<a href="https://github.com/zyronon/typing-word">https://github.com/zyronon/typing-word</a>
|
||||
</p>
|
||||
<p>
|
||||
反馈:<a href="https://github.com/zyronon/typing-word/issues">https://github.com/zyronon/typing-word/issues</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
|
||||
.setting {
|
||||
width: 40vw;
|
||||
height: 70vh;
|
||||
display: flex;
|
||||
color: var(--color-font-1);
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.tabs {
|
||||
padding: .6rem 1.6rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
//align-items: center;
|
||||
//justify-content: center;
|
||||
gap: .6rem;
|
||||
|
||||
.tab {
|
||||
cursor: pointer;
|
||||
padding: .6rem .9rem;
|
||||
border-radius: .5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .6rem;
|
||||
|
||||
&.active {
|
||||
background: var(--color-item-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.git-log {
|
||||
font-size: .6rem;
|
||||
color: gray;
|
||||
margin-bottom: .3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
background: var(--color-header-bg);
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 0 var(--space);
|
||||
|
||||
.row {
|
||||
min-height: 2.6rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: calc(var(--space) * 5);
|
||||
|
||||
.wrapper {
|
||||
height: 2rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space);
|
||||
|
||||
span {
|
||||
text-align: right;
|
||||
//width: 30rem;
|
||||
font-size: .7rem;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.set-key {
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
width: 9rem;
|
||||
box-sizing: border-box;
|
||||
margin-right: .6rem;
|
||||
height: 1.8rem;
|
||||
outline: none;
|
||||
font-size: 1rem;
|
||||
border: 1px solid gray;
|
||||
border-radius: .2rem;
|
||||
padding: 0 .3rem;
|
||||
background: var(--color-second-bg);
|
||||
color: var(--color-font-1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: .9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.scroll {
|
||||
flex: 1;
|
||||
padding-right: .6rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-bottom: 1.3rem;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-bottom: .6rem;
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
.line {
|
||||
border-bottom: 1px solid #c4c3c3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-option-row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.icon-wrapper {
|
||||
transform: translateX(10rem);
|
||||
}
|
||||
}
|
||||
|
||||
.import {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-modal {
|
||||
//height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: var(--space);
|
||||
//justify-content: center;
|
||||
color: var(--color-font-1);
|
||||
|
||||
p {
|
||||
font-size: 2.4rem;
|
||||
}
|
||||
|
||||
.github {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space);
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.about {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@
|
||||
import {Word} from "@/types.ts";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import {usePlayWordAudio} from "@/hooks/sound.ts";
|
||||
import {ElPopover} from 'element-plus'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
item: Word,
|
||||
@@ -35,7 +36,7 @@ const playWordAudio = usePlayWordAudio()
|
||||
</div>
|
||||
<div class="item-sub-title flex flex-col gap-2" v-if="item.trans.length && showTranslate">
|
||||
<div v-for="v in item.trans">
|
||||
<el-popover
|
||||
<ElPopover
|
||||
v-if="v.cn.length > 30 && showTransPop"
|
||||
width="300"
|
||||
:content="v.pos + ' ' + v.cn"
|
||||
@@ -44,7 +45,7 @@ const playWordAudio = usePlayWordAudio()
|
||||
<template #reference>
|
||||
<span>{{ v.pos + ' ' + v.cn.slice(0, 30) + '...' }}</span>
|
||||
</template>
|
||||
</el-popover>
|
||||
</ElPopover>
|
||||
<span v-else>{{ v.pos + ' ' + v.cn }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -279,7 +279,7 @@ $header-height: 4rem;
|
||||
|
||||
.modal {
|
||||
position: relative;
|
||||
background: var(--color-second-bg);
|
||||
background: var(--color-second);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -59,7 +59,7 @@ watch(() => props.modelValue, (n) => {
|
||||
position: absolute;
|
||||
z-index: 9;
|
||||
width: 12rem;
|
||||
background: var(--color-second-bg);
|
||||
background: var(--color-second);
|
||||
border-radius: .5rem;
|
||||
box-shadow: 0 0 8px 2px var(--color-item-border);
|
||||
padding: .6rem var(--space);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import Input from "@/pages/pc/components/Input.vue";
|
||||
import {cloneDeep, throttle} from "lodash-es";
|
||||
import {cloneDeep, throttle} from "@/utils";
|
||||
import {Article} from "@/types.ts";
|
||||
|
||||
interface IProps {
|
||||
@@ -203,4 +203,4 @@ defineExpose({scrollBottom})
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -4,6 +4,7 @@ import {Word} from "@/types.ts";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import BaseList from "@/pages/pc/components/list/BaseList.vue";
|
||||
import {usePlayWordAudio} from "@/hooks/sound.ts";
|
||||
import {ElPopover} from 'element-plus'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
list: Word[],
|
||||
@@ -53,7 +54,7 @@ defineExpose({scrollToBottom, scrollToItem})
|
||||
</div>
|
||||
<div class="item-sub-title flex flex-col gap-2" v-if="item.trans.length && showTranslate">
|
||||
<div v-for="v in item.trans">
|
||||
<el-popover
|
||||
<ElPopover
|
||||
v-if="v.cn.length > 30"
|
||||
width="300"
|
||||
:content="v.pos + ' ' + v.cn"
|
||||
@@ -62,7 +63,7 @@ defineExpose({scrollToBottom, scrollToItem})
|
||||
<template #reference>
|
||||
<span>{{ v.pos + ' ' + v.cn.slice(0, 30) + '...' }}</span>
|
||||
</template>
|
||||
</el-popover>
|
||||
</ElPopover>
|
||||
<span v-else>{{ v.pos + ' ' + v.cn }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,11 +6,10 @@ import IconWrapper from "@/pages/pc/components/IconWrapper.vue";
|
||||
import Tooltip from "@/pages/pc/components/Tooltip.vue";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {useWindowClick} from "@/hooks/event.ts";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import Dialog from "@/pages/pc/components/dialog/Dialog.vue";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {ShortcutKey} from "@/types.ts";
|
||||
import {ElSwitch, ElRadioGroup,ElRadioButton,ElSelect,ElOption} from 'element-plus'
|
||||
|
||||
const store = useBaseStore()
|
||||
const settingStore = useSettingStore()
|
||||
@@ -59,19 +58,19 @@ function save() {
|
||||
<div class="mini-row">
|
||||
<label class="item-title">显示翻译</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.translate"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
<ElSwitch v-model="settingStore.translate"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mini-row">
|
||||
<label class="item-title">翻译类型</label>
|
||||
<el-radio-group v-model="translateType" size="small">
|
||||
<el-radio-button :value="1">本地翻译</el-radio-button>
|
||||
<el-radio-button :value="0">网络翻译</el-radio-button>
|
||||
</el-radio-group>
|
||||
<ElRadioGroup v-model="translateType" size="small">
|
||||
<ElRadioButton :value="1">本地翻译</ElRadioButton>
|
||||
<ElRadioButton :value="0">网络翻译</ElRadioButton>
|
||||
</ElRadioGroup>
|
||||
</div>
|
||||
<div class="mini-row" v-if="translateType">
|
||||
<label class="item-title">本地翻译</label>
|
||||
@@ -88,14 +87,14 @@ function save() {
|
||||
<div class="mini-row" v-else>
|
||||
<label class="item-title">网络翻译</label>
|
||||
<div class="wrapper">
|
||||
<el-select v-model="networkTranslateEngine" class="m-2" placeholder="Select" size="small">
|
||||
<el-option
|
||||
<ElSelect v-model="networkTranslateEngine" class="m-2" placeholder="Select" size="small">
|
||||
<ElOption
|
||||
v-for="item in TranslateEngine"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</ElSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import {splitEnArticle2} from "@/hooks/article.ts";
|
||||
import test from '../../test/test.vue'
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
|
||||
function test1() {
|
||||
splitEnArticle2(
|
||||
@@ -19,8 +20,8 @@ function test2() {
|
||||
|
||||
<template>
|
||||
<div class="word flex center h-screen ">
|
||||
<El-Button @click="test1">test1</El-Button>
|
||||
<El-Button @click="test2">test2</El-Button>
|
||||
<base-button @click="test1">test1</base-button>
|
||||
<base-button @click="test2">test2</base-button>
|
||||
<test/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -19,10 +19,10 @@ const {toggleTheme} = useTheme()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout">
|
||||
<div class="layout anim">
|
||||
<!-- 第一个aside 占位用-->
|
||||
<div class="aside space" :class="{'expand':settingStore.sideExpand}"></div>
|
||||
<div class="aside fixed" :class="{'expand':settingStore.sideExpand}">
|
||||
<div class="aside anim fixed" :class="{'expand':settingStore.sideExpand}">
|
||||
<div class="top">
|
||||
<Logo v-if="settingStore.sideExpand"/>
|
||||
<div class="row" @click="router.push('/home')">
|
||||
@@ -78,11 +78,11 @@ const {toggleTheme} = useTheme()
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
background: var(--color-background);
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
.aside {
|
||||
background: var(--color-second-bg);
|
||||
background: var(--color-second);
|
||||
height: 100vh;
|
||||
padding: 1rem 1rem;
|
||||
box-sizing: border-box;
|
||||
@@ -91,7 +91,6 @@ const {toggleTheme} = useTheme()
|
||||
justify-content: space-between;
|
||||
box-shadow: rgb(0 0 0 / 3%) 0px 0px 12px 0px;
|
||||
width: 4.5rem;
|
||||
transition: all 0.3s;
|
||||
z-index: 2;
|
||||
|
||||
.row {
|
||||
@@ -100,7 +99,7 @@ const {toggleTheme} = useTheme()
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-primary);
|
||||
background: var(--color-select-bg);
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {DictId, getDefaultDict} from "@/types";
|
||||
import BasePage from "@/pages/pc/components/BasePage.vue";
|
||||
import {computed, onMounted, reactive} from "vue";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {assign, cloneDeep} from "lodash-es";
|
||||
import {assign, cloneDeep} from "@/utils";
|
||||
import {nanoid} from "nanoid";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import BaseTable from "@/pages/pc/components/BaseTable.vue";
|
||||
@@ -18,6 +18,7 @@ import {useRoute, useRouter} from "vue-router";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import EditBook from "@/pages/pc/article/components/EditBook.vue";
|
||||
import {_getDictDataByUrl, _nextTick, convertToWord} from "@/utils";
|
||||
import {ElForm, ElFormItem, ElInput, ElMessage} from "element-plus";
|
||||
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const base = useBaseStore()
|
||||
@@ -261,85 +262,86 @@ defineRender(() => {
|
||||
<div class="common-title">
|
||||
{wordForm.id ? '修改' : '添加'}单词
|
||||
</div>
|
||||
<el-form
|
||||
<ElForm
|
||||
class="flex-1 overflow-auto pr-2"
|
||||
ref={e => wordFormRef = e}
|
||||
rules={wordRules}
|
||||
model={wordForm}
|
||||
label-width="7rem">
|
||||
<el-form-item label="单词" prop="word">
|
||||
<el-input
|
||||
<ElFormItem label="单词" prop="word">
|
||||
<ElInput
|
||||
modelValue={wordForm.word}
|
||||
onUpdate:modelValue={e => wordForm.word = e}
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="英音音标">
|
||||
<el-input
|
||||
</ElFormItem>
|
||||
<ElFormItem label="英音音标">
|
||||
<ElInput
|
||||
modelValue={wordForm.phonetic0}
|
||||
onUpdate:modelValue={e => wordForm.phonetic0 = e}
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="美音音标">
|
||||
<el-input
|
||||
</ElFormItem>
|
||||
<ElFormItem label="美音音标">
|
||||
<ElInput
|
||||
modelValue={wordForm.phonetic1}
|
||||
onUpdate:modelValue={e => wordForm.phonetic1 = e}/>
|
||||
</el-form-item>
|
||||
<el-form-item label="翻译">
|
||||
<el-input
|
||||
</ElFormItem>
|
||||
<ElFormItem label="翻译">
|
||||
<ElInput
|
||||
modelValue={wordForm.trans}
|
||||
onUpdate:modelValue={e => wordForm.trans = e}
|
||||
placeholder="一行一个翻译,前面词性,后面内容(如n.取消);多个翻译请换行"
|
||||
autosize={{minRows: 6, maxRows: 10}}
|
||||
type="textarea"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="例句">
|
||||
<el-input
|
||||
</ElFormItem>
|
||||
<ElFormItem label="例句">
|
||||
<ElInput
|
||||
modelValue={wordForm.sentences}
|
||||
onUpdate:modelValue={e => wordForm.sentences = e}
|
||||
placeholder="一行原文,一行译文;多个请换两行"
|
||||
autosize={{minRows: 6, maxRows: 10}}
|
||||
type="textarea"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="短语">
|
||||
<el-input
|
||||
</ElFormItem>
|
||||
<ElFormItem label="短语">
|
||||
<ElInput
|
||||
modelValue={wordForm.phrases}
|
||||
onUpdate:modelValue={e => wordForm.phrases = e}
|
||||
placeholder="一行原文,一行译文;多个请换两行"
|
||||
autosize={{minRows: 6, maxRows: 10}}
|
||||
type="textarea"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="同义词">
|
||||
<el-input
|
||||
</ElFormItem>
|
||||
<ElFormItem label="同义词">
|
||||
<ElInput
|
||||
modelValue={wordForm.synos}
|
||||
onUpdate:modelValue={e => wordForm.synos = e}
|
||||
placeholder="请参考已有单词格式"
|
||||
autosize={{minRows: 6, maxRows: 20}}
|
||||
type="textarea"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="同根词">
|
||||
<el-input
|
||||
</ElFormItem>
|
||||
<ElFormItem label="同根词">
|
||||
<ElInput
|
||||
modelValue={wordForm.relWords}
|
||||
onUpdate:modelValue={e => wordForm.relWords = e}
|
||||
placeholder="请参考已有单词格式"
|
||||
autosize={{minRows: 6, maxRows: 20}}
|
||||
type="textarea"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="词源">
|
||||
<el-input
|
||||
</ElFormItem>
|
||||
<ElFormItem label="词源">
|
||||
<ElInput
|
||||
modelValue={wordForm.etymology}
|
||||
onUpdate:modelValue={e => wordForm.etymology = e}
|
||||
placeholder="请参考已有单词格式"
|
||||
autosize={{minRows: 6, maxRows: 10}}
|
||||
type="textarea"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<div class="center">
|
||||
<el-button
|
||||
<base-button
|
||||
type="info"
|
||||
onClick={closeWordForm}>关闭
|
||||
</el-button>
|
||||
<el-button type="primary"
|
||||
onClick={onSubmitWord}>保存
|
||||
</el-button>
|
||||
</base-button>
|
||||
<base-button type="primary"
|
||||
onClick={onSubmitWord}>保存
|
||||
</base-button>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
|
||||
@@ -13,7 +13,7 @@ import BackIcon from "@/pages/pc/components/BackIcon.vue";
|
||||
import DictGroup from "@/pages/pc/components/list/DictGroup.vue";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {useRouter} from "vue-router";
|
||||
import {groupBy} from "lodash-es";
|
||||
import {groupBy} from "@/utils";
|
||||
import {dictionaryResources} from "@/assets/dictionary.ts";
|
||||
import {computed} from "vue";
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {getDefaultWord, ShortcutKey, StudyData, Word} from "@/types.ts";
|
||||
import {useOnKeyboardEventListener, useStartKeyboardEventListener} from "@/hooks/event.ts";
|
||||
import useTheme from "@/hooks/theme.ts";
|
||||
import {getCurrentStudyWord, useWordOptions} from "@/hooks/dict.ts";
|
||||
import {cloneDeep, shuffle} from "lodash-es";
|
||||
import {_getDictDataByUrl, cloneDeep, shuffle} from "@/utils";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import Footer from "@/pages/pc/word/components/Footer.vue";
|
||||
@@ -23,7 +23,6 @@ import Empty from "@/components/Empty.vue";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {usePracticeStore} from "@/stores/practice.ts";
|
||||
import {dictionaryResources} from "@/assets/dictionary.ts";
|
||||
import {_getDictDataByUrl} from "@/utils";
|
||||
|
||||
interface IProps {
|
||||
new: Word[],
|
||||
|
||||
@@ -14,7 +14,8 @@ import {getCurrentStudyWord} from "@/hooks/dict.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import Book from "@/pages/pc/components/Book.vue";
|
||||
import PopConfirm from "@/pages/pc/components/PopConfirm.vue";
|
||||
import {ElMessage} from 'element-plus';
|
||||
import {ElMessage, ElProgress, ElSlider} from 'element-plus';
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
|
||||
const store = useBaseStore()
|
||||
const router = useRouter()
|
||||
@@ -172,7 +173,7 @@ const progressTextRight = $computed(() => {
|
||||
<div class="card flex gap-10">
|
||||
<div class="flex-1 flex flex-col gap-2">
|
||||
<div class="flex">
|
||||
<div class="bg-slate-200 px-3 h-14 rounded-md flex items-center">
|
||||
<div class="bg-third px-3 h-14 rounded-md flex items-center">
|
||||
<span class="text-xl font-bold">{{ store.sdict.name || '请选择书籍开始学习' }}</span>
|
||||
<BaseIcon title="切换词典" :icon="store.sdict.name ? 'gg:arrows-exchange' : 'fluent:add-20-filled'"
|
||||
class="ml-4"
|
||||
@@ -184,7 +185,7 @@ const progressTextRight = $computed(() => {
|
||||
<span>{{ progressTextLeft }}</span>
|
||||
<span>{{ progressTextRight }} / {{ store.sdict.words.length }}</span>
|
||||
</div>
|
||||
<el-progress class="mt-1" :percentage="store.currentStudyProgress" :show-text="false"></el-progress>
|
||||
<ElProgress class="mt-1" :percentage="store.currentStudyProgress" :show-text="false"></ElProgress>
|
||||
</div>
|
||||
<div class="text-sm text-align-end">
|
||||
预计完成日期:{{ _getAccomplishDate(store.sdict.words.length, store.sdict.perDayStudyNumber) }}
|
||||
@@ -214,16 +215,17 @@ const progressTextRight = $computed(() => {
|
||||
<div class="flex gap-1 items-center">
|
||||
每日目标
|
||||
<div style="color:#ac6ed1;" @click="setPerDayStudyNumber"
|
||||
class="bg-slate-200 px-2 h-10 flex center text-2xl rounded cursor-pointer">
|
||||
class="bg-third px-2 h-10 flex center text-2xl rounded cursor-pointer">
|
||||
{{ store.sdict.id ? store.sdict.perDayStudyNumber : 0 }}
|
||||
</div>
|
||||
个单词 <span class="color-blue cursor-pointer" @click="setPerDayStudyNumber">更改</span>
|
||||
</div>
|
||||
<div class="rounded-xl bg-slate-800 flex items-center gap-2 py-3 px-5 text-white cursor-pointer"
|
||||
:class="store.sdict.name || 'opacity-70 cursor-not-allowed'" @click="startStudy">
|
||||
<span>开始学习</span>
|
||||
<Icon icon="icons8:right-round" class="text-2xl"/>
|
||||
</div>
|
||||
<BaseButton :disabled="!store.sdict.name" @click="startStudy">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>开始学习</span>
|
||||
<Icon icon="icons8:right-round" class="text-2xl"/>
|
||||
</div>
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -277,8 +279,8 @@ const progressTextRight = $computed(() => {
|
||||
<div class="center text-sm" :style="{ opacity: tempPerDayStudyNumber === 20 ? 1 : 0 }">
|
||||
推荐
|
||||
</div>
|
||||
<el-slider :min="10" :step="10" show-stops :marks="{ 10: '10', 200: '200' }" size="small" class="my-6"
|
||||
:max="200" v-model="tempPerDayStudyNumber"/>
|
||||
<ElSlider :min="10" :step="10" show-stops :marks="{ 10: '10', 200: '200' }" size="small" class="my-6"
|
||||
:max="200" v-model="tempPerDayStudyNumber"/>
|
||||
<div class="flex gap-2 mb-2 mt-10 items-center">
|
||||
<div>预计</div>
|
||||
<span class="text-2xl" style="color:rgb(176,116,211)">{{
|
||||
|
||||
@@ -9,6 +9,7 @@ import {Icon} from "@iconify/vue";
|
||||
import IconWrapper from "@/pages/pc/components/IconWrapper.vue";
|
||||
import Tooltip from "@/pages/pc/components/Tooltip.vue";
|
||||
import TranslateSetting from "@/pages/pc/components/toolbar/TranslateSetting.vue";
|
||||
import {ElProgress} from 'element-plus';
|
||||
|
||||
const statisticsStore = usePracticeStore()
|
||||
const settingStore = useSettingStore()
|
||||
@@ -85,7 +86,7 @@ const progress = $computed(() => {
|
||||
</Tooltip>
|
||||
|
||||
<div class="bottom">
|
||||
<el-progress
|
||||
<ElProgress
|
||||
:percentage="progress"
|
||||
:stroke-width="8"
|
||||
:show-text="false"/>
|
||||
@@ -171,7 +172,7 @@ const progress = $computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<el-progress :percentage="progress"
|
||||
<ElProgress :percentage="progress"
|
||||
:stroke-width="8"
|
||||
:show-text="false"/>
|
||||
</div>
|
||||
@@ -206,7 +207,7 @@ const progress = $computed(() => {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-radius: .6rem;
|
||||
background: var(--color-second-bg);
|
||||
background: var(--color-second);
|
||||
padding: .2rem var(--space) .4rem var(--space);
|
||||
z-index: 2;
|
||||
border: 1px solid var(--color-item-border);
|
||||
|
||||
@@ -42,6 +42,6 @@ function escapeRegExp(string: string): string {
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.highlight-word) {
|
||||
color: var(--color-primary);
|
||||
color: var(--color-icon-hightlight);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -323,7 +323,7 @@ function mouseleave() {
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
border-bottom: 2px solid var(--color-font-1);
|
||||
border-bottom: 2px solid var(--color-font-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
// import origin from './data.json'
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {checkAndUpgradeSaveDict, shakeCommonDict} from "@/utils";
|
||||
import {checkAndUpgradeSaveDict} from "@/utils";
|
||||
import localforage from "localforage";
|
||||
import {SAVE_DICT_KEY} from "@/utils/const.ts";
|
||||
import str from './data.json'
|
||||
@@ -73,13 +73,6 @@ const data1 = generateData(columns, 1000)
|
||||
<BaseButton @click="set">设置data.json的数据到localforage</BaseButton>
|
||||
<BaseButton @click="check">检测升级逻辑</BaseButton>
|
||||
</div>
|
||||
<el-table-v2
|
||||
:columns="columns"
|
||||
:data="data1"
|
||||
:width="700"
|
||||
:height="400"
|
||||
fixed
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -52,7 +52,8 @@ export const routes: RouteRecordRaw[] = [
|
||||
]
|
||||
|
||||
const router = VueRouter.createRouter({
|
||||
history: VueRouter.createWebHistory(),
|
||||
// history: VueRouter.createWebHistory(),
|
||||
history: VueRouter.createWebHashHistory(),
|
||||
routes,
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
// console.log('savedPosition', savedPosition)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import {Dict, DictId, getDefaultDict, Word} from "../types.ts"
|
||||
import {cloneDeep, merge} from "lodash-es";
|
||||
import {cloneDeep} from "@/utils";
|
||||
import * as localforage from "localforage";
|
||||
import {nanoid} from "nanoid";
|
||||
import {SAVE_DICT_KEY} from "@/utils/const.ts";
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import {defineStore} from "pinia"
|
||||
import {cloneDeep, merge} from "lodash-es";
|
||||
import {checkAndUpgradeSaveSetting, cloneDeep} from "@/utils";
|
||||
import {DefaultShortcutKeyMap} from "@/types.ts";
|
||||
import {SAVE_SETTING_KEY} from "@/utils/const.ts";
|
||||
import {checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting} from "@/utils";
|
||||
|
||||
export interface SettingState {
|
||||
showToolbar: boolean,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Dict, DictResource, getDefaultDict} from "@/types.ts";
|
||||
import {getDictFile} from "@/utils/index.ts";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {cloneDeep} from "@/utils";
|
||||
import {nanoid} from "nanoid";
|
||||
|
||||
export async function getArticleBookDataByUrl(val: DictResource) {
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import axios, {AxiosInstance} from 'axios'
|
||||
// import globalMethods from './global-methods'
|
||||
// import Config from '../config/index'
|
||||
// import CONSTANT from './const_var'
|
||||
// import store from '../store'
|
||||
// import Storage from './storage'
|
||||
|
||||
export const axiosInstance: AxiosInstance = axios.create({
|
||||
// baseURL: process.env.NODE_ENV === 'production' ? Config.PRODUCT_API_URL : Config.API_URL,
|
||||
// baseURL: 'http://testtestgp.com',
|
||||
timeout: 15000,
|
||||
})
|
||||
|
||||
// request 拦截器
|
||||
axiosInstance.interceptors.request.use(
|
||||
(config) => {
|
||||
// console.log('config', config)
|
||||
if (config.url === 'https://api.fanyi.baidu.com/api/trans/vip/translate') {
|
||||
config.url = '/baidu'
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => Promise.reject(error),
|
||||
)
|
||||
// import axios, {AxiosInstance} from 'axios'
|
||||
// // import globalMethods from './global-methods'
|
||||
// // import Config from '../config/index'
|
||||
// // import CONSTANT from './const_var'
|
||||
// // import store from '../store'
|
||||
// // import Storage from './storage'
|
||||
//
|
||||
// export const axiosInstance: AxiosInstance = axios.create({
|
||||
// // baseURL: process.env.NODE_ENV === 'production' ? Config.PRODUCT_API_URL : Config.API_URL,
|
||||
// // baseURL: 'http://testtestgp.com',
|
||||
// timeout: 15000,
|
||||
// })
|
||||
//
|
||||
// // request 拦截器
|
||||
// axiosInstance.interceptors.request.use(
|
||||
// (config) => {
|
||||
// // console.log('config', config)
|
||||
// if (config.url === 'https://api.fanyi.baidu.com/api/trans/vip/translate') {
|
||||
// config.url = '/baidu'
|
||||
// }
|
||||
// return config
|
||||
// },
|
||||
// error => Promise.reject(error),
|
||||
// )
|
||||
|
||||
// respone 拦截器
|
||||
// instance.interceptors.response.use(
|
||||
@@ -111,4 +111,4 @@ axiosInstance.interceptors.request.use(
|
||||
// })
|
||||
// }
|
||||
|
||||
// export default request
|
||||
// export default request
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import {SAVE_DICT_KEY, SAVE_SETTING_KEY} from "@/utils/const.ts";
|
||||
import {BaseState, DefaultBaseState} from "@/stores/base.ts";
|
||||
import {getDefaultSettingState, SettingState} from "@/stores/setting.ts";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {getDefaultSettingState} from "@/stores/setting.ts";
|
||||
import {Dict, DictResource, DictType, getDefaultArticle, getDefaultDict, getDefaultWord} from "@/types.ts";
|
||||
import {ArchiveReader, libarchiveWasm} from "libarchive-wasm";
|
||||
import {useRouter} from "vue-router";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {nanoid} from "nanoid";
|
||||
@@ -514,3 +512,57 @@ export function convertToWord(raw: any) {
|
||||
custom: true
|
||||
});
|
||||
}
|
||||
|
||||
export function cloneDeep<T>(val: T) {
|
||||
return JSON.parse(JSON.stringify(val))
|
||||
}
|
||||
|
||||
export function shuffle<T>(array: T[]): T[] {
|
||||
const result = array.slice(); // 复制数组,避免修改原数组
|
||||
for (let i = result.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1)); // 生成 0 ~ i 的随机索引
|
||||
[result[i], result[j]] = [result[j], result[i]]; // 交换元素
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function last<T>(array: T[]): T | undefined {
|
||||
return array.length > 0 ? array[array.length - 1] : undefined;
|
||||
}
|
||||
|
||||
export function debounce<T extends (...args: any[]) => void>(func: T, wait: number) {
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
func.apply(this, args);
|
||||
}, wait);
|
||||
};
|
||||
}
|
||||
|
||||
export function throttle<T extends (...args: any[]) => void>(func: T, wait: number) {
|
||||
let lastTime = 0;
|
||||
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
|
||||
const now = Date.now();
|
||||
if (now - lastTime >= wait) {
|
||||
func.apply(this, args);
|
||||
lastTime = now;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function reverse<T>(array: T[]): T[] {
|
||||
return array.slice().reverse();
|
||||
}
|
||||
|
||||
export function assign<T extends object, U extends object>(target: T, ...sources: U[]): T & U {
|
||||
return Object.assign(target, ...sources);
|
||||
}
|
||||
|
||||
export function groupBy<T extends Record<string, any>>(array: T[], key: string) {
|
||||
return array.reduce<Record<string, T[]>>((result, item) => {
|
||||
const groupKey = String(item[key]);
|
||||
(result[groupKey] ||= []).push(item);
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
// uno.config.ts
|
||||
import {defineConfig, presetUno} from 'unocss'
|
||||
import {defineConfig, presetWind3} from 'unocss'
|
||||
|
||||
export default defineConfig({
|
||||
content: {
|
||||
|
||||
pipeline: {
|
||||
include: [
|
||||
'./src/**/*.{html,vue,ts,js}',
|
||||
'./index.html',
|
||||
],
|
||||
exclude: [
|
||||
'./node_modules/**/*',
|
||||
'./dist/**/*',
|
||||
'./.pnpm/**/*',
|
||||
'./.output/**/*',
|
||||
],
|
||||
},
|
||||
shortcuts: {
|
||||
'bg-primary': 'bg-[var(--color-primary)]',
|
||||
'bg-second': 'bg-[var(--color-second)]',
|
||||
'bg-third': 'bg-[var(--color-third)]',
|
||||
'bg-card-active': 'bg-[var(--color-card-active)]',
|
||||
'color-main': 'color-[var(--color-main-text)]',
|
||||
'gap-space': 'gap-[var(--space)]',
|
||||
},
|
||||
presets: [
|
||||
presetUno(),
|
||||
presetWind3(),
|
||||
],
|
||||
})
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import {defineConfig} from 'vite'
|
||||
import {defineConfig, UserConfig} from 'vite'
|
||||
import Vue from '@vitejs/plugin-vue'
|
||||
import VueJsx from "@vitejs/plugin-vue-jsx";
|
||||
import {resolve} from 'path'
|
||||
import {visualizer} from "rollup-plugin-visualizer";
|
||||
import SlidePlugin from './src/components/slide/data.js';
|
||||
import {ElementPlusResolver} from "unplugin-vue-components/resolvers";
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import {getLastCommit} from "git-last-commit";
|
||||
import UnoCSS from 'unocss/vite'
|
||||
import VueMacros from 'unplugin-vue-macros/vite'
|
||||
import {Plugin as importToCDN} from 'vite-plugin-cdn-import'
|
||||
import ElementPlus from 'unplugin-element-plus/vite'
|
||||
|
||||
function pathResolve(dir: string) {
|
||||
return resolve(__dirname, ".", dir)
|
||||
@@ -18,7 +16,7 @@ function pathResolve(dir: string) {
|
||||
|
||||
const lifecycle = process.env.npm_lifecycle_event;
|
||||
|
||||
async function s() {
|
||||
async function getConfig(): Promise<Partial<UserConfig>> {
|
||||
const latestCommitHash = await new Promise<string>((resolve) => {
|
||||
return getLastCommit((err, commit) => (err ? 'unknown' : resolve(commit.shortHash)))
|
||||
})
|
||||
@@ -31,12 +29,7 @@ async function s() {
|
||||
},
|
||||
}),
|
||||
UnoCSS(),
|
||||
AutoImport({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
Components({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
ElementPlus(),
|
||||
lifecycle === 'report' ?
|
||||
visualizer({
|
||||
gzipSize: true,
|
||||
@@ -59,15 +52,10 @@ async function s() {
|
||||
path: `https://cdn.jsdelivr.net/npm/vue-router@4.5.1/dist/vue-router.global.prod.min.js`
|
||||
},
|
||||
{
|
||||
name: 'jquery',
|
||||
var: 'jQuery',
|
||||
path: 'https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js'
|
||||
name: 'axios',
|
||||
var: 'axios',
|
||||
path: 'https://cdn.jsdelivr.net/npm/axios@1.9.0/dist/axios.min.js'
|
||||
},
|
||||
// {
|
||||
// name: 'axios',
|
||||
// var: 'axios',
|
||||
// path: 'https://cdn.jsdelivr.net/npm/axios@1.9.0/dist/axios.min.js'
|
||||
// },
|
||||
]
|
||||
})
|
||||
],
|
||||
@@ -82,6 +70,11 @@ async function s() {
|
||||
},
|
||||
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
|
||||
},
|
||||
// build: {
|
||||
// rollupOptions: {
|
||||
// external: ['axios'],// 使用全局的 axios。因为百度翻译库内部用了0.19版本的axios,会被打包到代码里面
|
||||
// }
|
||||
// },
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
@@ -105,4 +98,4 @@ async function s() {
|
||||
}
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(s as any)
|
||||
export default defineConfig(getConfig as any)
|
||||
|
||||
Reference in New Issue
Block a user