This commit is contained in:
2025-11-24 18:58:32 +08:00
parent 8b9fdb706f
commit a2cee7c27a
28 changed files with 931 additions and 81 deletions

21
backend/bun.config.mjs Normal file
View File

@@ -0,0 +1,21 @@
// @ts-check
import { resolvePath } from '@kevisual/use-config';
import { execSync } from 'node:child_process';
const entry = 'src/index.ts';
const naming = 'app';
const external = ['pm2'];
/**
* @type {import('bun').BuildConfig}
*/
await Bun.build({
target: 'node',
format: 'esm',
entrypoints: [resolvePath(entry, { meta: import.meta })],
outdir: resolvePath('./dist', { meta: import.meta }),
naming: {
entry: `${naming}.js`,
},
external,
env: 'KEVISUAL_*',
});

View File

@@ -3,12 +3,27 @@
"version": "0.0.1", "version": "0.0.1",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"basename": "/root/daily-question-server",
"app": {
"type": "system-app",
"key": "daily-question-server",
"entry": "app.js",
"runtime": [
"server"
]
},
"files": [
"dist"
],
"scripts": { "scripts": {
"dev": "bun --watch src/main.ts ", "dev": "bun --watch src/main.ts ",
"build": "rimraf dist && bun run bun.config.mjs", "build": "rimraf dist && rimraf pack-dist && bun run bun.config.mjs",
"compile": "bun build --compile ./src/main.ts --outfile myapp", "postbuild": "ev pack",
"compile:win": "bun build --compile ./src/main.ts --target=bun-windows-x64 --outfile myapp.exe", "compile": "bun build --compile ./src/main.ts --outfile daily-question",
"compile:win": "bun build --compile ./src/main.ts --target=bun-windows-x64 --outfile daily-question.exe",
"postcompile:win": "bun zip.ts",
"clean": "rm -rf dist", "clean": "rm -rf dist",
"sc": "rimraf *.zip && rimraf *.exe",
"pub": "envision pack -p -u" "pub": "envision pack -p -u"
}, },
"keywords": [], "keywords": [],
@@ -18,7 +33,7 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@kevisual/ai": "^0.0.12", "@kevisual/ai": "^0.0.12",
"@kevisual/local-proxy": "^0.0.6", "@kevisual/local-proxy": "^0.0.8",
"@kevisual/query": "^0.0.29", "@kevisual/query": "^0.0.29",
"@kevisual/router": "0.0.33", "@kevisual/router": "0.0.33",
"@kevisual/use-config": "^1.0.21", "@kevisual/use-config": "^1.0.21",
@@ -34,6 +49,7 @@
"@types/better-sqlite3": "^7.6.13", "@types/better-sqlite3": "^7.6.13",
"@types/bun": "^1.3.3", "@types/bun": "^1.3.3",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"archiver": "^7.0.1",
"drizzle-kit": "^0.31.7" "drizzle-kit": "^0.31.7"
} }
} }

View File

@@ -1,4 +1,8 @@
## 后端功能 ## 使用方法
运行exe后浏览器访问 http://localhost:9000/
## 后端功能提示词
使用dbbetter-sqite3 使用dbbetter-sqite3

View File

