This commit is contained in:
ZZY 2024-07-23 22:39:54 +08:00
commit e7cca10a29
44 changed files with 4521 additions and 0 deletions

33
.gitignore vendored Normal file
View File

@ -0,0 +1,33 @@
.*
!.gitignore
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

38
README.md Normal file
View File

@ -0,0 +1,38 @@
# vue-project
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```
https://tailwindcss.com/docs/guides/vite#vue
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

1
env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/Home.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>zzyxyz</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

2790
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View File

@ -0,0 +1,32 @@
{
"name": "vue-project",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build --force"
},
"dependencies": {
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-router": "^4.3.2"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.4",
"@types/node": "^20.12.5",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/tsconfig": "^0.5.1",
"autoprefixer": "^10.4.19",
"daisyui": "^4.11.1",
"npm-run-all2": "^6.1.2",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"typescript": "~5.4.0",
"vite": "^5.2.8",
"vue-tsc": "^2.0.11"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
public/Home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

154
public/fetch-wrapper.js Normal file
View File

@ -0,0 +1,154 @@
class FetchWrapper {
constructor(baseURL, options = {}, contentTypeMappings = {}, onError = console.error) {
this.baseURL = baseURL;
this.contentTypeMappings = contentTypeMappings;
this.defaultContentType = 'application/json';
this.onError = onError;
this.defaultOptions = {
...{
method: 'GET',
headers: new Headers({
'Content-Type': this.defaultContentType
}),
mode: 'cors', // 指定是否允许跨域请求默认为cors
cache: 'default', // 请求的缓存模式
credentials: 'same-origin', // 请求的凭据模式默认同源请求时发送cookies
parseResponseHandler: {
'application/json': response => response.json(),
'text/*': response => response.text(),
'image/*': response => response.blob(),
'application/octet-stream': response => response.blob(),
'*/*': response => response.arrayBuffer(), // 默认处理程序
},
...options
}
};
}
async fetch(url, options = {}) {
// 合并默认选项与传入的options
const mergedOptions = { ...this.defaultOptions, ...options };
// 根据URL自动设置Content-Type如果有匹配的映射
const matchedMapping = Object.entries(this.contentTypeMappings).find(([pattern, type]) => url.endsWith(pattern));
if (matchedMapping && !mergedOptions.headers.has('Content-Type')) {
mergedOptions.headers['Content-Type'] = matchedMapping[1];
}
// 根据请求方法处理数据
if (['POST', 'PUT', 'PATCH'].includes(mergedOptions.method)) {
if (typeof mergedOptions.body === 'object') {
mergedOptions.body = JSON.stringify(mergedOptions.body);
// 如果Content-Type未设置或默认为application/json则需要在这里设置
if (!mergedOptions.headers.has('Content-Type')) {
mergedOptions.headers.set('Content-Type', 'application/json');
}
} else if (mergedOptions.body !== null && typeof mergedOptions.body !== 'string') {
throw new Error('非GET请求时, body必须是一个对象或字符串');
}
} else if (mergedOptions.method === 'GET' && mergedOptions.body) {
console.warn('GET请求不支持发送请求体, 已忽略提供的body参数');
delete mergedOptions.body;
}
url = new URL(url, this.baseURL).href;
return fetch(url, mergedOptions)
.then(async response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
// console.warn(`HTTP warning! status: ${response.status}`);
}
const contentType = response.headers.get('content-type').split(';')[0].trim();
const parseResponseHandler = this.getParseResponseHandler(contentType);
const responseData = await parseResponseHandler(response);
return formatResponse(response, responseData);
})
.catch(error => {
error.options = mergedOptions;
error.url = url;
error.timestamp = Date.now();
error.onFunction = 'fetch';
this.onError(error.message);
throw error;
});
}
getParseResponseHandler(contentType) {
if (contentType.startsWith('text/')) {
return this.defaultOptions.parseResponseHandler['text/*'] || this.defaultOptions.parseResponseHandler['*/*'];
}
if (contentType.startsWith('image/')) {
return this.defaultOptions.parseResponseHandler['image/*'] || this.defaultOptions.parseResponseHandler['*/*'];
}
return this.defaultOptions.parseResponseHandler[contentType] || this.defaultOptions.parseResponseHandler['*/*'];
}
async fetchWithRetry(url, options = {}, maxRetries = 3, retryDelayBaseMs = 1000) {
let remainingAttempts = maxRetries;
const delays = [];
for (let i = 0; i < maxRetries; i++) {
delays.push(retryDelayBaseMs * Math.pow(2, i));
}
const attemptFetch = async (retryIndex = 0) => {
try {
return await this.fetch(url, options);
} catch (error) {
if (remainingAttempts > 1) {
console.log(`请求失败,剩余重试次数:${remainingAttempts - 1}`);
setTimeout(() => attemptFetch(retryIndex + 1), delays[retryIndex]);
remainingAttempts--;
return;
} else {
this.onError(error.message);
throw error;
}
}
};
return attemptFetch();
}
// 可以为不同的HTTP方法提供便捷的方法
async post(url, body, options = {}) {
return this.fetch(url, { ...options, method: 'POST', body });
}
async get(url, options = {}) {
return this.fetch(url, { ...options, method: 'GET' });
}
async put(url, body, options = {}) {
return this.fetch(url, { ...options, method: 'PUT', body });
}
async patch(url, body, options = {}) {
return this.fetch(url, { ...options, method: 'PATCH', body });
}
async delete(url, options = {}) {
return this.fetch(url, { ...options, method: 'DELETE' });
}
};
function formatResponse(response, data) {
return {
status: response.status,
message: response.statusText,
data,
};
}
// 检查是否在Node.js环境中
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
// Node.js环境使用CommonJS
module.exports = FetchWrapper;
} else if (typeof window !== 'undefined') {
// 浏览器环境,直接暴露到全局作用域(不推荐,但简单示例)
// window.FetchWrapper = FetchWrapper;
}

View File

@ -0,0 +1,144 @@
# FetchWrapper 类使用文档
**概述:**
`FetchWrapper` 是一个基于 `fetch API` 的封装类用于简化网络请求操作并提供默认选项、Content-Type 自动映射、错误处理和重试机制等功能。
## 构造函数
````javascript
new FetchWrapper(baseURL, contentTypeMappings = {}, defaultOptions = {}, onError = console.error)
````
- **baseURL**:基础 URL 字符串,所有相对路径的请求都会基于此 URL 进行拼接。
- **contentTypeMappings**:对象,键为文件扩展名或通配符模式,值为对应的 Content-Type当请求 URL 匹配时自动设置 Content-Type 头部。
- **defaultOptions**:对象,定义了发起请求时的默认选项,例如:
````javascript
{
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
// 其他默认配置项...
}
````
- **onError**:错误回调函数,当发生网络请求错误时触发,默认为 `console.error`
## 主要方法
### formatResponse(response, data)
````javascript
formatResponse(response, data)
````
- 格式化响应对象和数据,返回一个统一格式的对象。
### fetch(url, options = {})
````javascript
async fetch(url, options = {})
````
- 发起 HTTP 请求,参数说明:
- **url**:请求 URL。
- **options**:覆盖默认选项的对象,可自定义请求方法、头部、请求体等信息。
### getParseResponseHandler(contentType)
````javascript
getParseResponseHandler(contentType)
````
- 根据响应内容类型Content-Type获取相应的解析器函数。
### fetchWithRetry(url, options = {}, maxRetries = 3, retryDelayBaseMs = 1000)
````javascript
async fetchWithRetry(url, options = {}, maxRetries = 3, retryDelayBaseMs = 1000)
````
- 发起带有重试机制的 HTTP 请求,在请求失败时会按照指数退避策略进行重试。
- **maxRetries**最大重试次数默认为3次。
- **retryDelayBaseMs**首次重试延迟时间的基础单位默认为1000毫秒。
### post(url, body, options = {})
### get(url, options = {})
### put(url, body, options = {})
### patch(url, body, options = {})
### delete(url, options = {})
这些是针对不同HTTP方法的便捷调用方法。
## 示例使用
```javascript
const wrapper = new FetchWrapper('https://api.example.com', {
'.json': 'application/json',
}, {
credentials: 'include',
});
async function fetchData() {
try {
const response = await wrapper.get('/data.json');
console.log(response.data);
} catch (error) {
wrapper.onError(error);
}
}
//使用带重试机制的 POST 请求
async function postDataWithRetry() {
const body = { key: 'value' };
const options = { headers: { 'X-Custom-Header': 'value' } };
try {
const response = await wrapper.fetchWithRetry('/post-data', { body }, 5, 2000);
console.log(response.data);
} catch (error) {
wrapper.onError(error);
}
}
````
以上JavaScript代码已用Markdown代码块包裹可以直接复制并粘贴到您的项目中。通过创建一个 `FetchWrapper` 实例,您可以利用其便捷的方法来发起网络请求,并根据需要灵活定制请求选项以及实现重试功能。
当您在fetch-wrapper.js文件中使用export default class FetchWrapper {...}导出FetchWrapper类时您可以在其他JavaScript模块中这样导入
````javascript
// 导入 FetchWrapper 类
import FetchWrapper from './fetch-wrapper';
// 创建一个实例
const wrapper = new FetchWrapper('https://api.example.com', {}, { credentials: 'include' });
// 使用这个实例发起请求
async function fetchData() {
try {
const response = await wrapper.get('/data.json');
console.log(response.data);
} catch (error) {
wrapper.onError(error);
}
}
fetchData();
````
这里,./fetch-wrapper是相对路径表示FetchWrapper类所在的模块位置。根据实际项目结构您需要调整这个路径以便正确指向fetch-wrapper.js文件。
formatResponse(response, data) {
return {
status: response.status,
message: response.statusText,
data,
};
}

BIN
public/ico/beian.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
public/ico/qq-link.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/ico/qq.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/ico/wechat-link.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
public/ico/wechat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

3
public/setting.json Normal file
View File

@ -0,0 +1,3 @@
{
"isServer": false
}

10
src/App.vue Normal file
View File

@ -0,0 +1,10 @@
<template>
<RouterView></RouterView>
</template>
<script setup lang="ts">
import { RouterView } from 'vue-router'
</script>
<style scoped>
</style>

42
src/components/Footer.vue Normal file
View File

@ -0,0 +1,42 @@
<template>
<footer class="footer p-10 bg-base-200 text-base-content">
<aside>
<svg width="50" height="50" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd"
clip-rule="evenodd" class="fill-current">
<path
d="M22.672 15.226l-2.432.811.841 2.515c.33 1.019-.209 2.127-1.23 2.456-1.15.325-2.148-.321-2.463-1.226l-.84-2.518-5.013 1.677.84 2.517c.391 1.203-.434 2.542-1.831 2.542-.88 0-1.601-.564-1.86-1.314l-.842-2.516-2.431.809c-1.135.328-2.145-.317-2.463-1.229-.329-1.018.211-2.127 1.231-2.456l2.432-.809-1.621-4.823-2.432.808c-1.355.384-2.558-.59-2.558-1.839 0-.817.509-1.582 1.327-1.846l2.433-.809-.842-2.515c-.33-1.02.211-2.129 1.232-2.458 1.02-.329 2.13.209 2.461 1.229l.842 2.515 5.011-1.677-.839-2.517c-.403-1.238.484-2.553 1.843-2.553.819 0 1.585.509 1.85 1.326l.841 2.517 2.431-.81c1.02-.33 2.131.211 2.461 1.229.332 1.018-.21 2.126-1.23 2.456l-2.433.809 1.622 4.823 2.433-.809c1.242-.401 2.557.484 2.557 1.838 0 .819-.51 1.583-1.328 1.847m-8.992-6.428l-5.01 1.675 1.619 4.828 5.011-1.674-1.62-4.829z">
</path>
</svg>
<p>ACME Industries Ltd.<br>Providing reliable tech since 1992</p>
</aside>
<nav>
<h6 class="footer-title">Services</h6>
<a class="link link-hover">Branding</a>
<a class="link link-hover">Design</a>
<a class="link link-hover">Marketing</a>
<a class="link link-hover">Advertisement</a>
</nav>
<nav>
<h6 class="footer-title">Company</h6>
<a class="link link-hover">About us</a>
<a class="link link-hover">Contact</a>
<a class="link link-hover">Jobs</a>
<a class="link link-hover">Press kit</a>
</nav>
<nav>
<h6 class="footer-title">Legal</h6>
<a href="https://beian.miit.gov.cn" target="_blank" class="link link-hover">
<!-- <img class="rounded-circle shadow" src="/pic/ico/beian.png" alt="beian-pic" width="25"> -->
赣ICP备2023009103号
</a>
<a class="link link-hover">Terms of use</a>
<a class="link link-hover">Privacy policy</a>
<a class="link link-hover">Cookie policy</a>
</nav>
</footer>
</template>
<script setup lang="ts">
</script>
<style scoped></style>

25
src/components/Hero.vue Normal file
View File

@ -0,0 +1,25 @@
<template>
<div v-if="isView" class="hero min-h-screen bg-base-200">
<div class="hero-content text-center">
<div class="max-w-md">
<h1 class="text-5xl font-bold">Hello World</h1>
<p class="py-6">This is a Vue 3 app with Tailwind CSS.</p>
<button @click="btnClick" class="btn btn-primary">Get Started</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
let isView = ref(true)
function btnClick() {
isView.value = false
}
</script>
<style scoped>
</style>

85
src/components/Navbar.vue Normal file
View File

@ -0,0 +1,85 @@
<template>
<div class="navbar bg-base-100">
<div class="navbar-start">
<RouterLink :to="{ name: 'home' }" class="btn btn-ghost text-xl">ZZYXYZ</RouterLink>
<div class="dropdown">
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16" />
</svg>
</div>
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
<li><a>Item 1</a></li>
<li>
<a>Parent</a>
<ul class="p-2">
<li><a>Submenu 1</a></li>
<li><a>Submenu 2</a></li>
</ul>
</li>
<li><a>Item 3</a></li>
</ul>
</div>
<div class="hidden lg:flex">
<ul class="menu menu-horizontal px-1">
<li><a>Item 1</a></li>
<li>
<details>
<summary>Parent</summary>
<ul class="p-2">
<li><a>Submenu 1</a></li>
<li><a>Submenu 2</a></li>
</ul>
</details>
</li>
<li><a>Item 3</a></li>
</ul>
</div>
</div>
<div class="navbar-center hidden lg:flex">
</div>
<div class="navbar-end">
<div class="form-control">
<input type="text" placeholder="Search" class="input input-bordered w-24 md:w-auto" />
</div>
<button class="btn btn-ghost btn-circle">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
<button class="btn btn-ghost btn-circle">
<div class="indicator">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
<span class="badge badge-xs badge-primary indicator-item"></span>
</div>
</button>
</div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar">
<div class="w-10 rounded-full">
<img alt="Tailwind CSS Navbar component" src="/Home.png" />
</div>
</div>
<ul tabindex="0" class="mt-3 z-[1] p-2 shadow menu menu-sm dropdown-content bg-base-100 rounded-box w-52">
<li>
<a class="justify-between">
Profile
<span class="badge">New</span>
</a>
</li>
<li><a>Settings</a></li>
<li><a>Logout</a></li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped></style>

View File

@ -0,0 +1,26 @@
// import axios from "axios";
export const schema = [
{ name: 'title', label: '标题', type: 'input' },
{ name: 'body', label: '内容', type: 'input' },
{ name: 'jumpHerf', label: '跳转链接', type: 'input' },
{ name: 'btnText', label: '按钮内容', type: 'input' },
{ name: 'btnHref', label: '按钮链接', type: 'input' },
]
export type cardType = {
title?: string | null
body?: string | null
jumpHerf?: string | null
imgSrc?: string | null
imgAlt?: string | null
btnText?: string | null
btnHref?: string | null
};
function getData() {
}
function setData() {
}

View File

@ -0,0 +1,44 @@
<template>
<div :class="cardClass">
<div class="card-body">
<figure v-if="card.imgAlt && card.imgSrc">
<img :src="card.imgSrc" :alt="card.imgAlt" />
</figure>
<div class="card-title"> {{ card.title || "title" }}</div>
<p v-if="card.body"> {{ card.body }} </p>
<div class="card-actions justify-end">
<a v-if="card.btnHref" :href="getRealHref(card.btnHref)" class="btn btn-primary">
{{ card.btnText || "btn" }}
</a>
<a v-if="card.jumpHerf" :href="getRealHref(card.jumpHerf)" class="btn btn-primary">
{{ "Jump Link" }}
</a>
</div>
</div>
</div>
</template>
<script setup lang='ts'>
import { computed, ref, watch } from 'vue'
import { type cardType } from './Card'
let { card, idx } = defineProps<{
card: cardType,
idx: number
}>()
function getRealHref(href: string) {
if (href.startsWith('$')) {
return `/card/_${href.substring(1)}`;
}
return href;
}
const cardClass = computed(() => ({
'flex flex-col flex-grow flex-shrink': true,
'card bg-base-100 shadow-xl': true,
}))
</script>
<style scoped></style>

View File

@ -0,0 +1,80 @@
<template>
<div v-show="isShowEdit">
<div class="join indicator-item">
<button @click="editHandle" class="btn join-item btn-xs">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil-square"
viewBox="0 0 16 16">
<path
d="M15.502 1.94a.5.5 0 0 1 0 .706L14.459 3.69l-2-2L13.502.646a.5.5 0 0 1 .707 0l1.293 1.293zm-1.75 2.456-2-2L4.939 9.21a.5.5 0 0 0-.121.196l-.805 2.414a.25.25 0 0 0 .316.316l2.414-.805a.5.5 0 0 0 .196-.12l6.813-6.814z" />
<path fill-rule="evenodd"
d="M1 13.5A1.5 1.5 0 0 0 2.5 15h11a1.5 1.5 0 0 0 1.5-1.5v-6a.5.5 0 0 0-1 0v6a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5v-11a.5.5 0 0 1 .5-.5H9a.5.5 0 0 0 0-1H2.5A1.5 1.5 0 0 0 1 2.5v11z" />
</svg>
</button>
<button @click="deleteHandle" class="btn join-item btn-xs">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash-fill"
viewBox="0 0 16 16">
<path
d="M2.5 1a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1H3v9a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4h.5a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H10a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1H2.5zm3 4a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5zM8 5a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7A.5.5 0 0 1 8 5zm3 .5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 1 0z" />
</svg>
</button>
</div>
<div class="indicator-item indicator-middle indicator-start">
<button @click="insertHeadHandle" class="btn btn-xs btn-circle">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-plus-circle-fill" viewBox="0 0 16 16">
<path
d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8.5 4.5a.5.5 0 0 0-1 0v3h-3a.5.5 0 0 0 0 1h3v3a.5.5 0 0 0 1 0v-3h3a.5.5 0 0 0 0-1h-3v-3z" />
</svg>
</button>
</div>
<div class="indicator-item indicator-middle">
<button @click="insertTailHandle" class="btn btn-xs btn-circle">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-plus-circle-fill" viewBox="0 0 16 16">
<path
d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8.5 4.5a.5.5 0 0 0-1 0v3h-3a.5.5 0 0 0 0 1h3v3a.5.5 0 0 0 1 0v-3h3a.5.5 0 0 0 0-1h-3v-3z" />
</svg>
</button>
</div>
</div>
</template>
<script setup lang='ts'>
import { computed, ref } from 'vue';
const isShowEdit = defineModel<boolean>('isShowEdit')
const props = defineProps(['idx'])
const emits = defineEmits(['edit', 'delete', 'insertHead', 'insertTail'])
// Edit handle function
function editHandle() {
emits('edit', props.idx ?? -1);
}
// Delete handle function
function deleteHandle() {
emits('delete', props.idx ?? -1);
}
// Insert head handle function
function insertHeadHandle() {
emits('insertHead', props.idx ?? -1);
}
// Insert tail handle function
function insertTailHandle() {
emits('insertTail', props.idx ?? -1);
}
const modalFormClass = computed(() => ({
"mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50": true,
}))
let modalClass = ref({
'modal-open': false,
'modal': true,
'modal-backdrop': false,
})
</script>
<style scoped></style>

View File

View File

