This commit is contained in:
50
src/lib/controllers/FolderController.ts
Normal file
50
src/lib/controllers/FolderController.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
"use server";
|
||||
|
||||
import { pool } from "../db";
|
||||
|
||||
export async function deleteFolderById(id: number) {
|
||||
try {
|
||||
await pool.query("DELETE FROM folders WHERE id = $1", [id]);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFoldersByOwner(owner: string) {
|
||||
try {
|
||||
const folders = await pool.query("SELECT * FROM folders WHERE owner = $1", [
|
||||
owner,
|
||||
]);
|
||||
return folders.rows;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFoldersWithTextPairsCountByOwner(owner: string) {
|
||||
try {
|
||||
const folders = await pool.query(
|
||||
`select f.id, f.name, f.owner, count(tp.id) as text_pairs_count from folders f
|
||||
left join text_pairs tp on tp.folder_id = f.id
|
||||
where f.owner = $1
|
||||
group by f.id, f.name, f.owner`,
|
||||
[owner],
|
||||
);
|
||||
return folders.rows;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createFolder(name: string, owner: string) {
|
||||
try {
|
||||
return (
|
||||
await pool.query("INSERT INTO folders (name, owner) VALUES ($1, $2)", [
|
||||
name.trim(),
|
||||
owner,
|
||||
])
|
||||
).rows[0];
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
69
src/lib/controllers/TextPairController.ts
Normal file
69
src/lib/controllers/TextPairController.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
"use server";
|
||||
|
||||
import { pool } from "../db";
|
||||
|
||||
export async function createTextPair(
|
||||
locale1: string,
|
||||
locale2: string,
|
||||
text1: string,
|
||||
text2: string,
|
||||
folderId: number,
|
||||
) {
|
||||
try {
|
||||
await pool.query(
|
||||
"INSERT INTO text_pairs (locale1, locale2, text1, text2, folder_id) VALUES ($1, $2, $3, $4, $5)",
|
||||
[locale1.trim(), locale2.trim(), text1.trim(), text2.trim(), folderId],
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteTextPairById(id: number) {
|
||||
try {
|
||||
await pool.query("DELETE FROM text_pairs WHERE id = $1", [id]);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateWordPairById(
|
||||
id: number,
|
||||
locale1: string,
|
||||
locale2: string,
|
||||
text1: string,
|
||||
text2: string,
|
||||
) {
|
||||
try {
|
||||
await pool.query(
|
||||
"UPDATE text_pairs SET locale1 = $1, locale2 = $2, text1 = $3, text2 = $4 WHERE id = $5",
|
||||
[locale1.trim(), locale2.trim(), text1.trim(), text2.trim(), id],
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTextPairsByFolderId(folderId: number) {
|
||||
try {
|
||||
const textPairs = await pool.query(
|
||||
"SELECT * FROM text_pairs WHERE folder_id = $1",
|
||||
[folderId],
|
||||
);
|
||||
return textPairs.rows;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTextPairsCountByFolderId(folderId: number) {
|
||||
try {
|
||||
const count = await pool.query(
|
||||
"SELECT COUNT(*) FROM text_pairs WHERE folder_id = $1",
|
||||
[folderId],
|
||||
);
|
||||
return count.rows[0].count;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
139
src/lib/db.ts
139
src/lib/db.ts
@@ -1,4 +1,3 @@
|
||||
import bcrypt from "bcryptjs";
|
||||
import { Pool } from "pg";
|
||||
|
||||
export const pool = new Pool({
|
||||
@@ -9,141 +8,3 @@ export const pool = new Pool({
|
||||
connectionTimeoutMillis: 2000,
|
||||
maxLifetimeSeconds: 60,
|
||||
});
|
||||
|
||||
export class UserController {
|
||||
static async createUser(username: string, password: string) {
|
||||
const encodedPassword = await bcrypt.hash(password, 10);
|
||||
try {
|
||||
await pool.query(
|
||||
"INSERT INTO users (username, password) VALUES ($1, $2)",
|
||||
[username, encodedPassword],
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
static async getUserByUsername(username: string) {
|
||||
try {
|
||||
const user = await pool.query("SELECT * FROM users WHERE username = $1", [
|
||||
username,
|
||||
]);
|
||||
return user.rows[0];
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
static async deleteUserById(id: number) {
|
||||
try {
|
||||
await pool.query("DELETE FROM users WHERE id = $1", [id]);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class FolderController {
|
||||
static async getFolderById(id: number) {
|
||||
try {
|
||||
const folder = await pool.query("SELECT * FROM folders WHERE id = $1", [
|
||||
id,
|
||||
]);
|
||||
return folder.rows[0];
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
static async deleteFolderById(id: number) {
|
||||
try {
|
||||
await pool.query("DELETE FROM folders WHERE id = $1", [id]);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
static async getFoldersByOwner(owner: string) {
|
||||
try {
|
||||
const folders = await pool.query(
|
||||
"SELECT * FROM folders WHERE owner = $1",
|
||||
[owner],
|
||||
);
|
||||
return folders.rows;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
static async createFolder(name: string, owner: string) {
|
||||
try {
|
||||
return (
|
||||
await pool.query("INSERT INTO folders (name, owner) VALUES ($1, $2)", [
|
||||
name,
|
||||
owner,
|
||||
])
|
||||
).rows[0];
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class WordPairController {
|
||||
static async createWordPair(
|
||||
locale1: string,
|
||||
locale2: string,
|
||||
text1: string,
|
||||
text2: string,
|
||||
folderId: number,
|
||||
) {
|
||||
try {
|
||||
await pool.query(
|
||||
"INSERT INTO word_pairs (locale1, locale2, text1, text2, folder_id) VALUES ($1, $2, $3, $4, $5)",
|
||||
[locale1, locale2, text1, text2, folderId],
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
static async getWordPairById(id: number) {
|
||||
try {
|
||||
const wordPair = await pool.query(
|
||||
"SELECT * FROM word_pairs WHERE id = $1",
|
||||
[id],
|
||||
);
|
||||
return wordPair.rows[0];
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
static async deleteWordPairById(id: number) {
|
||||
try {
|
||||
await pool.query("DELETE FROM word_pairs WHERE id = $1", [id]);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
static async updateWordPairById(
|
||||
id: number,
|
||||
locale1: string,
|
||||
locale2: string,
|
||||
text1: string,
|
||||
text2: string,
|
||||
) {
|
||||
try {
|
||||
await pool.query(
|
||||
"UPDATE word_pairs SET locale1 = $1, locale2 = $2, text1 = $3, text2 = $4 WHERE id = $5",
|
||||
[locale1, locale2, text1, text2, id],
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
static async getWordPairsByFolderId(folderId: number) {
|
||||
try {
|
||||
const wordPairs = await pool.query(
|
||||
"SELECT * FROM word_pairs WHERE folder_id = $1",
|
||||
[folderId],
|
||||
);
|
||||
return wordPairs.rows;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
56
src/lib/interfaces.ts
Normal file
56
src/lib/interfaces.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import z from "zod";
|
||||
|
||||
export interface Word {
|
||||
word: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
export interface Letter {
|
||||
letter: string;
|
||||
letter_name_ipa: string;
|
||||
letter_sound_ipa: string;
|
||||
roman_letter?: string;
|
||||
}
|
||||
export type SupportedAlphabets =
|
||||
| "japanese"
|
||||
| "english"
|
||||
| "esperanto"
|
||||
| "uyghur";
|
||||
export const TextSpeakerItemSchema = z.object({
|
||||
text: z.string(),
|
||||
ipa: z.string().optional(),
|
||||
locale: z.string(),
|
||||
});
|
||||
export const TextSpeakerArraySchema = z.array(TextSpeakerItemSchema);
|
||||
|
||||
export const WordDataSchema = z.object({
|
||||
locales: z
|
||||
.tuple([z.string(), z.string()])
|
||||
.refine(([first, second]) => first !== second, {
|
||||
message: "Locales must be different",
|
||||
}),
|
||||
wordPairs: z
|
||||
.array(z.tuple([z.string(), z.string()]))
|
||||
.min(1, "At least one word pair is required")
|
||||
.refine(
|
||||
(pairs) => {
|
||||
return pairs.every(
|
||||
([first, second]) => first.trim() !== "" && second.trim() !== "",
|
||||
);
|
||||
},
|
||||
{
|
||||
message: "Word pairs cannot contain empty strings",
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
export const TranslationHistorySchema = z.object({
|
||||
text1: z.string(),
|
||||
text2: z.string(),
|
||||
locale1: z.string(),
|
||||
locale2: z.string(),
|
||||
});
|
||||
|
||||
export const TranslationHistoryArraySchema = z.array(TranslationHistorySchema);
|
||||
|
||||
export type WordData = z.infer<typeof WordDataSchema>;
|
||||
126
src/lib/utils.ts
Normal file
126
src/lib/utils.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { EdgeTTS, ProsodyOptions } from "edge-tts-universal/browser";
|
||||
import { env } from "process";
|
||||
import z from "zod";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export function inspect(word: string) {
|
||||
const goto = (url: string) => {
|
||||
window.open(url, "_blank");
|
||||
};
|
||||
return () => {
|
||||
word = word.toLowerCase();
|
||||
goto(`https://www.youdao.com/result?word=${word}&lang=en`);
|
||||
};
|
||||
}
|
||||
|
||||
export function urlGoto(url: string) {
|
||||
window.open(url, "_blank");
|
||||
}
|
||||
const API_KEY = env.ZHIPU_API_KEY;
|
||||
export async function callZhipuAPI(
|
||||
messages: { role: string; content: string }[],
|
||||
model = "glm-4.6",
|
||||
) {
|
||||
const url = "https://open.bigmodel.cn/api/paas/v4/chat/completions";
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer " + API_KEY,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
messages: messages,
|
||||
temperature: 0.2,
|
||||
thinking: {
|
||||
type: "disabled",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API 调用失败: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function getTTSAudioUrl(
|
||||
text: string,
|
||||
short_name: string,
|
||||
options: ProsodyOptions | undefined = undefined,
|
||||
) {
|
||||
const tts = new EdgeTTS(text, short_name, options);
|
||||
try {
|
||||
const result = await tts.synthesize();
|
||||
return URL.createObjectURL(result.audio);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export const getLocalStorageOperator = <T extends z.ZodTypeAny>(
|
||||
key: string,
|
||||
schema: T,
|
||||
) => {
|
||||
return {
|
||||
get: (): z.infer<T> => {
|
||||
try {
|
||||
if (!localStorage) return [];
|
||||
const item = localStorage.getItem(key);
|
||||
|
||||
if (!item) return [];
|
||||
|
||||
const rawData = JSON.parse(item) as z.infer<T>;
|
||||
const result = schema.safeParse(rawData);
|
||||
|
||||
if (result.success) {
|
||||
return result.data;
|
||||
} else {
|
||||
console.error(
|
||||
"Invalid data structure in localStorage:",
|
||||
result.error,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed to parse ${key} data:`, e);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
set: (data: z.infer<T>) => {
|
||||
if (!localStorage) return;
|
||||
localStorage.setItem(key, JSON.stringify(data));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export function handleAPIError(error: unknown, message: string) {
|
||||
console.error(message, error);
|
||||
return NextResponse.json(
|
||||
{ error: "服务器内部错误", message },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export const letsFetch = (
|
||||
url: string,
|
||||
onSuccess: (message: string) => void,
|
||||
onError: (message: string) => void,
|
||||
onFinally: () => void,
|
||||
) => {
|
||||
return fetch(url)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.status === "success") {
|
||||
onSuccess(data.message);
|
||||
} else if (data.status === "error") {
|
||||
onError(data.message);
|
||||
} else {
|
||||
onError("Unknown error");
|
||||
}
|
||||
})
|
||||
.finally(onFinally);
|
||||
};
|
||||
Reference in New Issue
Block a user