generated from template/router-template
update
This commit is contained in:
21
backend/bun.config.mjs
Normal file
21
backend/bun.config.mjs
Normal 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_*',
|
||||||
|
});
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
## 后端功能
|
## 使用方法
|
||||||
|
|
||||||
|
运行exe后,浏览器访问 http://localhost:9000/
|
||||||
|
|
||||||
|
## 后端功能提示词
|
||||||
|
|
||||||
使用db,better-sqite3
|
使用db,better-sqite3
|
||||||
|
|
||||||
|
|||||||
@@ -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
4
backend/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { app } from './app.ts'
|
||||||
|
import './router/index.ts';
|
||||||
|
|
||||||
|
export { app }
|
||||||
@@ -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');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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, '获取今天的问题失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
93
backend/zip.ts
Normal 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
34
frontend/copy.ts
Normal 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);
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
50
frontend/src/apps/footer.tsx
Normal file
50
frontend/src/apps/footer.tsx
Normal 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">
|
||||||
|
© 2025 Daily Question
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '01-frontend'
|
title: '01-frontend'
|
||||||
tags: ['frontend', 'react', 'prompt']
|
tags: ['frontend', 'react', 'prompt']
|
||||||
description: '第一次对功能进行最简单的解析'
|
description: '第一次对功能进行最简单的解析, 整个前端页面的功能的大概概览'
|
||||||
---
|
---
|
||||||
|
|
||||||
## 前端页面
|
## 前端页面
|
||||||
|
|||||||
16
frontend/src/data/docs/02-downlocal.md
Normal file
16
frontend/src/data/docs/02-downlocal.md
Normal 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
12
frontend/src/lib/json.ts
Normal 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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
});
|
});
|
||||||
@@ -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
545
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user