@@ -1,6 +1,6 @@
import { App } from "@kevisual/router"; import { App } from "@kevisual/router";
import { useContextKey } from "@kevisual/use-config/context"; import { useContextKey } from "@kevisual/use-config/context";
export const app = useContextKey('app', new App({ export const app: App = useContextKey<App>('app', new App({
serverOptions: { serverOptions: {
path: '/client/router' path: '/client/router'
} }

4
backend/src/index.ts Normal file
View File

@@ -0,0 +1,4 @@
import { app } from './app.ts'
import './router/index.ts';
export { app }

View File

@@ -1,16 +1,15 @@
import { app } from './app.ts' import { app } from './app.ts'
import './router/daily/index.ts';
import './router/library/index.ts';
import './router/index.ts'; import './router/index.ts';
import { proxyRoute, initProxy } from '@kevisual/local-proxy/proxy.ts'; import { proxyRoute, initProxy } from '@kevisual/local-proxy/proxy.ts';
initProxy({
pagesDir: './public',
watch: true,
home: '/root/daily-question'
});
export { app } export { app }
if (require.main === module) { if (require.main === module) {
initProxy({
pagesDir: './public',
watch: true,
home: '/root/daily-question'
});
app.listen(9000, () => { app.listen(9000, () => {
console.log('Server is running on http://localhost:9000'); console.log('Server is running on http://localhost:9000');
}); });

View File

@@ -23,7 +23,6 @@ export const getDb = () => {
export const init = async () => { export const init = async () => {
console.log('Database path:', dbPath); console.log('Database path:', dbPath);
const database = getDb(); const database = getDb();
// 使用 Drizzle ORM 创建表 (如果不存在)
if (sqliteDb) { if (sqliteDb) {
sqliteDb.run(` sqliteDb.run(`
CREATE TABLE IF NOT EXISTS question_library ( CREATE TABLE IF NOT EXISTS question_library (
@@ -34,6 +33,8 @@ export const init = async () => {
repeat INTEGER DEFAULT 0, repeat INTEGER DEFAULT 0,
isUse INTEGER DEFAULT 0, isUse INTEGER DEFAULT 0,
usedAt TEXT, usedAt TEXT,
uid TEXT,
data TEXT,
createdAt TEXT NOT NULL, createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL updatedAt TEXT NOT NULL
) )
@@ -49,6 +50,8 @@ export const init = async () => {
createdAt TEXT NOT NULL, createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL, updatedAt TEXT NOT NULL,
date TEXT NOT NULL, date TEXT NOT NULL,
uid TEXT,
data TEXT,
qid TEXT, qid TEXT,
FOREIGN KEY (qid) REFERENCES question_library(id) FOREIGN KEY (qid) REFERENCES question_library(id)
) )

View File

@@ -12,8 +12,10 @@ export const questionLibrary = sqliteTable(
repeat: integer({ mode: 'boolean' }).default(false), repeat: integer({ mode: 'boolean' }).default(false),
isUse: integer({ mode: 'boolean' }).default(false), isUse: integer({ mode: 'boolean' }).default(false),
usedAt: text(), // YYYY-MM-DD format usedAt: text(), // YYYY-MM-DD format
uid: text().notNull().default(''), // 用户ID默认系统用户
createdAt: text().notNull().default(new Date().toISOString()), createdAt: text().notNull().default(new Date().toISOString()),
updatedAt: text().notNull().default(new Date().toISOString()).$onUpdate(() => new Date().toISOString()), updatedAt: text().notNull().default(new Date().toISOString()).$onUpdate(() => new Date().toISOString()),
data: text(), // JSON string
}, },
(table) => ({ (table) => ({
isUseIdx: index('idx_library_isUse').on(table.isUse), isUseIdx: index('idx_library_isUse').on(table.isUse),
@@ -33,7 +35,9 @@ export const dailyQuestions = sqliteTable(
createdAt: text().notNull().default(new Date().toISOString()), createdAt: text().notNull().default(new Date().toISOString()),
updatedAt: text().notNull().default(new Date().toISOString()).$onUpdate(() => new Date().toISOString()), updatedAt: text().notNull().default(new Date().toISOString()).$onUpdate(() => new Date().toISOString()),
date: text().notNull(), // YYYY-MM-DD format date: text().notNull(), // YYYY-MM-DD format
uid: text().notNull().default(''), // 用户ID默认系统用户
qid: text().references(() => questionLibrary.id), qid: text().references(() => questionLibrary.id),
data: text(), // JSON string
}, },
(table) => ({ (table) => ({
dateIdx: index('idx_daily_date').on(table.date), dateIdx: index('idx_daily_date').on(table.date),

View File

@@ -2,12 +2,13 @@ import { generateId, getTodayDate } from '../module/utils.ts';
import { app } from '../app.ts'; import { app } from '../app.ts';
import { getDb } from '../module/db.ts'; import { getDb } from '../module/db.ts';
import { dailyQuestions, questionLibrary } from '../module/schema.ts'; import { dailyQuestions, questionLibrary } from '../module/schema.ts';
import { eq } from 'drizzle-orm'; import { desc, eq } from 'drizzle-orm';
app.route({ app.route({
path: 'daily', path: 'daily',
key: 'random', key: 'random',
description: '随机获取一条未使用的问题' description: '随机获取一条未使用的问题',
middleware: ['auth']
}).define(async (ctx) => { }).define(async (ctx) => {
const force = ctx.query.force ?? false; const force = ctx.query.force ?? false;
const db = getDb(); const db = getDb();
@@ -37,14 +38,18 @@ app.route({
.from(questionLibrary) .from(questionLibrary)
.where(eq(questionLibrary.isUse, false)); .where(eq(questionLibrary.isUse, false));
let selectedQuestion;
if (unusedQuestions.length === 0) { if (unusedQuestions.length === 0) {
ctx.throw(404, '没有未使用的问题'); selectedQuestion = {
return; title: '今天发生了什么有趣的事情?',
description: '请描述一下你今天经历的有趣事件,分享你的感受和想法。',
tags: JSON.stringify(['生活', '分享']),
}
} else {
const randomIndex = Math.floor(Math.random() * unusedQuestions.length);
selectedQuestion = unusedQuestions[randomIndex];
} }
const randomIndex = Math.floor(Math.random() * unusedQuestions.length);
const selectedQuestion = unusedQuestions[randomIndex];
// 更新questionLibrary中的isUse和usedAt字段 // 更新questionLibrary中的isUse和usedAt字段
await db await db
.update(questionLibrary) .update(questionLibrary)
@@ -60,8 +65,9 @@ app.route({
.insert(dailyQuestions) .insert(dailyQuestions)
.values({ .values({
id: generateId(), id: generateId(),
qid: selectedQuestion.id, qid: selectedQuestion.id || '',
title: selectedQuestion.title, title: selectedQuestion.title,
summary: selectedQuestion?.description,
description: '', description: '',
tags: selectedQuestion.tags, tags: selectedQuestion.tags,
date: day, date: day,
@@ -78,11 +84,12 @@ app.route({
app.route({ app.route({
description: '获取今天的问题', description: '获取今天的问题',
path: 'daily', path: 'daily',
key: 'today' key: 'today',
middleware: ['auth']
}).define(async (ctx) => { }).define(async (ctx) => {
const db = getDb(); const db = getDb();
const day = getTodayDate(); const day = getTodayDate();
const token = ctx.query?.token;
try { try {
const todayQuestion = await db const todayQuestion = await db
.select() .select()
@@ -95,11 +102,15 @@ app.route({
const res = await ctx.call({ const res = await ctx.call({
path: 'daily', path: 'daily',
key: 'random', key: 'random',
payload: {
token: token,
}
}); });
if (res.code === 200) { if (res.code === 200) {
ctx.body = res.body; ctx.body = res.body;
return; return;
} }
console.error(res.message);
ctx.throw(500, '获取今天的问题失败'); ctx.throw(500, '获取今天的问题失败');
} }

View File

@@ -2,22 +2,41 @@ import { generateId } from '../../module/utils.ts';
import { app } from '../../app.ts'; import { app } from '../../app.ts';
import { getDb } from '../../module/db.ts'; import { getDb } from '../../module/db.ts';
import { dailyQuestions } from '../../module/schema.ts'; import { dailyQuestions } from '../../module/schema.ts';
import { eq } from 'drizzle-orm'; import { eq, and, or, like } from 'drizzle-orm';
// 列出每日问题 // 列出每日问题
app.route({ app.route({
description: '列出每日问题', description: '列出每日问题',
path: 'daily', path: 'daily',
key: 'list' key: 'list',
middleware: ['auth']
}).define(async (ctx) => { }).define(async (ctx) => {
const query = ctx.query; const query = ctx.query;
const page = query.page ?? 1; const page = query.page ?? 1;
const pageSize = query.pageSize ?? 99999; const pageSize = query.pageSize ?? 99999;
const db = getDb(); const db = getDb();
const search = query.search ?? '';
const id = query.id ?? '';
try { try {
const offset = (page - 1) * pageSize; const offset = (page - 1) * pageSize;
const allResults = await db.select().from(dailyQuestions); const allResults = await db.select().from(dailyQuestions).where(() => {
const conditions = [];
if (search) {
conditions.push(like(dailyQuestions.title, `%${search}%`));
}
if (id) {
conditions.push(eq(dailyQuestions.id, id));
}
// 如果需要 OR 逻辑(search 或 id 任一匹配)
return conditions.length > 0 ? or(...conditions) : undefined;
// 如果需要 AND 逻辑(search 和 id 都要匹配)
// return conditions.length > 0 ? and(...conditions) : undefined;
});
// Sort by createdAt in descending order (newest first) // Sort by createdAt in descending order (newest first)
const sortedResults = allResults.sort((a, b) => const sortedResults = allResults.sort((a, b) =>
@@ -47,7 +66,8 @@ app.route({
app.route({ app.route({
description: '更新每日问题', description: '更新每日问题',
path: 'daily', path: 'daily',
key: 'update' key: 'update',
middleware: ['auth']
}).define(async (ctx) => { }).define(async (ctx) => {
const query = ctx.query; const query = ctx.query;
const id = query.id; const id = query.id;
@@ -102,7 +122,8 @@ app.route({
app.route({ app.route({
description: '删除每日问题', description: '删除每日问题',
path: 'daily', path: 'daily',
key: 'delete' key: 'delete',
middleware: ['auth']
}).define(async (ctx) => { }).define(async (ctx) => {
const query = ctx.query; const query = ctx.query;
const id = query.id; const id = query.id;
@@ -128,7 +149,8 @@ app.route({
app.route({ app.route({
description: '获取每日问题详情', description: '获取每日问题详情',
path: 'daily', path: 'daily',
key: 'detail' key: 'detail',
middleware: ['auth']
}).define(async (ctx) => { }).define(async (ctx) => {
const query = ctx.query; const query = ctx.query;
const id = query.id; const id = query.id;

View File

@@ -1,4 +1,17 @@
import './daily/index.ts' import './daily/index.ts'
import './library/index.ts' import './library/index.ts'
import './daily-task.ts' import './daily-task.ts'
import './library-task.ts' import './library-task.ts'
import { app } from '../app.ts';
const hasAuth = app.router.routes.some(r => r.id === 'auth');
if (!hasAuth) {
console.log('添加认证中间件路由');
app.route({
path: 'auth',
key: 'auth',
description: '用户认证',
id: 'auth'
}).define(async (ctx) => {
// 这里可以添加实际的认证逻辑
}).addTo(app);
}

View File

@@ -7,7 +7,8 @@ import { eq } from 'drizzle-orm';
app.route({ app.route({
path: 'library', path: 'library',
key: 'setAllUnused', key: 'setAllUnused',
description: '将所有问题设置为未使用' description: '将所有问题设置为未使用',
middleware: ['auth']
}).define(async (ctx) => { }).define(async (ctx) => {
const db = getDb(); const db = getDb();
try { try {

View File

@@ -8,7 +8,8 @@ import { eq } from 'drizzle-orm';
app.route({ app.route({
description: '列出问题库', description: '列出问题库',
path: 'library', path: 'library',
key: 'list' key: 'list',
middleware: ['auth']
}).define(async (ctx) => { }).define(async (ctx) => {
const query = ctx.query; const query = ctx.query;
const page = query.page ?? 1; const page = query.page ?? 1;
@@ -47,7 +48,8 @@ app.route({
app.route({ app.route({
description: '更新问题库', description: '更新问题库',
path: 'library', path: 'library',
key: 'update' key: 'update',
middleware: ['auth']
}).define(async (ctx) => { }).define(async (ctx) => {
const query = ctx.query; const query = ctx.query;
const id = query.id; const id = query.id;
@@ -104,7 +106,8 @@ app.route({
app.route({ app.route({
description: '删除问题库', description: '删除问题库',
path: 'library', path: 'library',
key: 'delete' key: 'delete',
middleware: ['auth']
}).define(async (ctx) => { }).define(async (ctx) => {
const query = ctx.query; const query = ctx.query;
const id = query.id; const id = query.id;
@@ -130,7 +133,8 @@ app.route({
app.route({ app.route({
description: '获取问题库详情', description: '获取问题库详情',
path: 'library', path: 'library',
key: 'detail' key: 'detail',
middleware: ['auth']
}).define(async (ctx) => { }).define(async (ctx) => {
const query = ctx.query; const query = ctx.query;
const id = query.id; const id = query.id;

93
backend/zip.ts Normal file
View File

@@ -0,0 +1,93 @@
import archiver from 'archiver';
import fs from 'fs';
import path from 'path';
import * as pkgs from './package.json';
/**
* 创建 ZIP 压缩包
* @param outputPath - 输出的 zip 文件路径
* @param files - 要压缩的文件列表
* @param directories - 要压缩的目录列表
*/
export async function createZip(
outputPath: string,
files: string[] = [],
directories: string[] = []
): Promise<void> {
return new Promise((resolve, reject) => {
// 创建输出流
const output = fs.createWriteStream(outputPath);
const archive = archiver('zip', {
zlib: { level: 9 } // 最高压缩级别
});
// 监听完成事件
output.on('close', () => {
console.log(`✅ 压缩完成: ${archive.pointer()} bytes`);
console.log(`📦 输出文件: ${outputPath}`);
resolve();
});
// 监听警告
archive.on('warning', (err) => {
if (err.code === 'ENOENT') {
console.warn('⚠️ 警告:', err);
} else {
reject(err);
}
});
// 监听错误
archive.on('error', (err) => {
reject(err);
});
// 连接输出流
archive.pipe(output);
// 添加文件
for (const file of files) {
if (fs.existsSync(file)) {
archive.file(file, { name: path.basename(file) });
console.log(`📄 添加文件: ${file}`);
} else {
console.warn(`⚠️ 文件不存在: ${file}`);
}
}
// 添加目录
for (const dir of directories) {
if (fs.existsSync(dir)) {
archive.directory(dir, path.basename(dir));
console.log(`📁 添加目录: ${dir}`);
} else {
console.warn(`⚠️ 目录不存在: ${dir}`);
}
}
// 完成归档
archive.finalize();
});
}
/**
* 为项目创建发布包
*/
export async function createReleaseZip(): Promise<void> {
const files = [
'daily-question.exe',
'readme.md'
];
const directories = [
'public'
];
await createZip(`daily-question-${pkgs.version}.zip`, files, directories);
}
// 如果直接运行此文件
if (import.meta.main) {
createReleaseZip()
.then(() => console.log('🎉 打包成功!'))
.catch((err) => console.error('❌ 打包失败:', err));
}

34
frontend/copy.ts Normal file
View File

@@ -0,0 +1,34 @@
/**
* 将 dist 目录的内容复制到 backend/public 目录, 使用bun运行
*
* @tags build, deploy, copy
* @description 构建后将前端静态资源复制到后端公共目录,复制前会删除已有内容
* @title 前端资源复制脚本
* @createdAt 2025-11-24
*/
import { existsSync, rmSync, cpSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const distPath = resolve(__dirname, 'dist');
const backendPath = resolve(__dirname, '../backend/public');
const frontendPath = resolve(backendPath, 'root/daily-question');
// 删除已有的 backend/public 目录内容
if (existsSync(backendPath)) {
console.log('删除已有的 backend/public 目录...');
rmSync(backendPath, { recursive: true, force: true });
console.log('✓ 已删除旧内容');
}
// 复制 dist 内容到 backend/public
if (existsSync(distPath)) {
console.log('复制 dist 内容到 backend/public...');
cpSync(distPath, frontendPath, { recursive: true });
console.log('✓ 复制完成');
} else {
console.error('错误: dist 目录不存在,请先运行构建命令');
process.exit(1);
}

View File

@@ -7,6 +7,7 @@
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"build": "astro build", "build": "astro build",
"postbuild": "bun copy.ts",
"preview": "astro preview", "preview": "astro preview",
"pub": "envision deploy ./dist -k daily-question -v 0.0.1 -u", "pub": "envision deploy ./dist -k daily-question -v 0.0.1 -u",
"ui": "pnpm dlx shadcn@latest add " "ui": "pnpm dlx shadcn@latest add "
@@ -48,11 +49,13 @@
}, },
"devDependencies": { "devDependencies": {
"@kevisual/types": "^0.0.10", "@kevisual/types": "^0.0.10",
"@types/node": "^24.10.1",
"@types/react": "^19.2.6", "@types/react": "^19.2.6",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-basic-ssl": "^2.1.0", "@vitejs/plugin-basic-ssl": "^2.1.0",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"tailwindcss": "^4.1.17", "tailwindcss": "^4.1.17",
"tsx": "^4.20.6",
"tw-animate-css": "^1.4.0" "tw-animate-css": "^1.4.0"
}, },
"packageManager": "pnpm@10.23.0", "packageManager": "pnpm@10.23.0",

View File

@@ -5,6 +5,8 @@ import { useShallow } from 'zustand/shallow';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { AppModal } from './modal.tsx' import { AppModal } from './modal.tsx'
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { Footer } from '../footer.tsx';
import { exportJson } from '@/lib/json.ts';
const render = (dateStr: string) => { const render = (dateStr: string) => {
return dayjs(dateStr).format('YYYY-MM-DD HH:mm:ss'); return dayjs(dateStr).format('YYYY-MM-DD HH:mm:ss');
@@ -22,6 +24,10 @@ export const App = () => {
store.getList(); store.getList();
}, []) }, [])
const columns = [ const columns = [
{
title: 'ID',
dataIndex: 'id',
},
{ {
title: '日期', title: '日期',
dataIndex: 'date', dataIndex: 'date',
@@ -80,7 +86,6 @@ export const App = () => {
<Button danger onClick={async () => { <Button danger onClick={async () => {
await store.deleteItem(record.id); await store.deleteItem(record.id);
message.success('删除成功');
store.getList(); store.getList();
}}></Button> }}></Button>
</Space> </Space>
@@ -96,6 +101,7 @@ export const AppProvider = () => {
<WrapperTable> <WrapperTable>
<App /> <App />
</WrapperTable> </WrapperTable>
<Footer />
</ConfigProvider> </ConfigProvider>
} }
export const Header = () => { export const Header = () => {
@@ -103,6 +109,7 @@ export const Header = () => {
open: state.open, open: state.open,
setOpen: state.setOpen, setOpen: state.setOpen,
setFormData: state.setFormData, setFormData: state.setFormData,
list: state.dailyList,
}))); })));
return <div className='container mx-auto px-4 py-4 flex justify-between items-center'> return <div className='container mx-auto px-4 py-4 flex justify-between items-center'>
<h1 className='text-2xl font-bold'></h1> <h1 className='text-2xl font-bold'></h1>
@@ -112,6 +119,9 @@ export const Header = () => {
store.setFormData({}); store.setFormData({});
store.setOpen(true) store.setOpen(true)
}}></Button> }}></Button>
<Button type="default" onClick={async () => {
exportJson(store.list, 'question-daily.json');
}}></Button>
</Space> </Space>
</div> </div>
} }

View File

@@ -0,0 +1,50 @@
import { wrapBasename } from "@/modules/basename"
export const Footer = () => {
const links = [
{
href: wrapBasename('/'),
label: '每日一题',
},
{
href: wrapBasename('/daily'),
label: '历史记录',
},
{
href: wrapBasename('/library'),
label: '资料库',
},
{
href: wrapBasename('/docs'),
label: '文档',
},
]
return (
<footer className="fixed bottom-0 w-full bg-white border-t border-gray-200 shadow-lg">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
{/* 链接区域 */}
<nav className="flex flex-wrap justify-center items-center gap-2 sm:gap-4 mb-3">
{links.map((link) => (
<a
key={link.href}
href={link.href}
className="relative px-4 py-2 text-sm sm:text-base font-medium text-gray-600 hover:text-blue-600 transition-all duration-300 ease-in-out
before:absolute before:bottom-0 before:left-0 before:w-0 before:h-0.5 before:bg-blue-600 before:transition-all before:duration-300
hover:before:w-full active:scale-95"
>
{link.label}
</a>
))}
</nav>
{/* 版权信息 */}
<div className="text-center text-xs sm:text-sm text-gray-500">
&copy; 2025 Daily Question
</div>
</div>
</footer>
)
}

View File

@@ -5,6 +5,8 @@ import { useShallow } from 'zustand/shallow';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { AppModal } from './modal' import { AppModal } from './modal'
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { Footer } from '../footer';
import { exportJson } from '@/lib/json';
const render = (dateStr: string) => { const render = (dateStr: string) => {
return dayjs(dateStr).format('YYYY-MM-DD HH:mm:ss'); return dayjs(dateStr).format('YYYY-MM-DD HH:mm:ss');
@@ -87,7 +89,6 @@ export const App = () => {
<Button danger onClick={async () => { <Button danger onClick={async () => {
await store.deleteItem(record.id); await store.deleteItem(record.id);
message.success('删除成功');
store.getList(); store.getList();
}}></Button> }}></Button>
</Space> </Space>
@@ -101,12 +102,14 @@ export const AppProvider = () => {
<WrapperTable> <WrapperTable>
<App /> <App />
</WrapperTable> </WrapperTable>
<Footer />
</ConfigProvider> </ConfigProvider>
} }
export const Header = () => { export const Header = () => {
const store = useStore(useShallow((state) => ({ const store = useStore(useShallow((state) => ({
open: state.open, open: state.open,
setOpen: state.setOpen, setOpen: state.setOpen,
list: state.libraryList,
setFormData: state.setFormData, setFormData: state.setFormData,
}))); })));
return <div className='container mx-auto px-4 py-4 flex justify-between items-center'> return <div className='container mx-auto px-4 py-4 flex justify-between items-center'>
@@ -117,6 +120,10 @@ export const Header = () => {
store.setFormData({}); store.setFormData({});
store.setOpen(true) store.setOpen(true)
}}></Button> }}></Button>
<Button type="default" onClick={async () => {
exportJson(store.list, 'question-library.json');
}}></Button>
</Space> </Space>
</div> </div>
} }

View File

@@ -5,6 +5,8 @@ import { useShallow } from "zustand/shallow";
import MDEditor from '@uiw/react-md-editor'; import MDEditor from '@uiw/react-md-editor';
import { parseDataTag } from './utils.ts'; import { parseDataTag } from './utils.ts';
import { ConfigProvider } from 'antd'; import { ConfigProvider } from 'antd';
import { Footer } from "../footer.tsx";
import { wrapBasename } from "@/modules/basename.ts";
export const App = () => { export const App = () => {
const store = useStore(useShallow((state) => ({ const store = useStore(useShallow((state) => ({
today: state.today, today: state.today,
@@ -35,10 +37,10 @@ export const App = () => {
<div className="container mx-auto px-4 py-8 max-w-5xl"> <div className="container mx-auto px-4 py-8 max-w-5xl">
{/* 头部标题 */} {/* 头部标题 */}
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<h1 className="text-4xl font-bold mb-2 bg-linear-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"> <div className="text-4xl font-bold mb-2 bg-linear-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
{store.today?.date}
</h1> </div>
<p className="text-gray-500 text-sm">Daily Question Challenge</p> <p className="text-gray-500 text-sm">{store.today?.date}</p>
</div> </div>
{/* 题目卡片 */} {/* 题目卡片 */}
@@ -46,9 +48,15 @@ export const App = () => {
<div className="mb-8 bg-white rounded-2xl shadow-lg hover:shadow-xl transition-shadow duration-300 p-6 border border-gray-100"> <div className="mb-8 bg-white rounded-2xl shadow-lg hover:shadow-xl transition-shadow duration-300 p-6 border border-gray-100">
<div className="flex items-start justify-between mb-4"> <div className="flex items-start justify-between mb-4">
<h2 className="text-2xl font-bold text-gray-800 flex-1">{store.today.title}</h2> <h2 className="text-2xl font-bold text-gray-800 flex-1">{store.today.title}</h2>
{store.today.qid && ( {store.today.id && (
<span className="ml-4 px-3 py-1 bg-linear-to-r from-blue-500 to-purple-500 text-white rounded-full text-xs font-semibold"> <span className="ml-4 px-3 py-1 bg-linear-to-r from-blue-500 to-purple-500 text-white rounded-full text-xs font-semibold cursor-pointer"
#{store.today.qid} onClick={() => {
const url = new URL(window.location.href);
url.searchParams.set('id', store.today!.id);
url.pathname = wrapBasename('/daily');
window.open(url.toString(), '_blank');
}}>
#{store.today.id}
</span> </span>
)} )}
</div> </div>
@@ -114,5 +122,6 @@ export const App = () => {
export const AppProvider = () => { export const AppProvider = () => {
return <ConfigProvider> return <ConfigProvider>
<App /> <App />
<Footer />
</ConfigProvider> </ConfigProvider>
} }

View File

@@ -93,17 +93,24 @@ export const useStore = create<TodayStore>((set, get) => ({
} }
}); });
if (res.code === 200) { if (res.code === 200) {
console.log('答案提交成功'); message.success('答案提交成功');
} else { } else {
console.error('答案提交失败'); message.error('答案提交失败');
} }
}, },
dailyList: [], dailyList: [],
setDailyList: (data: DailyData[]) => set({ dailyList: data }), setDailyList: (data: DailyData[]) => set({ dailyList: data }),
getDailyList: async () => { getDailyList: async () => {
const searchParam = new URLSearchParams(window.location.search);
const search = searchParam.get('search') || '';
const id = searchParam.get('id') || '';
const res = await query.post({ const res = await query.post({
path: 'daily', path: 'daily',
key: 'list' key: 'list',
payload: {
search,
id
}
}) })
console.log('daily list', res); console.log('daily list', res);
if (res.code === 200) { if (res.code === 200) {

View File

@@ -1,7 +1,7 @@
--- ---
title: '01-frontend' title: '01-frontend'
tags: ['frontend', 'react', 'prompt'] tags: ['frontend', 'react', 'prompt']
description: '第一次对功能进行最简单的解析' description: '第一次对功能进行最简单的解析, 整个前端页面的功能的大概概览'
--- ---
## 前端页面 ## 前端页面

View File

@@ -0,0 +1,16 @@
---
title: '02-下载应用到本地运行'
tags: ['frontend', 'prompt']
description: '本地使用sqlite为后端, 访问 http://localhost:9000 访问对应的页面的内容'
---
## 下载应用
https://kevisual.lanzoub.com/s/daily-question
## 使用方法
1. 双击执行,访问 http://localhost:9000
首页是`http://localhost:9000/root/daily-question/`

12
frontend/src/lib/json.ts Normal file
View File

@@ -0,0 +1,12 @@
export const exportJson = (data: any, filename = 'data.json') => {
const jsonStr = JSON.stringify(data, null, 2);
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}

View File

@@ -1,4 +1,12 @@
// @ts-ignore // @ts-ignore
export const basename = BASE_NAME; export const basename = BASE_NAME;
console.log(basename); console.log(basename);
export const wrapBasename = (path: string) => {
if (basename) {
return `${basename}${path}/`;
} else {
return path;
}
}

View File

@@ -1,5 +1,14 @@
import { Query } from '@kevisual/query' import { Query } from '@kevisual/query'
const getUrl = () => {
const host = window.location.host
const isKevisual = host.includes('kevisual');
if (isKevisual) {
return '/api/router'
}
return '/client/router'
}
export const query = new Query({ export const query = new Query({
url: '/client/router' url: getUrl()
}); });

View File

@@ -1,7 +1,6 @@
--- ---
import { getCollection } from 'astro:content'; import { getCollection } from 'astro:content';
const posts = await getCollection('docs'); const posts = await getCollection('docs');
console.log('post', posts);
import { basename } from '@/modules/basename'; import { basename } from '@/modules/basename';
import Blank from '@/layouts/blank.astro'; import Blank from '@/layouts/blank.astro';
--- ---
@@ -28,7 +27,7 @@ import Blank from '@/layouts/blank.astro';
<article class='group bg-white dark:bg-slate-800 rounded-xl shadow-sm hover:shadow-xl transition-all duration-300 overflow-hidden border border-slate-200 dark:border-slate-700 hover:border-blue-500 dark:hover:border-blue-400'> <article class='group bg-white dark:bg-slate-800 rounded-xl shadow-sm hover:shadow-xl transition-all duration-300 overflow-hidden border border-slate-200 dark:border-slate-700 hover:border-blue-500 dark:hover:border-blue-400'>
<div class='p-6'> <div class='p-6'>
{/* 文档标题 */} {/* 文档标题 */}
<a href={`${basename}/docs/${post.id}`} class='block'> <a href={`${basename}/docs/${post.id}/`} class='block'>
<h2 class='text-xl sm:text-2xl font-semibold text-slate-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-200 mb-3'> <h2 class='text-xl sm:text-2xl font-semibold text-slate-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-200 mb-3'>
{post.data.title} {post.data.title}
</h2> </h2>
@@ -51,7 +50,7 @@ import Blank from '@/layouts/blank.astro';
{/* 阅读更多指示器 */} {/* 阅读更多指示器 */}
<a <a
href={`${basename}/docs/${post.id}`} href={`${basename}/docs/${post.id}/`}
class='mt-4 flex items-center text-blue-600 dark:text-blue-400 text-sm font-medium opacity-0 group-hover:opacity-100 transition-opacity duration-200'> class='mt-4 flex items-center text-blue-600 dark:text-blue-400 text-sm font-medium opacity-0 group-hover:opacity-100 transition-opacity duration-200'>
<span>阅读更多</span> <span>阅读更多</span>
<svg <svg

545
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff