4 Commits

Author SHA1 Message Date
zyronon
3f581d7aa5 feat:移除无用依赖 2025-08-03 22:13:45 +08:00
zyronon
c372a18ca0 feat:移除无用依赖 2025-08-03 22:02:00 +08:00
zyronon
6d9fbf234f feat:修复主题 2025-08-03 16:32:20 +08:00
zyronon
102f5fbc1f feat:save 2025-08-03 13:49:03 +08:00
91 changed files with 793 additions and 6831 deletions

10
auto-imports.d.ts vendored
View File

@@ -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
View File

@@ -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']
}
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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
View File

@@ -1,4 +1,4 @@
import {cloneDeep} from "lodash-es"
import {cloneDeep} from "@/utils"
export {}

View File

@@ -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";

View File

@@ -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() {

View File

@@ -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"

View File

@@ -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) => {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>-->

View File

@@ -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>

View File

@@ -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);
}
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">
开启后输入时不区分大小写如输入helloHello都会被认为是正确的
</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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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%;

View File

@@ -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 {

View File

@@ -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>

View File

@@ -45,6 +45,6 @@ useDisableEventListener(() => props.modelValue)
width: 100%;
height: 100%;
display: flex;
background: var(--color-main-bg);
background: var(--color-primary);
}
</style>

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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/>

View File

@@ -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"/>

View File

@@ -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 {

View File

@@ -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

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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">
开启后输入时不区分大小写如输入helloHello都会被认为是正确的
</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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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);

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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";

View File

@@ -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[],

View File

@@ -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)">{{

View File

@@ -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);

View File

@@ -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>

View File

@@ -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);
}
}
}

View File

@@ -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>

View File

@@ -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)

View File

@@ -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";

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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

View File

@@ -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;
}, {});
}

View File

@@ -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(),
],
})

View File

@@ -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)