@ -0,0 +1,67 @@
<template>
<div :class="classCompute">
<div v-for="(card, idx) in cards" :key="idx" class="w-full md:w-1/3 p-5">
<div class="indicator w-full">
<slot :card="card" :idx="idx"></slot>
<CardActions :is-show-edit="isShowEdit" @edit="() => { editModalRef.onShow('edit', cards[idx], idx) }"
@delete="removeCard(idx)" @insert-head="insertCardBefore(idx)" @insert-tail="insertCardAfter(idx)" />
</div>
</div>
<EditModal ref="editModalRef" :header="'编辑卡片'" :schema="{ fields: cardSchema }" @submit="submitHandle" />
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import EditModal, { type FieldDefinition } from '@/components/utils/EditModal.vue'
import CardActions from './CardActions.vue'
let cards = defineModel<Array<any>>('cards', { required: true });
let isShowEdit = defineModel<boolean>('isShowEdit', { default: false });
const props = defineProps<{
cardSchema: FieldDefinition[],
checkCallback?: (type: string, data: any, idx: number) => boolean
}>();
const emits = defineEmits(['submit'])
const editModalRef = ref();
const classCompute = computed(() => ({
"container": true,
"md:px-20": true,
"py-5 mx-auto flex flex-wrap md:flex-no-wrap items-center justify-start": true
}));
function submitHandle(type: string, data: any, idx: number) {
if (props.checkCallback && !props.checkCallback(type, data, idx)) {
return;
}
if (type === 'edit') {
cards.value.splice(idx, 1, { ...data });
} else if (type === 'insterHead') {
cards.value.splice(idx, 0, { ...data });
} else if (type === 'insterTail') {
cards.value.splice(idx, 0, { ...data }); // 使idx+1
} else if (type === 'remove') {
cards.value.splice(idx, 1);
}
console.log(cards.value, idx);
}
function removeCard(idx: number) {
submitHandle('remove', {}, idx);
}
function insertCardBefore(idx: number) {
editModalRef.value?.onShow('insterHead', {}, idx);
}
function insertCardAfter(idx: number) {
editModalRef.value?.onShow('insterTail', {}, idx + 1);
}
</script>
<style scoped></style>

View File

@ -0,0 +1,39 @@
<template>
<CardsContaner :checkCallback="checkCallback" :cardSchema="cardSchema" v-model:isShowEdit="isShowEdit" v-model:cards="cards">
<template #="param">
<!-- <Card :="param"/> -->
<component :is="cardType" :="param"/>
</template>
</CardsContaner>
</template>
<script setup lang='ts'>
import CardsContaner from './CardsContaner.vue'
import Card from './Card.vue'
import { schema as CardSchema } from './Card'
import { type FieldDefinition } from '@/components/utils/EditModal.vue'
import { onMounted, ref } from 'vue';
const props = defineProps(['cardType', 'checkCallback'])
let cards = defineModel<Array<any>>('cards', { required: true })
let isShowEdit = defineModel<boolean>('isShowEdit', { default: false });
let cardType:any;
let cardSchema = ref<FieldDefinition[]>([] as FieldDefinition[]);
onMounted(() => {
switch (props.cardType) {
case 'Card':
cardType = Card;
cardSchema.value = CardSchema;
break;
default:
console.log(cardSchema.value);
cardType = undefined;
cardSchema.value = [] as FieldDefinition[];
break;
}
})
</script>
<style scoped></style>

View File

@ -0,0 +1,30 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path v-if="type === 'default'" stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
<path v-else :d="customIconPath" />
</svg>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface IconPaths {
[key: string]: string; // SVG
}
const props = defineProps<{
type: string;
customIconPath?: string;
}>();
//
const defaultIconPath = 'M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z';
//
const customIconPath = computed(() => {
if (props.customIconPath) {
return props.customIconPath;
}
return defaultIconPath;
});
</script>

View File

@ -0,0 +1,99 @@
<template>
<ul class="menu menu-xs bg-base-200 rounded-lg max-w-xs w-full">
<li><a>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /></svg>
resume.pdf
</a></li>
<li>
<details open>
<summary>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z" /></svg>
My Files
</summary>
<ul>
<li><a>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /></svg>
Project-final.psd
</a></li>
<li><a>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /></svg>
Project-final-2.psd
</a></li>
<li>
<details open>
<summary>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z" /></svg>
Images
</summary>
<ul>
<li><a>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" /></svg>
Screenshot1.png
</a></li>
<li><a>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" /></svg>
Screenshot2.png
</a></li>
<li>
<details open>
<summary>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z" /></svg>
Others
</summary>
<ul>
<li><a>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" /></svg>
Screenshot3.png
</a></li>
</ul>
</details>
</li>
</ul>
</details>
</li>
</ul>
</details>
</li>
<li><a>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /></svg>
reports-final-2.pdf
</a></li>
</ul>
</template>
<script setup lang="ts">
// import { ref } from 'vue';
// const FileIcon = defineAsyncComponent(() => import('./FileIcon.vue'));
// //
// interface File {
// name: string;
// type: 'file';
// size: number;
// modifiedDate?: Date;
// }
// interface Folder {
// name: string;
// type: 'folder';
// open?: boolean; //
// children: (File | Folder)[]; //
// }
// // filesFileFolder
// export type FileSystemItem = File | Folder;
// // props
// const props = defineProps<{
// files: FileSystemItem[];
// }>();
// // 使
// const toggleFolder = (folder: Folder) => {
// folder.open = !folder.open;
// };
// const downloadFile = (file: File) => {
// console.log(`Downloading ${file.name}`);
// };
</script>

View File

@ -0,0 +1,99 @@
<template>
<dialog v-if="showModal" :class="['modal', { 'modal-open': showModal }]">
<div class="modal-box">
<h3 class="font-bold text-lg">{{ props.header ?? 'edit' }}</h3>
<form @submit.prevent="handleSubmit">
<div v-for="(field, index) in schema.fields" :key="index" class="mb-4">
<label :for="field.name" class="block text-sm font-medium mb-1">{{ field.label??'label' }}</label>
<component :is="field.type" :value="formData[field.name]??'value'" @input="handleInput($event, field.name)" />
</div>
<div class="flex justify-end">
<button type="button" class="btn btn-error mr-2" @click="showModal=false">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</dialog>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
export interface FieldDefinition {
name: string;
label: string;
type: string;
options?: string[];
}
export interface Props {
header?: string;
schema: { fields: FieldDefinition[] };
initialData?: Record<string, any>;
}
const props = defineProps<Props>();
const emits = defineEmits(['submit']);
const formData = ref({ ...props.initialData });
const showModal = ref(false);
let editType = '';
let argc: any[] = []
const onShow = (type: string = '', data: Record<string, any> = formData.value, ...args: any[]) => {
formData.value = { ...data };
showModal.value = true;
editType = type;
argc = args;
};
const onHide = (type: string = '') => {
showModal.value = false;
editType = type;
formData.value = {}
};
defineExpose({
onShow,
onHide,
})
function handleInput(event: Event, fieldName: string) {
const value = event.target instanceof HTMLInputElement ? event.target.value : null;
formData.value[fieldName] = value;
}
function handleSubmit() {
// Submit logic here, e.g., emit the updated data to parent component
emits('submit', editType, formData.value, argc);
showModal.value = false;
}
onMounted(() => {
// Optionally, you can set up any additional logic here when the component is mounted.
});
</script>
<style scoped>
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: none;
justify-content: center;
align-items: center;
}
.modal-open {
display: flex;
}
.modal-box {
background-color: #fff;
padding: 2rem;
border-radius: 0.5rem;
}
</style>

View File

@ -0,0 +1,52 @@
interface FetchOptions extends RequestInit {
retry?: number;
timeout?: number;
}
export default class FetchWrapper {
static async fetch(url: RequestInfo, options: FetchOptions = {}): Promise<Response> {
const mergedOptions: RequestInit = {
...{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
},
...options,
};
let response: Response | unknown;
let retries = options.retry || 0;
while (true) {
try {
if (options.timeout ?? 0) {
response = await Promise.race([
fetch(url, mergedOptions),
new Promise((_, reject) =>
setTimeout(() =>
reject(new Error(`Request timed out after ${options.timeout}ms`)),
options.timeout)),
]);
} else {
response = await fetch(url, options);
}
if (!(response instanceof Response)) {
throw response;
}
return response;
} catch (error) {
if (retries > 0) {
retries--;
console.debug(`Retrying fetch request... (${retries} retries left)`);
await new Promise(resolve => setTimeout(resolve, 1000));
} else {
throw error;
}
}
}
}
}

View File

@ -0,0 +1,30 @@
<!-- components/Icon.vue -->
<template>
<svg :class="svgClass" :width="size" :height="size" :viewBox="viewBox" aria-hidden="true" focusable="false">
<use :xlink:href="`#${iconName}`"></use>
</svg>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = withDefaults(defineProps<{
name: string,
size?: number | string,
}>(), {
size: 24,
});
const iconName = computed(() => `icon-${props.name}`);
const svgClass = computed(() => `icon icon-${props.name}`);
const viewBox = '0 0 24 24'; // Adjust this based on your icons
</script>
<style scoped>
.icon {
display: inline-block;
vertical-align: middle;
overflow: hidden;
}
</style>

11
src/main.ts Normal file
View File

@ -0,0 +1,11 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
// import PrimeVue from 'primevue/config';
import router from '@/router'
import '@/style.css'
import App from '@/App.vue'
createApp(App)
.use(createPinia())
.use(router)
.mount('#app')

84
src/pages/CardShower.vue Normal file
View File

@ -0,0 +1,84 @@
<template>
<Navbar />
<div>
<span class="badge">TOKEN is</span>
<input type="text" v-model.trim="TOKEN" placeholder="TOKEN" class="input input-ghost input-xs w-full max-w-xs" />
</div>
<CardsManager :checkCallback="checkHandle" cardType="Card" v-model:cards="cardsData" v-model:is-show-edit="isShowEdit"/>
<Footer />
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from "vue"
import Navbar from "@/components/Navbar.vue"
import Footer from "@/components/Footer.vue"
// import axios, { AxiosError } from "axios"
import router from "@/router"
import FetchWrapper from "@/components/utils/FetchWapper";
import type { cardType } from "@/components/cards/Card";
import CardsManager from "@/components/cards/CardsManager.vue";
const props = defineProps(['user_name', 'pages_name', 'token'])
let isShowEdit = ref(false)
let TOKEN = ref("")
onMounted(() => {
FetchWrapper.fetch(`/api/files/json/${props.pages_name}.json`, { timeout: 3000 })
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}).then(body => {
console.log(body)
body.datas.forEach((i:any) => {
const item = {...body.default_item, ...i}
cardsData.value.push({
title: item.title,
body: item.intro,
jumpHerf: item.url
})
})
console.log(cardsData.value)
})
});
let cardsData = ref<cardType[]>([])
function checkHandle() {
return false
}
// watch(cardsData, () => {
// console.log("cards updated")
// }, {
// deep: true
// })
// onMounted(async () => {
// try {
// console.log("axios get")
// const resp = await axios.get(`/api/get/cards/${props.user_name}/${props.pages_name}`, {
// timeout: 100,
// });
// if (resp.status !== 200) {
// console.error(`Server responded with an unexpected status: ${resp.status}`);
// }
// if (resp.data.code === '0000') {
// cardsData = resp.data.data;
// }
// console.log(resp)
// console.log("axios end")
// } catch (error) {
// console.error('Error fetching data:', error);
// // if (axios.isCancel(error)) {
// // console.log('Request canceled');
// // } else if ((error as AxiosError).code === 'ECONNABORTED') {
// // console.log('Connection timed out');
// // }
// }
// })
</script>

110
src/pages/ErrorPage.vue Normal file
View File

@ -0,0 +1,110 @@
<template>
<div :class="['min-h-screen flex items-center justify-center', backgroundColor]">
<div class="text-center">
<h1 :class="errorCodeClass">{{ statusCode }}</h1>
<h2 :class="errorTitleClass">{{ errorTitles[statusCode] || defaultErrorTitle }}</h2> <p :class="errorMessageClass">{{ errorMessages[500] || defaultErrorMessage }}</p>
<button :class="buttonClass" @click="goTo">{{ buttonText }}</button>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
const props = defineProps({
statusCode: {
type: Number,
required: true
},
goToPath: {
type: String,
default: '/'
},
backgroundColor: {
type: String,
default: 'bg-base-200'
},
errorCodeClass: {
type: String,
default: 'text-9xl font-bold text-error'
},
errorTitleClass: {
type: String,
default: 'text-5xl font-semibold mb-4'
},
errorMessageClass: {
type: String,
default: 'text-xl mb-8'
},
buttonClass: {
type: String,
default: 'btn btn-primary'
},
buttonText: {
type: String,
default: 'Go Home'
}
});
const errorTitles = ref<{[key: number]: string}>({
200: 'OK',
201: 'Created',
204: 'No Content',
301: 'Moved Permanently',
302: 'Found (Temporary Redirect)',
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'Page Not Found',
405: 'Method Not Allowed',
408: 'Request Timeout',
409: 'Conflict',
410: 'Gone',
413: 'Payload Too Large',
415: 'Unsupported Media Type',
422: 'Unprocessable Entity',
429: 'Too Many Requests',
500: 'Internal Server Error',
501: 'Not Implemented',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Timeout',
});
const errorMessages = ref({
200: 'The request has succeeded.',
201: 'The request has been fulfilled and resulted in a new resource being created.',
204: 'The server successfully processed the request and is not returning any content.',
301: 'This and all future requests should be directed to the given URI.',
302: 'This is an alias for 301 Moved Permanently due to historical reasons.',
400: 'The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).',
401: 'The request requires user authentication.',
403: 'The server understood the request but refuses to authorize it.',
404: 'The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.',
405: 'The method specified in the Request-Line is not allowed for the resource identified by the Request-URI.',
408: 'The client did not produce a request within the time that the server was prepared to wait.',
409: 'The request could not be completed due to a conflict with the current state of the resource.',
410: 'The requested resource is no longer available at the server and no forwarding address is known.',
413: 'The server is refusing to process a request because the request entity is larger than the server is willing or able to process.',
415: 'The server refuses to accept the request because the payload format is in an unsupported format.',
422: 'When the instance given in the request is well-formed but unable to be followed due to semantic errors.',
429: 'The user has sent too many requests in a given amount of time ("rate limiting").',
500: 'A generic error message, given when an unexpected condition was encountered and no more specific message is suitable.',
501: 'The server either does not recognize the request method, or it lacks the ability to fulfill the request.',
502: 'The server, while acting as a gateway or proxy, received an invalid response from the upstream server it accessed in attempting to fulfill the request.',
503: 'The server is currently unable to handle the request due to a temporary overloading or maintenance of the server.',
504: 'The server, while acting as a gateway or proxy, did not receive a timely response from the upstream server specified by the URI.',
});
const defaultErrorTitle = 'An Error Occurred';
const defaultErrorMessage = 'We are unable to display the requested page. Please try again later.';
const router = useRouter();
const goTo = () => {
router.push(props.goToPath);
};
</script>
<style scoped>
/* You can add any additional styles here */
</style>

113
src/pages/Files.vue Normal file
View File

@ -0,0 +1,113 @@
<template>
<Navbar />
<div :class="mainClass">
<input @change="fileInput" type="file" class="file-input file-input-bordered w-full max-w-xs" />
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
文件名
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
大小
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
修改日期
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
操作
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<!-- 这里将被动态渲染 -->
<tr v-for="file in files" :key="file.id">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<!-- 直接使用SVG占位符图标 -->
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 text-gray-500">
<path
d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" />
</svg>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">
{{ file.name }}
</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="text-sm text-gray-500">{{ file.size }}</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="text-sm text-gray-500">{{ file.modifiedDate }}</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<a href="#" class="text-indigo-600 hover:text-indigo-900">下载</a>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup lang="ts">
import Navbar from '@/components/Navbar.vue';
import { computed, ref, watch } from 'vue';
function fileInput(event: Event) {
const target = event.target as HTMLInputElement;
if (target.files && target.files.length > 0) {
const files = Array.from(target.files);
console.log(files); //
//
}
}
const mainClass = computed(() => ({
"container mx-auto": true,
"flex flex-col": true,
"overflow-x-auto sm:-mx-6 lg:-mx-8": false,
"py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8": false,
"shadow overflow-hidden border-b border-gray-200 sm:rounded-lg": false,
}))
//
const files = ref([
{
id: 1,
name: 'Documents',
isFolder: true, //
//
},
{
id: 2,
name: 'example.txt',
isFolder: false,
size: '1.5MB',
modifiedDate: '2022-01-01',
},
// ...
]);
//
// 1.
// 2.
// 3.
// 4.
//
// async function fetchFiles() {
// const response = await axios.get('/api/files');
// files.value = response.data;
// }
// fetchFiles()
// onMounted(() => {
// fetchFiles();
// });
</script>
<style scoped>
/* 根据需要添加自定义样式 */
</style>

86
src/pages/Home.vue Normal file
View File

@ -0,0 +1,86 @@
<template>
<Navbar />
<Hero />
<!-- <RouterLink :to="{ name: 'file' }" class="btn">goToFiles</RouterLink> -->
<!-- <button class="btn" @click="async ()=>{
let res = FetchWrapper.fetch('https://www.zzyxyz.com/api/files/json/index-content.json'
, {mode: 'no-cors'}
);
res.then(res=>{
console.log(res)
})
}">BTN</button> -->
<div>
<span class="badge">TOKEN is</span>
<input type="text" v-model.trim="TOKEN" placeholder="TOKEN" class="input input-ghost input-xs w-full max-w-xs" />
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">isShowEdit</span>
<input type="checkbox" v-model="isShowEdit" class="checkbox" />
</label>
</div>
<CardsManager :checkCallback="checkHandle" cardType="Card" v-model:cards="cards" v-model:is-show-edit="isShowEdit" />
<Footer />
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from "vue"
import Navbar from "@/components/Navbar.vue"
import Hero from "@/components/Hero.vue"
import Footer from "@/components/Footer.vue"
import { type cardType } from "@/components/cards/Card"
import CardsManager from "@/components/cards/CardsManager.vue"
import router from "@/router"
import FetchWrapper from "@/components/utils/FetchWapper"
let isShowEdit = ref(false)
let TOKEN = ref("")
watch(TOKEN, () => {
console.log(TOKEN.value)
if (TOKEN.value) {
isShowEdit.value = true
}
})
function checkHandle() {
return false
}
onMounted(() => {
FetchWrapper.fetch("/api/files/json/index-content.json", { timeout: 3000 })
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}).then(body => {
body.datas.forEach((i: cardType) => {
const item = {...body.default_item, ...i}
cards.value.push({
title: item.title,
body: item.intro,
jumpHerf: item.url
})
})
})
});
let cards = ref<cardType[]>([
{
title: "404",
body: "test",
jumpHerf: "/404",
imgSrc: null,
imgAlt: null,
btnText: null,
btnHref: null,
},
])
// watch(myItems, () => {
// console.log("cards updated")
// }, {
// deep: true
// })
</script>

54
src/pages/Users.vue Normal file
View File

@ -0,0 +1,54 @@
<template>
<div class="container">
<h1 class="mb-4">Cards</h1>
<div class="row justify-content-between">
<div v-for="(card, index) in cards" :key="index" class="col-md-3 mb-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">{{ card.title }}</h5>
<p class="card-text">{{ card.description }}</p>
<div class="d-flex justify-content-between align-items-center">
<button @click="editCard(index)" class="btn btn-primary">Edit</button>
<button @click="deleteCard(index)" class="btn btn-danger">Delete</button>
</div>
</div>
</div>
</div>
</div>
<button @click="addCard" class="btn btn-success mt-4">Add Card</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { } from 'pinia';
// Define initial cards
const cards = ref([
{ title: 'Card 1', description: 'Description for card 1' },
{ title: 'Card 2', description: 'Description for card 2' },
{ title: 'Card 3', description: 'Description for card 3' },
]);
// Use Pinia store (assuming you've set up a store with a cards property)
// const store = useStore('yourStore'); // Replace 'yourStore' with your actual store name
// Object.assign(cards, store.state.cards);
// Functions
function addCard() {
cards.value.push({ title: '', description: '' });
}
function editCard(index: number) {
// Implement your edit logic here, e.g. open a modal to update the card
console.log(`Editing card at index ${index}`);
}
function deleteCard(index: number) {
cards.value.splice(index, 1);
// store.commit('removeCard', index); // Assuming you have a removeCard mutation in your Pinia store
}
</script>
<style scoped>
</style>

43
src/router/index.ts Normal file
View File

@ -0,0 +1,43 @@
import { createRouter, createWebHistory } from "vue-router"
const router = createRouter({
history: createWebHistory(),
routes: [
{
name: "root",
path: "/",
redirect: "/home",
},
{
name: "home",
path: "/home",
component: () => import("@/pages/Home.vue"),
},
{
name: "files",
path: "/files",
component: () => import("@/pages/Files.vue"),
},
{
name: "users",
path: "/users",
component: () => import("@/pages/Users.vue"),
},
{
name: "cardShower",
path: "/card/:user_name/:pages_name/:token?",
component: () => import("@/pages/CardShower.vue"),
props: true,
},
{
name: 'notFound',
path: "/:any*", // 使用正则表达式匹配所有路径
component: () => import('@/pages/ErrorPage.vue'),
props() {
return { statusCode: 404 }
},
},
],
});
export default router

3
src/style.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

15
tailwind.config.js Normal file
View File

@ -0,0 +1,15 @@
/** @type {import('tailwindcss').Config} */
import daisyui from "daisyui"
export default {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [daisyui],
}

14
tsconfig.app.json Normal file
View File

@ -0,0 +1,14 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

11
tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

19
tsconfig.node.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "@tsconfig/node20/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

16
vite.config.ts Normal file
View File

@ -0,0 +1,16 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})