This commit is contained in:
AaronHux 2024-06-30 21:39:37 +08:00
parent 61ef28c83e
commit bd27f18e4c
746 changed files with 80780 additions and 0 deletions

1
.anima/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
cache

41
.dockerignore Normal file
View File

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
.solid
.turbo
# /dist/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

10
.eslintrc.js Normal file
View File

@ -0,0 +1,10 @@
module.exports = {
root: true,
// This tells ESLint to load the config from the package `eslint-config-custom`
extends: ['@myturborepo/eslint-config-custom'],
settings: {
next: {
rootDir: ['apps/*/'],
},
},
}

46
.gitignore vendored Normal file
View File

@ -0,0 +1,46 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
.pnp
.pnp.js
# testing
coverage
# next.js
.next/
out/
build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
**/.env
# turbo
.turbo
# IDEs
.idea
# nuxt
.output
# histoire
.histoire
# vitepress
**/.vitepress/dist/**

2
.npmrc Normal file
View File

@ -0,0 +1,2 @@
shamefully-hoist=true
strict-peer-dependencies=false

18
.release-it.json Normal file
View File

@ -0,0 +1,18 @@
{
"git": {
"commitMessage": "chore: release v${version}"
},
"github": {
"release": true,
"releaseName": "v${version}"
},
"npm": {
"publish": false
},
"plugins": {
"@release-it/conventional-changelog": {
"preset": "conventionalcommits",
"infile": "CHANGELOG.md"
}
}
}

16
CHANGELOG.md Normal file
View File

@ -0,0 +1,16 @@
### [0.0.5](https://github.com/gurvan-guss/turborepo-nuxt-boilerplate/compare/0.0.4...0.0.5) (2022-10-18)
### [0.0.4](https://github.com/gurvan-guss/turborepo-nuxt-boilerplate/compare/0.0.3...0.0.4) (2022-10-18)
### [0.0.3](https://github.com/gurvan-guss/turborepo-nuxt-boilerplate/compare/0.0.2...0.0.3) (2022-10-18)
### [0.0.2](https://github.com/gurvan-guss/turborepo-nuxt-boilerplate/compare/0.0.1...0.0.2) (2022-10-18)
### 0.0.1 (2022-10-18)
### Reverts
* Revert "cd: bump to node 18" ([b11ebe4](https://github.com/gurvan-guss/turborepo-nuxt-boilerplate/commit/b11ebe469bf62bfa9642da00facb2d9394d3f09e))

15
Dockerfile Normal file
View File

@ -0,0 +1,15 @@
FROM node:20
WORKDIR /app
COPY . /app
RUN npm config set registry https://registry.npmmirror.com
RUN npm install -g pnpm
RUN pnpm config set registry https://registry.npmmirror.com/
RUN pnpm i
RUN pnpm build:web
EXPOSE 3000
CMD [ "pnpm" ,"preview:web" ]

15
apps/docs/package.json Normal file
View File

@ -0,0 +1,15 @@
{
"name": "docs",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vitepress dev pages",
"build": "vitepress build pages",
"serve": "vitepress serve pages"
},
"devDependencies": {
"@myturborepo/ui": "workspace:*",
"vitepress": "1.0.0-beta.1",
"vue": "^3.3.4"
}
}

View File

@ -0,0 +1,3 @@
{
"type": "module"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,22 @@
export default {
title: 'VitePress',
description: 'Just playing around with turborepo',
themeConfig: {
siteTitle: 'My Custom Title',
nav: [
{ text: 'Index', link: '/index' },
{ text: 'Getting started', link: '/getting-started' },
{ text: 'Github', link: 'https://' },
],
sidebar: [
{
text: 'Guide',
items: [
{ text: 'Index', link: '/index' },
{ text: 'Getting started', link: '/getting-started' },
],
},
],
},
}

View File

@ -0,0 +1,9 @@
import DefaultTheme from 'vitepress/theme'
import MyButton from '@myturborepo/ui/components/Button.vue'
export default {
...DefaultTheme,
enhanceApp({ app }) {
app.component('MyButton', MyButton)
},
}

View File

@ -0,0 +1 @@
# Getting started

2
apps/docs/pages/index.md Normal file
View File

@ -0,0 +1,2 @@
# Hello VitePress
<MyButton />

9
apps/web/.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
node_modules
*.log*
.nuxt
.nitro
.cache
.output
.env
dist
.netlify

42
apps/web/README.md Normal file
View File

@ -0,0 +1,42 @@
# Nuxt 3 Minimal Starter
Look at the [nuxt 3 documentation](https://v3.nuxtjs.org) to learn more.
## Setup
Make sure to install the dependencies:
```bash
# yarn
yarn install
# npm
npm install
# pnpm
pnpm install --shamefully-hoist
```
## Development Server
Start the development server on http://localhost:3000
```bash
npm run dev
```
## Production
Build the application for production:
```bash
npm run build
```
Locally preview production build:
```bash
npm run preview
```
Checkout the [deployment documentation](https://v3.nuxtjs.org/guide/deploy/presets) for more information.

View File

@ -0,0 +1,24 @@
export interface AccessKeyInfo {
AccessKey: string
SecretKey: string
Expiration: number
}
export interface AccessKeyCreate {
// 过期时间
Expiration: number
}
export const accessKeyApi = {
list: (params: Page.Request) => request.get<Page.Response<AccessKeyInfo>>('access_key/list', params),
show: (params: AccessKeyInfo) => request.get<{
SecretKey: string
}>('access_key/show', params),
create: (params: AccessKeyCreate) => request.post('access_key/save', params),
delete: (params: AccessKeyInfo) => {
return request.post('access_key/change/status', {
AccessKey: params.AccessKey,
Status: 3,
})
},
}

98
apps/web/api/auth.ts Normal file
View File

@ -0,0 +1,98 @@
export const authApi = {
captcha: () => request.get<{
imgByte: string
uuid: string
}>('captcha'),
auditors: () => request.get<{
Id: string
UserName: string
}[]>('sys_user_audit_list'),
sendCode(params: { Phone?: string; Email?: string }) {
return request.post('dep_send_login_code', params)
},
sendSafeCode(params: { Type?: 'phone' | 'totp' | 'email' }) {
return request.post('dep_person/send/check/code', params)
},
loginSafeCheck(params: {
Code: string
UserName?: string
Phone?: string
Email?: string
}) {
return request.post<{
Token: string
}>('dep_login_code', params)
},
loginByDevice(params: {
Email?: string
Phone?: string
UserName?: string
Code: string
}) {
return request.post<{
Token: string
Email?: string
Phone?: string
}>('dep_login_code', params)
},
callbackSSO: (sso: string, params: { Code: string }) => {
return request.post<{ Token: string }>(`dep_login/ssocallback/${sso}`, params)
},
loginByOauth: async (sso: 'ihep' | 'carsipre', state: string) => {
const res = await request.get<{
RedirectURL: string
}>(`dep_login/sso/${sso}`, {
state,
})
window.open(res.RedirectURL)
},
login(params: {
UserName: string
Password: string
Code: string
UUID: string
}) {
return request.post<{
Token: string
Email?: string
Phone?: string
UserName?: string
}>('dep_login', params)
},
register: (params: Partial<ReigsterParams>) => request.post('dep_register', params),
postSafeCheck(params: { Code: string; CodeType: string }) {
return request.post('dep_person/safe/check', params)
},
postIsSafe() {
return request.post<boolean>('dep_person/is/safe')
},
}
export interface ReigsterParams {
/**
*
*/
Code: string
/**
* Email
*/
Email: string
/**
*
*/
Password: string
/**
* ID
*/
SysUserId?: string
/**
*
*/
UserName: string
/**
* Uuid
*/
Uuid: string
RandomPass?: boolean
}

205
apps/web/api/bucket.ts Normal file
View File

@ -0,0 +1,205 @@
export interface BucketInfo {
FileNum: number
Mtime: number
Name: string
Path: string
QuotaSize: number
TotalSize: number
}
export interface BucketCreateAndEditParams {
BucketName: string
BucketLimitSize: number
}
export interface FileInfo {
Name: string
FileSize: number
Type: string
Mtime: number
Path: string
DownloadUrl: string
}
export interface FileEditParams {
OldFileName: string
NewFileName: string
}
export interface CreateDirParams {
DirName: string
}
/**
*
*/
export interface FileinfoData {
ChunkList?: FileinfoDataChunkList
/**
*
*/
Ctime?: number
/**
*
*/
Mtime?: number
/**
*
*/
Name?: string
/**
*
*/
Path?: string
/**
*
*/
Size?: number
/**
*
*/
Type?: string
[property: string]: any
}
/**
* jwanfs-web-server.server.api.v1.ChunkList
*/
export interface FileinfoDataChunkList {
/**
*
*/
Chunks?: FileinfoDataChunkInfo[]
/**
*
*/
Total?: number
[property: string]: any
}
/**
* jwanfs-web-server.server.api.v1.ChunkInfo
*/
export interface FileinfoDataChunkInfo {
/**
* ID
*/
ChunkID?: string
/**
* KB
*/
ChunkSize?: number
/**
*
*/
DataCenterTags?: string[]
/**
*
*/
InternalURL?: string[]
/**
*
*/
ServerTags?: string[]
[property: string]: any
}
export interface RecycledInfo {
/**
*
*/
DeleteTime: number
/**
*
*/
FileSize: number
/**
*
*/
IsDirectory: boolean
/**
* Grpc
*/
IsFromGrpc: boolean
/**
*
*/
Mtime: number
/**
*
*/
Name: string
/**
*
*/
Path: string
/**
* Grpc客户端进程号
*/
PID: string
/**
*
*/
RecycleEffectTime: number
/**
*
*/
Status: number
/**
*
*/
Type: string
}
export const bucketApi = {
list: (params: Page.Request) => request.get<Page.Response<BucketInfo>>('file_manage/bucket', params),
create: (params: BucketCreateAndEditParams) => request.post(`file_manage/bucket/${params.BucketName}`, {
BucketLimitSize: params.BucketLimitSize,
}),
edit: (params: BucketCreateAndEditParams) => request.put(`file_manage/bucket/${params.BucketName}`, {
BucketLimitSize: params.BucketLimitSize,
}),
delete: (BucketName: string) => request.delete(`file_manage/bucket/${BucketName}`),
detail: {
list: (params: Page.Request, path: string) => request.get<Page.Response<FileInfo>>(`file_manage/file/${path}`, params),
delete: (path: string) => request.delete(`file_manage/file/${path}`),
edit: (params: FileEditParams, path: string) => request.put(`file_manage/file/${path}/${params.OldFileName}`, params),
createDir: (params: CreateDirParams, path: string) => request.post(`file_manage/folder/${path}/${params.DirName}`),
upload: (file: File, url: string, token: string) => {
const formData = new FormData()
formData.append('File', file)
formData.append('Size', file.size.toString())
return request.upload(`${url}?Token=${token}`, formData, token)
},
info: (path: string) => request.get<FileinfoData>(`file_manage/fileinfo/${path}`),
uploadUrl: (params: {
Path: string
}) => request.get<{
token: string
url: string
endpoint: string
}>('file_manage/get/upload/info', params),
download: (params: FileInfo) => {
request.get<{
Url: string
}>(`file_manage/download/${params.Path}`).then((res) => {
window.open(res.Url)
})
// "/file_manage/download"
// window.open(`${params.DownloadUrl}`)
},
downloadUrl: (params: FileInfo) => {
return (`${params.DownloadUrl}`)
},
},
recycle: {
list: (params: Page.Request & { BucketName: string }) => request.get<Page.Response<RecycledInfo>>('recycle_file/list', params),
delete: (params: RecycledInfo) => request.post<{
ShareUrl: string
}>('recycle_file/delete', params),
restore: (params: RecycledInfo) => request.post<{
ShareUrl: string
}>('recycle_file/restore', params),
},
}

47
apps/web/api/common.ts Normal file
View File

@ -0,0 +1,47 @@
export const commonApi = {
categorys: () => request.get<CategoryInfo>('public/class'),
resources: (params: ResourceRequest & {
ShopName?: string
}) => request.get<Page.Response<ResourceInfo>>('public/resource', params),
resourceDetail: (params: {
ID?: string
Path?: string
Fgw?: string
}) => request.get<ResourceInfo>('public/resource/detail', params),
storeDetail: (params: {
ShopName: string
}) => request.get<StoreInfo>('public/shop/detail', params),
}
export interface CategoryInfo {
/**
*
*/
Class: ClassInfo[]
/**
*
*/
Tags: TagInfo[]
}
export interface ClassInfo {
/**
* ID
*/
ID: string
/**
*
*/
Name: string
}
export interface TagInfo {
/**
* ID
*/
ID: string
/**
*
*/
Name: string
}

44
apps/web/api/dashboard.ts Normal file
View File

@ -0,0 +1,44 @@
export interface Overview {
AllocateBucketSize: number
BuketSurplusSize: number
FileNum: number
MonthFileSize: number
QuotaSize: number
TotalSize: number
CurrentBucketNum: number
BucketSurplusQuotaSize: number
}
export interface Storage {
Name: string
FileNum: number
TotalSize: number
QuotaSize: number
UsagePercent: number
DocumentTypeSize: number
MusicTypeSize: number
PictureTypeSize: number
VedioTypeSize: number
OtherTypeSize: number
}
export const dashboardApi = {
overview() {
return request.get<Overview>('dep_dashboard/topic1')
},
storage(params: {
BucketName?: string
isAll?: number
}) {
return request.get<Storage>('dep_dashboard/topic3', params)
},
storageBucket() {
return request.get<
{
Name: string
TotalSize: number
QuotaSize: number
}[]
>('dep_dashboard/topic2')
},
}

7
apps/web/api/filer.ts Normal file
View File

@ -0,0 +1,7 @@
export interface FilerInfo { Name: string; Address: string; ID: string | number; OSSAddress: string }
export const filerApi = {
list: () => request.get<FilerInfo[]>(
'dep_dashboard/filer/list',
),
}

36
apps/web/api/log.ts Normal file
View File

@ -0,0 +1,36 @@
export interface LogInfo {
/**
*
1-
2-
3-
4-
*/
LogType: string
/**
*
*/
LogTypeContent: string
/**
*
*/
Mtime: string
/**
*
*/
ObjectName: string
/**
*
*/
ObjectType: string
ObjectSize: number
/**
*
*/
OwnerID: string
}
export const logApi = {
list: (params: Page.Request) => request.get<Page.Response<LogInfo>>('filer_log/list', params),
}

View File

@ -0,0 +1,93 @@
export const baseURL = {
v1: '/api/v1/',
}
async function post<T>(method: 'POST' | 'PATCH' | 'PUT', url: string, data?: any, opts: { [k: string]: any } = { headers: {} }) {
const headers = opts.headers ?? {}
const res = await $fetch<HTTP.Response<T>>(baseURL.v1 + url, {
method,
body: data,
...opts,
headers: {
UToken: storage.GetItem('UToken'),
...headers,
},
})
// 第三方登录跳转至需要注册(信息补充)
if (res.Code === 401) {
navigateTo(`/login/complete?CarsiCode=${(res as HTTP.Response<T, 401>).Data.Token}`, {
replace: true,
})
}
if (res.Code === 13) {
storage.RemoveItem('UToken')
navigateTo('/login')
}
// 安全验证
if (res.Code === 60)
throw res.Code
if (res.Code !== 0)
throw new Error(res.Msg)
return res.Data
}
async function get<T>(method: 'DELETE' | 'GET', url: string, data?: any) {
const res = await $fetch<HTTP.Response<T>>(baseURL.v1 + url, {
method,
query: data,
// @ts-expect-error
headers: {
UToken: storage.GetItem('UToken'),
},
})
// 第三方登录跳转至需要注册(信息补充)
if (res.Code === 401) {
navigateTo(`/register?CarsiCode=${(res as HTTP.Response<T, 401>).Data.Token}`, {
replace: true,
})
}
if (res.Code === 13) {
storage.RemoveItem('UToken')
navigateTo('/login')
}
// 安全验证
if (res.Code === 60)
throw res.Code
if (res.Code !== 0)
throw new Error(res.Msg)
return res.Data
}
export const request = {
post: <T>(url: string, data?: any) => post<T>('POST', url, data),
patch: <T>(url: string, data?: any) => post<T>('PATCH', url, data),
put: <T>(url: string, data?: any) => post<T>('PUT', url, data),
get: <T>(url: string, data?: any) => get<T>('GET', url, data),
delete: <T>(url: string, data?: any) => get<T>('DELETE', url, data),
upload: <T>(url: string, data?: any, token = '') => {
// $fetch 上传会报错
return fetch(url, {
method: 'post',
body: data,
headers: {
token,
},
})
.then((response) => {
if (response.ok)
return response.json() as T
else
throw new Error(`${response.status} : ${response.statusText}`)
})
},
}

123
apps/web/api/resource.ts Normal file
View File

@ -0,0 +1,123 @@
import type { FileInfo } from './bucket'
// 资源属性
export interface ResourceInfo {
BucketName: string
ClassID: string
ClassName: string
Createtime: number
Description: string
Document: string
EffectiveTime: number
Ico: string
ID: string
IsEnable: boolean
Name: string
Download: number
OwnerID: string
DownloadUrl: string
ShopID: string
ShopName?: string
FileInfo: FileInfo[]
Tags?: string[]
Type?: string
}
// 资源创建
export interface ResourceCreate {
Type: string
Ico: File
Background: File
Name: string
Path: string
BucketName: string
Description: string
IsEnable: boolean
ClassID: string
Tags: string[]
Document: string
ShopID: string
}
// 资源修改
export interface ResourceUpdate extends Partial<ResourceCreate> {
ResourceID: string
}
export interface ResourceRequest {
/**
*
*/
ClassName?: string
/**
*
*/
DepId?: string
/**
*
*/
Name?: string
/**
*
*/
Page?: number
/**
*
*/
PageSize?: number
/**
*
*/
ShopName?: string
/**
* Name,使TotalSize,FileNum,QuotaSize,
*/
SortName?: string
/**
* asc,desc
*/
SortOrder?: string
/**
*
*/
Tags?: string[]
}
export interface ResourceDashboard {
ID: string
Name: string
ResourceFile: ResourceInfo[]
}
export const resourceApi = {
dashboard: () => request.get<Page.Response<ResourceDashboard>>('user/data/resource/dashboard'),
list: (params: ResourceRequest & {
ShopName?: string
}) => request.get<Page.Response<ResourceInfo>>('user/data/resource', params),
detail: (params: {
ID?: string
Path?: string
}) => request.get<ResourceInfo>('user/data/resource/detail', params),
// 添加资源
create: (params: ResourceCreate) => {
const data = paramsTool.toFormData(params)
return request.post('user/data/resource', data)
},
download: (params: {
DownloadUrl: string
}) => {
window.open(`${params.DownloadUrl}`)
},
// 删除资源
delete: (data: ResourceInfo) => request.delete('user/data/resource', {
ID: data.ID,
}),
resolution: (src?: string, style: 'st1' | 'st2' | 'st3' = 'st1', refresh?: boolean) => src ? `${src}&style=${style}${refresh ? `&timestamp=${new Date().getTime()}` : ''}` : '/null.webp',
// 更新资源
update: (params: ResourceUpdate) => {
const data = paramsTool.toFormData(params)
return request.patch('user/data/resource', data)
},
// 标签列表
tags: () => request.get<string[]>('/resource/tags'),
}

21
apps/web/api/share.ts Normal file
View File

@ -0,0 +1,21 @@
export interface ShareInfo {
Id: any
Name: string
ShareUrl: string
Status: number
ExploreTime: number
}
export interface ShareCreate {
Path: string
Expire: number
ShareFilePassword?: string
IsEncrypt: boolean
}
export const shareApi = {
list: (params: Page.Request) => request.get<Page.Response<ShareInfo>>('file_share/list', params),
create: (params: ShareCreate) => request.post<{ ShareUrl: string }>('file_share/save', params),
delete: (params: ShareInfo) => request.post<{ ShareUrl: string }>('file_share/delete', params),
shareUrl: (params: ShareInfo) => `${window.location.origin}${baseURL.v1}file_manage/proxyfgw/${params.ShareUrl}`,
}

81
apps/web/api/store.ts Normal file
View File

@ -0,0 +1,81 @@
// 店铺属性
export interface StoreInfo {
Background: string
ClassID: string
ClassName: string
Createtime: number
Ico: string
Mark: string
Name: string
OwnerID: string
ShopID: string
Tags: string[]
}
// 店铺创建属性
export interface StoreCreate {
Ico: File
ShopName: string
Background: File
ClassID: string
Tags: string[]
Mark: string
}
export interface StoreListQuery {
/**
*
*/
ClassName?: string
/**
*
*/
DepId?: string | number
/**
*
*/
Name?: string
/**
*
*/
Page?: number
/**
*
*/
PageSize?: number
/**
* Name,使TotalSize,FileNum,QuotaSize,
*/
SortName?: string
/**
* asc,desc
*/
SortOrder?: string
/**
*
*/
Tags?: string[]
ShopName?: string
}
export const storeApi = {
// 店铺列表
list: (params: StoreListQuery) => request.get<Page.Response<StoreInfo>>('user/manage/shop', params),
detail: (params: {
ShopName: string
}) => request.get<StoreInfo>('user/manage/shop/detail', params),
// 添加店铺
create: (params: StoreCreate) => {
const data = paramsTool.toFormData(params)
return request.post('user/manage/shop', data)
},
// 删除店铺
delete: (params: StoreInfo) => request.delete<StoreInfo>('user/manage/shop', params),
// 修改店铺
update: (params: StoreCreate & { ShopID: string }) => {
const data = paramsTool.toFormData(params)
return request.patch('user/manage/shop', data)
},
official: () => request.get<Page.Response<StoreInfo>>('user/manage/sysshop'),
}

117
apps/web/api/test.ts Normal file
View File

@ -0,0 +1,117 @@
export interface ViewCard {
src: any
id?: string
name?: string
star?: boolean
backgroundColor?: string
[attr: string]: any
}
/*
* @Description:
* @Version: 2.0
* @Author: Yaowen Liu
* @Date: 2021-10-14 13:34:56
* @LastEditors: Yaowen Liu
* @LastEditTime: 2023-09-21 09:23:25
*/
// import type { ViewCard } from '../lib/types/waterfall'
/**
* ID
* @param {*} length
* @returns
*/
export function randomID(length = 6) {
return Number(Math.random().toString().substr(3, length) + Date.now()).toString(36)
}
const COLORS = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399']
// const NAMES = [
// '小当家',
// '樱木花道',
// '木之本樱',
// '小可',
// '水冰月',
// '哆啦A梦',
// '大雄',
// '项少羽',
// '天明',
// '月儿',
// '石兰',
// '夏尔凡多姆海恩',
// '塞巴斯蒂安',
// '亚伦沃克',
// '皮卡丘',
// '鸣人',
// '宇智波佐助',
// '旗木卡卡西',
// '喜洋洋',
// '灰太狼',
// '爱德华',
// '阿冈',
// '黑崎一护',
// '路飞',
// '索隆',
// '山治',
// '恋次',
// '越前龙马',
// ]
function getRandomNum(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min
}
function randomColor() {
return COLORS[getRandomNum(0, 4)]
}
// function randomName() {
// return NAMES[getRandomNum(0, 25)]
// }
// let start = 100
// export function getList(pageSize = 10) {
// const end = start + pageSize
// const list: ViewCard[] = []
// for (let i = start; i <= end; i++) {
// const successURL = `https://api.mz-moe.cn/img/img${i}.jpg`
// // const successURL = `https://images.weserv.nl/?url=https://api.mz-moe.cn/img/img${i}.jpg?timestamp=${Date.now()}`
// const errorURL = 'https://api.mz-moe.cn/img/img00000.jpg'
// list.push({
// id: randomID(),
// star: false,
// src: {
// original: Math.random() < 0.95 ? successURL : errorURL,
// },
// backgroundColor: randomColor(),
// name: randomName(),
// })
// }
// start = end + 1
// return list
// }
const website = 'https://www.getphotoblanket.com'
// const website = 'https://www.getphotoblanket.com';
export function getList({ page = 1, pageSize = 20 }) {
const url = `${website}/products.json?page=${page}&limit=${pageSize}`
return fetch(url)
.then(res => res.json())
.then(res => res.products).then((res) => {
return res.map((item: any) => {
return {
id: randomID(),
star: false,
price: item.variants[0].price,
src: {
original: Math.random() > 0.1 ? item.images[0].src : 'https://www.example.com/non-existent-image.jpg',
// original: 'https://tq-alg-public.s3.us-west-2.amazonaws.com/kol/Seraphina_1702987997_0.png',
},
backgroundColor: randomColor(),
name: item.title,
}
})
})
}

93
apps/web/api/user.ts Normal file
View File

@ -0,0 +1,93 @@
export interface UserSendCode {
Phone?: string
Email?: string
Type: 'phone' | 'email'
}
export const userApi = {
info: () => request.get<UserInfo>('dep_person/info'),
edit: (params: UserEdit) => request.post('dep_person/update', params),
sendCode: (params: UserSendCode) => request.post('dep_person/send/new/info/code', params),
qrTotpCode: () => request.get<{
Qr: string,
TotpSecret: string
}>('dep_person/reload/totp'),
bindSSO:(sso:string,isBind?:boolean)=>{
if(!isBind){
return request.post<{ Token: string }>(`dep_login/bindcallback/${sso}`)
}else{
return request.post<{ Token: string }>(`dep_login/unbindcallback/${sso}`)
}
},
}
export interface UserEdit {
NickName?: string
/**
* ,1
*/
ClearTotp?: number;
/**
*
*/
Code?: string;
/**
*
*/
Email?: string;
/**
* Id
*/
Id?: string;
/**
* 是否安全认证:1-,2-
*/
IsSafeCheck?: number;
/**
*
*/
OldPassword?: string;
/**
*
*/
Password?: string;
/**
*
*/
Phone?: string;
/**
*
*/
ProtocolPassword?: string;
/**
*
*/
TotpCode?: string;
/**
*
*/
TotpSecret?: string;
}
export interface UserInfo {
CreateTime: number;
Email: string;
Id: number;
IsAdmin: number;
NickName: string;
Status: number;
UpdateTime: number;
DepId: number;
IsSafeCheck: number;
IsTotp: number;
Phone: string;
status: number;
IsBindIHEPEmail: number;
IsBindBeiHang: number
IsBindCarsiUID:number;
}

70
apps/web/app.vue Normal file
View File

@ -0,0 +1,70 @@
<script setup lang="ts">
useHead({
title: 'NuxTya',
meta: [
{
name: 'NuxTya | Nuxt 3 Starter Template',
content:
'A Nuxt 3 starter template with TypeScript, Tailwind CSS, Shadcn-vue and Pinia.',
},
],
bodyAttrs: {
class: 'nuxt-tya',
},
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.svg' }],
})
function setScale() {
const designWidth = 2560// 稿
const designHeight = 1440// 稿
const scale = document.documentElement.clientWidth / document.documentElement.clientHeight < designWidth / designHeight
? (document.documentElement.clientWidth / designWidth)
: (document.documentElement.clientHeight / designHeight)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
document.querySelector('#screen').style.transform = `scale(${scale}) translate(-50%)`
}
onMounted(() => {
setScale()
})
window.onresize = function () {
setScale()
}
</script>
<template>
<div class="screen-wrapper">
<div id="screen">
<NuxtLoadingIndicator />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<Sonner />
</div>
</div>
</template>
<style>
body {
background-image: linear-gradient(145deg, #070a10 10%, #070a10 70%);
}
.screen-wrapper {
height: 100vh;
width: 100vw;
overflow: hidden;
}
.screen-wrapper #screen {
display: inline-block;
width: 2560px;
height: 1440px;
overflow: hidden;
transform-origin: 0 0;
position: absolute;
left: 50%;
}
</style>

11147
apps/web/assets/china.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,154 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* add fonts here */
/* @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap'); */
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border:214.3 31.8% 91.4%;
--input:214.3 31.8% 91.4%;
--ring:221.2 83.2% 53.3%;
--radius: 0.5rem;
}
.dark {
--background:222.2 84% 4.9%;
--foreground:210 40% 98%;
--card:222.2 84% 4.9%;
--card-foreground:210 40% 98%;
--popover:222.2 84% 4.9%;
--popover-foreground:210 40% 98%;
--primary:217.2 91.2% 59.8%;
--primary-foreground:222.2 47.4% 11.2%;
--secondary:217.2 32.6% 17.5%;
--secondary-foreground:210 40% 98%;
--muted:217.2 32.6% 17.5%;
--muted-foreground:215 20.2% 65.1%;
--accent:217.2 32.6% 17.5%;
--accent-foreground:210 40% 98%;
--destructive:0 62.8% 30.6%;
--destructive-foreground:210 40% 98%;
--border:217.2 32.6% 17.5%;
--input:217.2 32.6% 17.5%;
--ring:224.3 76.3% 48%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
/* font-family: 'Poppins', sans-serif; */
}
.typography-h1{
@apply text-3xl md:text-4xl font-extrabold tracking-tight scroll-m-20 lg:text-5xl dark:text-white
}
.typography-h2{
@apply pb-2 text-3xl font-semibold tracking-tight transition-colors border-b scroll-m-20 first:mt-0 dark:text-white/90;
}
.typography-h3{
@apply text-2xl font-semibold tracking-tight scroll-m-20 dark:text-white;
}
.typography-h4{
@apply text-xl font-semibold tracking-tight scroll-m-20 dark:text-white;
}
.typography-p{
@apply leading-7 [&:not(:first-child)]:mt-6 dark:text-white;
}
.typography-code{
@apply relative rounded bg-gray-500/50 px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold dark:text-white/90;
}
.typography-lead{
@apply text-lg opacity-50 dark:text-white;
}
.typography-large{
@apply text-lg font-semibold dark:text-white;
}
.typography-small{
@apply text-sm font-medium leading-none dark:text-white;
}
.typography-muted{
@apply text-sm opacity-50 dark:text-white;
}
[class*=v-md-icon-], [class*=v-md-icon-]:before, [class*=v-md-icon-]:after {
font-size: 16px;
font-family: v-md-iconfont!important;
font-style: normal!important;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.v-md-editor textarea{
background:unset;
}
.v-md-editor-preview{
color: #2c3e50;
}
.overflow-y-auto,
.overflow-x-auto,
.overflow-auto {
@apply scrollbar-thumb-slate-700/70 scrollbar-track-transparent scrollbar-thin;
}
.singleLine {
display: flex;
flex-direction: row;
/* height: 40px; */
width: 100%;
overflow: hidden;
position: relative;
}
.singleLine span {
display: inline-block;
height: 20px;
margin-top: 12px;
white-space: nowrap;
}
}

View File

@ -0,0 +1,137 @@
<template>
<div class="mx-auto w-fit">
<svg xmlns="http://www.w3.org/2000/svg" class=" h-[133px] w-fit" width="2895" height="133" fill="none" viewBox="0 0 2895 133">
<path fill="url(#a)" d="M10 10h2880v116H10z" />
<path
class=""
fill="url(#b)" fill-rule="evenodd"
d="M82.818 13.858 2.933 95.613a4 4 0 0 0 .207 5.787l14.971 13.283a4.996 4.996 0 0 0 3.452 1.258l73.13-1.953a50.003 50.003 0 0 0 24.14-6.96l11.321-6.704a51.006 51.006 0 0 1 24.887-7.104L583 84h1278.74l1008.31.5V0H95.165L82.818 13.858Z"
clip-rule="evenodd"
/>
<path
fill="url(#c)" fill-rule="evenodd"
d="m102.818 23.858-74.751 76.501c-4.391 4.494-4.133 11.746.567 15.916l9.477 8.408a4.996 4.996 0 0 0 3.452 1.258l73.129-1.953a50.004 50.004 0 0 0 24.141-6.96l11.321-6.704a51.011 51.011 0 0 1 24.887-7.104L603 94h1278.74l1008.31.5V10H115.165l-12.347 13.858Z"
clip-rule="evenodd" opacity=".7"
/>
<path
stroke="url(#d)"
d="M2890.05 95h.5V9.5H114.941l-.149.167-12.339 13.85-80.61 82.497a3.5 3.5 0 0 0 .18 5.064l15.756 13.979a5.494 5.494 0 0 0 3.797 1.384l73.13-1.954a50.504 50.504 0 0 0 24.382-7.028l11.321-6.704a50.499 50.499 0 0 1 24.643-7.035L603 94.5h1278.74l1008.31.5Z"
opacity=".488"
/>
<g opacity=".762">
<mask id="f" width="2885" height="123" x="6" y="10" maskUnits="userSpaceOnUse" style="mask-type:alpha">
<path fill="url(#e)" fill-opacity=".5" d="M6.5 10h2884v123H6.5z" />
</mask>
<g mask="url(#f)" class="">
<path
fill="url(#g)" fill-opacity=".5" fill-rule="evenodd"
d="M102.818 23.858 37.696 90.504c-9.632 9.858-9.065 25.768 1.244 34.915.417.37.958.567 1.515.552l74.237-1.983a50.004 50.004 0 0 0 24.141-6.96l11.028-6.53a51.004 51.004 0 0 1 25.523-7.115L603.388 99.5H2890.05V10H115.165l-12.347 13.858Z"
clip-rule="evenodd"
/>
<path
fill="url(#h)" fill-rule="evenodd"
d="M102.818 23.858 37.696 90.504c-9.632 9.858-9.065 25.768 1.244 34.915.417.37.958.567 1.515.552l74.237-1.983a50.004 50.004 0 0 0 24.141-6.96l11.028-6.53a51.004 51.004 0 0 1 25.523-7.115L603.388 99.5H2890.05V10H115.165l-12.347 13.858Z"
clip-rule="evenodd"
/>
<path
fill="url(#i)" fill-rule="evenodd"
d="M102.818 23.858 37.696 90.504c-9.632 9.858-9.065 25.768 1.244 34.915.417.37.958.567 1.515.552l74.237-1.983a50.004 50.004 0 0 0 24.141-6.96l11.028-6.53a51.004 51.004 0 0 1 25.523-7.115L603.388 99.5H2890.05V10H115.165l-12.347 13.858Z"
clip-rule="evenodd"
/>
<path
fill="url(#j)" fill-rule="evenodd"
d="M102.818 23.858 37.696 90.504c-9.632 9.858-9.065 25.768 1.244 34.915.417.37.958.567 1.515.552l74.237-1.983a50.004 50.004 0 0 0 24.141-6.96l11.028-6.53a51.004 51.004 0 0 1 25.523-7.115L603.388 99.5H2890.05V10H115.165l-12.347 13.858Z"
clip-rule="evenodd" style="mix-blend-mode:soft-light"
/>
</g>
</g>
<mask id="l" width="2885" height="123" x="10" y="10" maskUnits="userSpaceOnUse" style="mask-type:alpha">
<path fill="url(#k)" d="M10.5 10h2884v123H10.5z" />
</mask>
<g mask="url(#l)">
<path
fill="url(#m)" fill-opacity=".5" fill-rule="evenodd"
d="m106.818 23.858-79.885 81.755a4 4 0 0 0 .207 5.787l14.971 13.283a4.996 4.996 0 0 0 3.452 1.258l73.129-1.953a50.004 50.004 0 0 0 24.141-6.96l11.028-6.53a51.004 51.004 0 0 1 25.523-7.115L607.388 99.5H2894.05V10H137.725a41.396 41.396 0 0 0-30.907 13.858Z"
clip-rule="evenodd" opacity=".478"
/>
</g>
<mask id="o" width="2885" height="123" x="10" y="10" maskUnits="userSpaceOnUse" style="mask-type:alpha">
<path fill="url(#n)" d="M10.5 10h2884v123H10.5z" />
</mask>
<g mask="url(#o)">
<path
stroke="#DEFCFC" stroke-width="2"
d="M134.725 9a42.396 42.396 0 0 0-31.638 14.175l-77.669 79.487c-3.193 3.268-3.005 8.543.413 11.575l12.616 11.194a5.997 5.997 0 0 0 4.143 1.51l73.129-1.954a51.007 51.007 0 0 0 24.624-7.098l11.028-6.531a50 50 0 0 1 25.022-6.975l427.995-3.883H2892.05V9H134.725Z"
/>
</g>
<defs>
<linearGradient id="a" x1="10" x2="10" y1="10" y2="126" gradientUnits="userSpaceOnUse">
<stop stop-color="#1A2531" />
<stop offset="1" stop-color="#0B1016" stop-opacity=".01" />
</linearGradient>
<linearGradient id="b" x1="-530.728" x2="-530.403" y1="45.685" y2="91.411" gradientUnits="userSpaceOnUse">
<stop stop-color="#526A8B" />
<stop offset=".43" stop-color="#0D1219" stop-opacity=".785" />
<stop offset="1" stop-color="#111821" />
</linearGradient>
<linearGradient id="c" x1="20" x2="20" y1="10" y2="125.994" gradientUnits="userSpaceOnUse">
<stop stop-color="#171F2C" />
<stop offset=".43" stop-color="#0D1219" stop-opacity=".785" />
<stop offset="1" stop-color="#111821" />
</linearGradient>
<linearGradient id="d" x1="1990.75" x2="1986.94" y1="25.904" y2="143.261" gradientUnits="userSpaceOnUse">
<stop stop-color="#fff" />
<stop offset="1" stop-color="#fff" stop-opacity=".01" />
</linearGradient>
<linearGradient id="e" x1="1909.39" x2="1911.12" y1="152.221" y2="-13.685" gradientUnits="userSpaceOnUse">
<stop stop-color="#313136" />
<stop offset="1" stop-color="#161619" stop-opacity=".01" />
</linearGradient>
<linearGradient id="k" x1="1106.91" x2="1106.97" y1="133.81" y2="46.092" gradientUnits="userSpaceOnUse">
<stop stop-color="#313136" />
<stop offset="1" stop-color="#161619" stop-opacity=".01" />
</linearGradient>
<linearGradient id="n" x1="1057.05" x2="1057.08" y1="121.718" y2="39.194" gradientUnits="userSpaceOnUse">
<stop stop-color="#313136" />
<stop offset="1" stop-color="#161619" stop-opacity=".01" />
</linearGradient>
<radialGradient
id="g" cx="0" cy="0" r="1"
gradientTransform="matrix(-15.79932 138.77654 -5319.5869 -605.62013 188.243 45.762)"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#506096" />
<stop offset="1" stop-color="#A0A7D7" />
</radialGradient>
<radialGradient
id="h" cx="0" cy="0" r="1" gradientTransform="matrix(0 156.784 -13149.4 0 716.251 10)"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#94CAEB" />
<stop offset="1" stop-color="#456182" />
</radialGradient>
<radialGradient
id="i" cx="0" cy="0" r="1" gradientTransform="scale(9269.85 115.994) rotate(90 -.28 .306)"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#80E0FD" />
<stop offset="1" stop-color="#456182" stop-opacity=".01" />
</radialGradient>
<radialGradient
id="j" cx="0" cy="0" r="1" gradientTransform="matrix(0 115.641 -6024.05 0 172.809 20.1)"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#44F6D9" />
<stop offset="1" stop-color="#A4FFF0" />
</radialGradient>
<radialGradient
id="m" cx="0" cy="0" r="1" gradientTransform="matrix(0 132.119 -5129.91 0 192.243 45.76)"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#68C3D5" />
<stop offset="1" stop-color="#A8EDD7" />
</radialGradient>
</defs>
</svg>
</div>
</template>

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
const colorMode = useColorMode()
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost">
<Icon icon="radix-icons:moon" class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Icon icon="radix-icons:sun" class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span class="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem @click="colorMode.preference = 'light'">
亮色
</DropdownMenuItem>
<DropdownMenuItem @click="colorMode.preference = 'dark'">
暗色
</DropdownMenuItem>
<DropdownMenuItem @click="colorMode.preference = 'system'">
跟随系统
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>

View File

@ -0,0 +1,15 @@
<template>
<svg viewBox="0 0 506 1080" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:anim="http://www.w3.org/2000/anim" anim="" anim:transform-origin="50% 50%" anim:duration="1" anim:ease="ease-in-out">
<g id="左侧面板-黑透">
<rect id="透" opacity="0.973" width="506" height="1080" transform="matrix(-1 0 0 1 506 0)" fill="url(#paint0_linear_0_9847)" />
</g>
<defs>
<linearGradient id="paint0_linear_0_9847" x1="505.702" y1="27.5797" x2="2.1156" y2="25.5704" gradientUnits="userSpaceOnUse">
<stop stop-color="#0A131C" stop-opacity="0.8" />
<stop offset="0.409839" stop-color="#0F1C28" stop-opacity="0.6" />
<stop offset="0.679029" stop-color="#101C28" stop-opacity="0.4" />
<stop offset="1" stop-color="#101D29" stop-opacity="0.01" />
</linearGradient>
</defs>
</svg>
</template>

View File

@ -0,0 +1,52 @@
<template>
<svg viewBox="0 0 1120 1080" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:anim="http://www.w3.org/2000/anim" anim="" anim:transform-origin="50% 50%" anim:duration="1" anim:ease="ease-in-out">
<g id="右侧面板-黑透">
<g id="编组">
<rect id="右侧面板-é»é€_2" opacity="0.7" x="234" y="-3" width="1255" height="1080" fill="url(#paint0_linear_0_9849)" />
<rect id="右侧面板-é»é€_3" opacity="0.1" x="234" y="-3" width="1255" height="1080" fill="url(#paint1_linear_0_9849)" />
<rect id="矩形" x="399" y="-4" width="1090" height="1083" fill="url(#paint2_linear_0_9849)" />
</g>
<g id="编组 2">
<path id="右侧面板-é»é€_4" opacity="0.7" fill-rule="evenodd" clip-rule="evenodd" d="M0 1H1120V1081H0V1Z" fill="url(#paint3_linear_0_9849)" />
<path id="右侧面板-é»é€_5" opacity="0.1" fill-rule="evenodd" clip-rule="evenodd" d="M0 1H1120V1081H0V1Z" fill="url(#paint4_linear_0_9849)" />
<path id="矩形_2" fill-rule="evenodd" clip-rule="evenodd" d="M76 0H1120V1083H76V0Z" fill="url(#paint5_linear_0_9849)" />
</g>
</g>
<defs>
<linearGradient id="paint0_linear_0_9849" x1="1494.34" y1="22.0178" x2="239.376" y2="9.59891" gradientUnits="userSpaceOnUse">
<stop stop-color="#0A131C" />
<stop offset="0.886665" stop-color="#0F1C28" stop-opacity="0.8" />
<stop offset="0.930356" stop-color="#101C28" stop-opacity="0.6" />
<stop offset="1" stop-color="#101D29" stop-opacity="0.01" />
</linearGradient>
<linearGradient id="paint1_linear_0_9849" x1="1494.34" y1="22.0178" x2="239.376" y2="9.59891" gradientUnits="userSpaceOnUse">
<stop stop-color="#0A131C" />
<stop offset="0.886665" stop-color="#0F1C28" stop-opacity="0.8" />
<stop offset="0.930356" stop-color="#101C28" stop-opacity="0.6" />
<stop offset="1" stop-color="#101D29" stop-opacity="0.01" />
</linearGradient>
<linearGradient id="paint2_linear_0_9849" x1="1489" y1="-3.20769" x2="400.595" y2="-3.20769" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0.35" />
<stop offset="0.824744" stop-opacity="0.15" />
<stop offset="1" stop-opacity="0.01" />
</linearGradient>
<linearGradient id="paint3_linear_0_9849" x1="1124.77" y1="26.0178" x2="4.77502" y2="16.1268" gradientUnits="userSpaceOnUse">
<stop stop-color="#0A131C" />
<stop offset="0.886665" stop-color="#0F1C28" stop-opacity="0.8" />
<stop offset="0.930356" stop-color="#101C28" stop-opacity="0.6" />
<stop offset="1" stop-color="#101D29" stop-opacity="0.01" />
</linearGradient>
<linearGradient id="paint4_linear_0_9849" x1="1124.77" y1="26.0178" x2="4.77502" y2="16.1268" gradientUnits="userSpaceOnUse">
<stop stop-color="#0A131C" />
<stop offset="0.886665" stop-color="#0F1C28" stop-opacity="0.8" />
<stop offset="0.930356" stop-color="#101C28" stop-opacity="0.6" />
<stop offset="1" stop-color="#101D29" stop-opacity="0.01" />
</linearGradient>
<linearGradient id="paint5_linear_0_9849" x1="1120" y1="0.79231" x2="77.5276" y2="0.79231" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0.35" />
<stop offset="0.824744" stop-opacity="0.15" />
<stop offset="1" stop-opacity="0.01" />
</linearGradient>
</defs>
</svg>
</template>

View File

@ -0,0 +1,25 @@
<script setup lang="ts">
withDefaults(defineProps<{
src?: string
fullScreen?: boolean
rounded?: boolean
}>(), {
src: '',
fullScreen: false,
rounded: true
})
</script>
<template>
<div
class="flex justify-center items-center overflow-hidden"
:class="[fullScreen ? 'h-full w-full' : 'h-[100px] w-[200px]', rounded ? 'rounded-md' : '']"
>
<img :src="resourceApi.resolution(src,'st3')" alt="background" class=" h-full w-full object-cover">
<!-- <img v-else src="/null.webp" alt="background" class=" object-cover h-full w-full dark:invert opacity-25"> -->
</div>
</template>

View File

@ -0,0 +1,25 @@
<script setup lang="ts">
withDefaults(defineProps<{
src?: string
rounded?: boolean
shadow?: boolean
}>(), {
src: '',
rounded: true,
shadow: false
})
</script>
<template>
<div class="h-[90px] flex justify-center items-center w-[90px] overflow-hidden" :class="[rounded ? 'rounded-md' : '',shadow ? 'shadow-inner' : '',]">
<img :src="resourceApi.resolution(src,'st1')" alt="Image" class=" object-cover w-full h-full">
<!-- <img v-else src="/null.webp" alt="Image" class=" object-cover w-full h-full dark:invert opacity-55 dark:opacity-20"> -->
</div>
</template>

View File

@ -0,0 +1,43 @@
<script setup lang="ts">
function handleBtnEvent() {
}
function handleConfirmEvent() {
}
const data = ref({
maxDot: 5,
imageBase64: '',
thumbBase64: '',
})
</script>
<template>
<UPopover>
<div class="w-full">
<!-- 初始状态 -->
<UButton
icon="i-solar-shield-keyhole-bold-duotone text-blue-500"
variant="ghost"
color="cyan"
label="请进行人机验证"
:trailing="false"
size="lg"
block
@click="handleBtnEvent"
/>
</div>
<template #panel>
<Captcha
width="300px"
height="240px"
:max-dot="data.maxDot"
:image-base64="data.imageBase64"
:thumb-base64="data.thumbBase64"
@confirm="handleConfirmEvent"
/>
</template>
</UPopover>
</template>

View File

@ -0,0 +1,277 @@
<script setup lang="ts">
const {
value,
width = '300px',
height = '240px',
calcPosType = 'dom',
maxDot = 5,
imageBase64,
thumbBase64,
} = defineProps<{
value?: boolean
width?: string
height?: string
calcPosType?: string
maxDot?: number
imageBase64?: string
thumbBase64?: string
}>()
const emit = defineEmits<{
(e: 'close'): void
(e: 'success', value: { CaptchaCode: string }): void
(e: 'refresh'): void
}>()
const { data, refresh } = useAsyncData(API.common.auth.capthcha)
const toast = useToastHandle()
const state = reactive({
dots: ref<any[]>([]),
imageBase64Code: imageBase64,
thumbBase64Code: thumbBase64,
style: computed(() => {
return `width:${width}; height:${height};`
}),
/**
* @Description: 处理关闭事件
*/
handleCloseEvent() {
emit('close')
state.dots = []
state.imageBase64Code = ''
state.thumbBase64Code = ''
},
/**
* @Description: 处理刷新事件
*/
handleRefreshEvent() {
state.dots = []
refresh()
emit('refresh')
},
/**
* @Description: 处理确认事件
*/
async handleConfirmEvent() {
try {
//
const res = await API.common.auth.checkCaptcha({
UUID: data.value?.UUID,
Check: state.dots.map(i => [i.x, i.y].join(',')).join(','),
})
emit('success', { CaptchaCode: res?.Code ?? '' })
} catch (err: any) {
toast.error(err.message)
state.handleRefreshEvent()
}
},
/**
* @Description: 处理dot
* @param ev
*/
handleClickPos(ev: any) {
if (state.dots.length >= maxDot)
return
const e = ev || window.event
e.preventDefault()
const dom = e.currentTarget
const { domX, domY } = state.getDomXY(dom)
// ===============================================
// @notice getDomXY 使 calcLocationLeft calcLocationTop
// const domX = state.calcLocationLeft(dom)
// const domY = state.calcLocationTop(dom)
// ===============================================
let mouseX = navigator.appName === 'Netscape'
? e.pageX
: e.x + document.body.offsetTop
let mouseY = navigator.appName === 'Netscape'
? e.pageY
: e.y + document.body.offsetTop
if (calcPosType === 'screen') {
mouseX = navigator.appName === 'Netscape' ? e.clientX : e.x
mouseY = navigator.appName === 'Netscape' ? e.clientY : e.y
}
//
const xPos = mouseX - domX
const yPos = mouseY - domY
//
const xp = Number.parseInt(xPos.toString())
const yp = Number.parseInt(yPos.toString())
//
state.dots.push({
x: xp - 11,
y: yp - 11,
index: state.dots.length + 1,
})
return false
},
/**
* @Description: 找到元素的屏幕位置
* @param el
*/
calcLocationLeft(el: any) {
let tmp = el.offsetLeft
let val = el.offsetParent
while (val != null) {
tmp += val.offsetLeft
val = val.offsetParent
}
return tmp
},
/**
* @Description: 找到元素的屏幕位置
* @param el
*/
calcLocationTop(el: any) {
let tmp = el.offsetTop
let val = el.offsetParent
while (val != null) {
tmp += val.offsetTop
val = val.offsetParent
}
return tmp
},
/**
* @Description: 找到元素的屏幕位置
* @param dom
*/
getDomXY(dom: any) {
let x = 0
let y = 0
if (dom.getBoundingClientRect) {
const box = dom.getBoundingClientRect()
const D = document.documentElement
x = box.left
+ Math.max(D.scrollLeft, document.body.scrollLeft)
- D.clientLeft
y = box.top
+ Math.max(D.scrollTop, document.body.scrollTop)
- D.clientTop
}
else {
while (dom !== document.body) {
x += dom.offsetLeft
y += dom.offsetTop
dom = dom.offsetParent
}
}
return {
domX: x,
domY: y,
}
},
})
effect(() => {
if (data.value) {
const res = (data?.value)
// const res = JSON.parse(data?.value)
state.imageBase64Code = res?.BackgroundImage
state.thumbBase64Code = res?.CheckImage
}
})
watch(() => value, () => {
state.dots = []
state.imageBase64Code = ''
state.thumbBase64Code = ''
})
watch(() => imageBase64, (val) => {
state.dots = []
state.imageBase64Code = val
})
watch(() => thumbBase64, (val) => {
state.dots = []
state.thumbBase64Code = val
})
</script>
<template>
<UCard>
<template #header>
<div class="flex items-center justify-center">
<span>
请在下图
<em>依次</em>
点击
</span>
<img v-if="state.thumbBase64Code" :src="state.thumbBase64Code" alt=" ">
</div>
</template>
<div class="wg-cap-wrap__body">
<img
v-if="state.imageBase64Code"
class="wg-cap-wrap__picture"
:src="state.imageBase64Code"
alt=" "
@click="state.handleClickPos($event)"
>
<template v-for="(dot, key) in state.dots" :key="key">
<div class="wg-cap-wrap__dot" :style="`top: ${dot.y}px; left:${dot.x}px;`">
<span>{{ dot.index }}</span>
</div>
</template>
</div>
<template #footer>
<div class="flex justify-between">
<div>
<!-- <UButton icon="i-solar-close-circle-line-duotone" variant="ghost" color="gray" title="关闭" @click="state.handleCloseEvent" /> -->
<UButton
icon="i-solar-refresh-circle-line-duotone"
variant="ghost"
color="gray"
title="刷新"
@click="state.handleRefreshEvent"
/>
</div>
<div>
<UButton @click="state.handleConfirmEvent">
确认
</UButton>
</div>
</div>
</template>
</UCard>
</template>
<style>
.wg-cap-wrap__body {
position: relative;
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
background: #34383e;
margin: auto;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
overflow: hidden;
}
.wg-cap-wrap__body .wg-cap-wrap__dot {
position: absolute;
z-index: 10;
width: 22px;
height: 22px;
color: #cedffe;
background: #3e7cff;
border: 2px solid #f7f9fb;
line-height: 20px;
text-align: center;
-webkit-border-radius: 22px;
-moz-border-radius: 22px;
border-radius: 22px;
cursor: default;
}
</style>

View File

@ -0,0 +1,379 @@
<template>
<div class="wg-cap-btn" :style="style">
<div class="wg-cap-btn__inner" :class="activeClass">
<!-- wg-cap-active__default wg-cap-active__error wg-cap-active__over wg-cap-active__success -->
<template>
<div @click="handleBtnEvent" class="wg-cap-state__default">
<!-- 初始状态 -->
<div class="wg-cap-state__inner">
<div class="wg-cap-btn__ico wg-cap-btn__verify">
<img
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSLlm77lsYJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiCgkgdmlld0JveD0iMCAwIDIwMCAyMDAiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDIwMCAyMDA7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojM0U3Q0ZGO30KCS5zdDF7ZmlsbDojRkZGRkZGO30KPC9zdHlsZT4KPGNpcmNsZSBjbGFzcz0ic3QwIiBjeD0iMTAwIiBjeT0iMTAwIiByPSI5Ni4zIi8+CjxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik0xNDAuOCw2NC40bC0zOS42LTExLjloLTIuNEw1OS4yLDY0LjRjLTEuNiwwLjgtMi44LDIuNC0yLjgsNHYyNC4xYzAsMjUuMywxNS44LDQ1LjksNDIuMyw1NC42CgljMC40LDAsMC44LDAuNCwxLjIsMC40YzAuNCwwLDAuOCwwLDEuMi0wLjRjMjYuNS04LjcsNDIuMy0yOC45LDQyLjMtNTQuNlY2OC4zQzE0My41LDY2LjgsMTQyLjMsNjUuMiwxNDAuOCw2NC40eiIvPgo8L3N2Zz4K" />
</div>
<span class="wg-cap-btn__text">点击按键进行人机验证</span>
</div>
</div>
<div @click="() => false" class="wg-cap-state__check">
<!-- 验证状态 -->
<div class="wg-cap-state__inner">
<div class="wg-cap-btn__ico">
<img
src="data:image/svg+xml;base64,PHN2ZyB0PSIxNjI3MDU1NTg2NTk0IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjEyMTEiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMTIwLjI1OTQ1NiA1MTIuMDAxMDIzbS0xMTcuOTIzNzYgMGExMTUuMjM4IDExNS4yMzggMCAxIDAgMjM1Ljg0NzUxOSAwIDExNS4yMzggMTE1LjIzOCAwIDEgMC0yMzUuODQ3NTE5IDBaIiBwLWlkPSIxMjEyIiBmaWxsPSIjZmZhMDAwIj48L3BhdGg+PHBhdGggZD0iTTUxMS45OTk0ODggNTEyLjAwMTAyM20tMTE3LjkyMTcxMyAwYTExNS4yMzYgMTE1LjIzNiAwIDEgMCAyMzUuODQzNDI2IDAgMTE1LjIzNiAxMTUuMjM2IDAgMSAwLTIzNS44NDM0MjYgMFoiIHAtaWQ9IjEyMTMiIGZpbGw9IiNmZmEwMDAiPjwvcGF0aD48cGF0aCBkPSJNOTAzLjczOTUyMSA1MTIuMDAxMDIzbS0xMTcuOTIzNzYgMGExMTUuMjM4IDExNS4yMzggMCAxIDAgMjM1Ljg0NzUxOSAwIDExNS4yMzggMTE1LjIzOCAwIDEgMC0yMzUuODQ3NTE5IDBaIiBwLWlkPSIxMjE0IiBmaWxsPSIjZmZhMDAwIj48L3BhdGg+PC9zdmc+"
alt="" />
</div>
<span class="wg-cap-btn__text">正在进行人机验证...</span>
</div>
</div>
<div @click="handleBtnEvent" class="wg-cap-state__error">
<!-- 验证失败状态 -->
<div class="wg-cap-state__inner">
<div class="wg-cap-btn__ico">
<img
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGw6I0VENDYzMDt9Cjwvc3R5bGU+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xODQsMjYuNkwxMDIuNCwyLjFoLTQuOUwxNiwyNi42Yy0zLjMsMS42LTUuNyw0LjktNS43LDguMnY0OS44YzAsNTIuMiwzMi42LDk0LjcsODcuMywxMTIuNgoJYzAuOCwwLDEuNiwwLjgsMi40LDAuOHMxLjYsMCwyLjQtMC44YzU0LjctMTgsODcuMy01OS42LDg3LjMtMTEyLjZWMzQuN0MxODkuOCwzMS41LDE4Ny4zLDI4LjIsMTg0LDI2LjZ6IE0xMzQuNSwxMjMuMQoJYzMuMSwzLjEsMy4xLDguMiwwLDExLjNjLTEuNiwxLjYtMy42LDIuMy01LjcsMi4zcy00LjEtMC44LTUuNy0yLjNMMTAwLDExMS4zbC0yMy4xLDIzLjFjLTEuNiwxLjYtMy42LDIuMy01LjcsMi4zCgljLTIsMC00LjEtMC44LTUuNy0yLjNjLTMuMS0zLjEtMy4xLTguMiwwLTExLjNMODguNywxMDBMNjUuNSw3Ni45Yy0zLjEtMy4xLTMuMS04LjIsMC0xMS4zYzMuMS0zLjEsOC4yLTMuMSwxMS4zLDBMMTAwLDg4LjcKCWwyMy4xLTIzLjFjMy4xLTMuMSw4LjItMy4xLDExLjMsMGMzLjEsMy4xLDMuMSw4LjIsMCwxMS4zTDExMS4zLDEwMEwxMzQuNSwxMjMuMXoiLz4KPC9zdmc+Cg=="
alt="失败" />
</div>
<span>
人机验证失败
<em>点击重试</em>
</span>
</div>
</div>
<div @click="handleBtnEvent" class="wg-cap-state__over">
<!-- 验证次数过多状态 -->
<div class="wg-cap-state__inner">
<div class="wg-cap-btn__ico">
<img
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGw6I0VENDYzMDt9Cjwvc3R5bGU+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xODQsMjYuNkwxMDIuNCwyLjFoLTQuOUwxNiwyNi42Yy0zLjMsMS42LTUuNyw0LjktNS43LDguMnY0OS44YzAsNTIuMiwzMi42LDk0LjcsODcuMywxMTIuNgoJYzAuOCwwLDEuNiwwLjgsMi40LDAuOHMxLjYsMCwyLjQtMC44YzU0LjctMTgsODcuMy01OS42LDg3LjMtMTEyLjZWMzQuN0MxODkuOCwzMS41LDE4Ny4zLDI4LjIsMTg0LDI2LjZ6IE0xMzQuNSwxMjMuMQoJYzMuMSwzLjEsMy4xLDguMiwwLDExLjNjLTEuNiwxLjYtMy42LDIuMy01LjcsMi4zcy00LjEtMC44LTUuNy0yLjNMMTAwLDExMS4zbC0yMy4xLDIzLjFjLTEuNiwxLjYtMy42LDIuMy01LjcsMi4zCgljLTIsMC00LjEtMC44LTUuNy0yLjNjLTMuMS0zLjEtMy4xLTguMiwwLTExLjNMODguNywxMDBMNjUuNSw3Ni45Yy0zLjEtMy4xLTMuMS04LjIsMC0xMS4zYzMuMS0zLjEsOC4yLTMuMSwxMS4zLDBMMTAwLDg4LjcKCWwyMy4xLTIzLjFjMy4xLTMuMSw4LjItMy4xLDExLjMsMGMzLjEsMy4xLDMuMSw4LjIsMCwxMS4zTDExMS4zLDEwMEwxMzQuNSwxMjMuMXoiLz4KPC9zdmc+Cg=="
alt="失败" />
</div>
<span>
点击次数过多
<em>点击重试</em>
</span>
</div>
</div>
<div @click="() => false" class="wg-cap-state__success">
<!-- 验证成功状态 -->
<div class="wg-cap-state__inner">
<div class="wg-cap-btn__ico">
<img
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGw6IzVFQUEyRjt9Cjwvc3R5bGU+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xODMuMywyNy4yTDEwMi40LDIuOWgtNC45TDE2LjcsMjcuMkMxMy40LDI4LjgsMTEsMzIsMTEsMzUuM3Y0OS40YzAsNTEuOCwzMi40LDkzLjksODYuNiwxMTEuNwoJYzAuOCwwLDEuNiwwLjgsMi40LDAuOGMwLjgsMCwxLjYsMCwyLjQtMC44YzU0LjItMTcuOCw4Ni42LTU5LjEsODYuNi0xMTEuN1YzNS4zQzE4OSwzMiwxODYuNiwyOC44LDE4My4zLDI3LjJ6IE0xNDYuMSw4MS40CglsLTQ4LjUsNDguNWMtMS42LDEuNi0zLjIsMi40LTUuNywyLjRjLTIuNCwwLTQtMC44LTUuNy0yLjRMNjIsMTA1LjdjLTMuMi0zLjItMy4yLTguMSwwLTExLjNjMy4yLTMuMiw4LjEtMy4yLDExLjMsMGwxOC42LDE4LjYKCWw0Mi45LTQyLjljMy4yLTMuMiw4LjEtMy4yLDExLjMsMEMxNDkuNCw3My4zLDE0OS40LDc4LjIsMTQ2LjEsODEuNEwxNDYuMSw4MS40eiIvPgo8L3N2Zz4K"
alt="成功" />
</div>
<span>人机验证已通过</span>
</div>
</div>
</template>
<el-dialog v-model:visible="popoverVisible" :close-on-click-modal="false" append-to-body :center="true" title="人机校验"
:show-close="false" z-index="999999" width="360px">
<CommonCaptcha v-model="popoverVisible" width="300px" height="240px" :max-dot="maxDot" :image-base64="imageBase64"
:thumb-base64="thumbBase64" @close="handleCloseEvent" @refresh="handleRefreshEvent"
@confirm="handleConfirmEvent" />
</el-dialog>
</div>
</div>
</template>
<script>
import CommonCaptcha from './captcha.vue'
import { defineComponent, watch, toRefs, computed, ref, reactive } from 'vue'
export default defineComponent({
name: 'CommonCaptchaDialog',
components: { CommonCaptcha },
props: {
value: {
type: String,
default: 'default',
validator: value =>
['default', 'check', 'error', 'over', 'success'].indexOf(value) > -1,
},
width: String,
height: String,
maxDot: {
type: Number,
default: 5,
},
imageBase64: String,
thumbBase64: String,
},
setup (props, { emit }) {
const state = reactive({
popoverVisible: ref(false),
captStatus: ref('default'),
activeClass: computed(() => {
return `wg-cap-active__${state.captStatus}`
}),
style: computed(() => {
return `width:${props.width}; height:${props.height};`
}),
handleBtnEvent () {
setTimeout(() => {
state.popoverVisible = true
}, 0)
},
handleRefreshEvent () {
state.captStatus = 'check'
emit('refresh')
},
handleConfirmEvent (data) {
emit('confirm', data)
},
handleCloseEvent () {
state.popoverVisible = false
},
})
watch(
() => state.popoverVisible,
v => {
if (v) {
state.captStatus = 'check'
emit('refresh')
} else if (state.captStatus === 'check') {
state.captStatus = props.value
}
}
)
watch(
() => props.value,
val => {
if (state.captStatus !== 'check') {
state.captStatus = val
}
if (val === 'over' || val === 'success') {
setTimeout(() => {
state.popoverVisible = false
}, 0)
}
}
)
watch(
() => state.captStatus,
val => {
if (val !== 'check' && props.value !== val) {
emit('input', val)
}
}
)
return {
...toRefs(state),
}
},
})
</script>
<style scoped>
.wg-cap-btn {
width: 100%;
height: 48px;
}
.wg-cap-btn .wg-cap-btn__inner {
width: 100%;
height: 48px;
position: relative;
letter-spacing: 1px;
}
.wg-cap-btn .wg-cap-state__default,
.wg-cap-btn .wg-cap-state__check,
.wg-cap-btn .wg-cap-state__error,
.wg-cap-btn .wg-cap-state__success,
.wg-cap-btn .wg-cap-state__over {
position: absolute;
width: 100%;
height: 48px;
font-size: 13px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
display: inline-block;
line-height: 1;
white-space: nowrap;
cursor: pointer;
-webkit-appearance: none;
appearance: none;
box-sizing: border-box;
outline: none;
margin: 0;
transition: 0.1s;
font-weight: 500;
-moz-user-select: none;
-webkit-user-select: none;
user-select: none;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
justify-content: center;
justify-items: center;
visibility: hidden;
}
.wg-cap-btn .wg-cap-state__default {
color: #3e7cff;
border: 1px solid #50a1ff;
background: #ecf5ff;
box-shadow: 0 0 20px rgba(62, 124, 255, 0.1);
-webkit-box-shadow: 0 0 20px rgba(62, 124, 255, 0.1);
-moz-box-shadow: 0 0 20px rgba(62, 124, 255, 0.1);
}
.wg-cap-btn .wg-cap-state__check {
cursor: default;
color: #ffa000;
background: #fdf6ec;
border: 1px solid #ffbe09;
}
.wg-cap-btn .wg-cap-state__error {
color: #ed4630;
background: #fef0f0;
border: 1px solid #ff5a34;
}
.wg-cap-btn .wg-cap-state__over {
color: #ed4630;
background: #fef0f0;
border: 1px solid #ff5a34;
}
.wg-cap-btn .wg-cap-state__success {
color: #5eaa2f;
background: #f0f9eb;
border: 1px solid #8bc640;
}
.wg-cap-btn .wg-cap-active__default .wg-cap-state__default,
.wg-cap-btn .wg-cap-active__error .wg-cap-state__error,
.wg-cap-btn .wg-cap-active__over .wg-cap-state__over,
.wg-cap-btn .wg-cap-active__success .wg-cap-state__success,
.wg-cap-btn .wg-cap-active__check .wg-cap-state__check {
visibility: visible;
}
.wg-cap-btn .wg-cap-state__inner {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
justify-content: center;
justify-items: center;
}
.wg-cap-btn .wg-cap-state__inner em {
padding-left: 5px;
color: #3e7cff;
font-style: normal;
}
.wg-cap-btn .wg-cap-btn__inner .wg-cap-btn__ico {
position: relative;
width: 24px;
height: 24px;
margin-right: 12px;
font-size: 14px;
display: inline-block;
/*float: left;*/
flex: 0;
}
.wg-cap-btn .wg-cap-btn__inner .wg-cap-btn__ico img {
width: 24px;
height: 24px;
float: left;
position: relative;
z-index: 10;
}
@keyframes ripple {
0% {
opacity: 0;
}
5% {
opacity: 0.05;
}
20% {
opacity: 0.35;
}
65% {
opacity: 0.01;
}
100% {
transform: scaleX(2) scaleY(2);
opacity: 0;
}
}
@-webkit-keyframes ripple {
0% {
opacity: 0;
}
5% {
opacity: 0.05;
}
20% {
opacity: 0.35;
}
65% {
opacity: 0.01;
}
100% {
transform: scaleX(2) scaleY(2);
opacity: 0;
}
}
.wg-cap-btn .wg-cap-btn__inner .wg-cap-btn__verify::after {
background: #409eff;
-webkit-border-radius: 50px;
-moz-border-radius: 50px;
border-radius: 50px;
content: '';
display: block;
width: 24px;
height: 24px;
opacity: 0;
position: absolute;
top: 0;
left: 0;
z-index: 9;
animation: ripple 1.3s infinite;
-moz-animation: ripple 1.3s infinite;
-webkit-animation: ripple 1.3s infinite;
animation-delay: 2s;
-moz-animation-delay: 2s;
-webkit-animation-delay: 2s;
}
.wg-cap-tip {
padding: 50px 20px 100px;
font-size: 13px;
color: #76839b;
text-align: center;
line-height: 180%;
width: 100%;
max-width: 680px;
}
</style>

View File

@ -0,0 +1,57 @@
<script setup lang="ts">
defineProps<{
loading?: boolean
}>()
const open = ref(false)
const emit = defineEmits<{
(e: 'verify', value: (res: boolean) => void): void
(e: 'success', value: (res: boolean) => void): void
}>()
const { counter, reset, pause, resume } = useInterval(1000, { controls: true, immediate: false })
const count = computed(() => {
const res = 60 - counter.value
if (res === 0) {
pause()
reset()
return ''
}
if (res === 60) {
return ''
}
return `${res}`
})
const verify = () => {
emit('verify', (res) => {
if (res) {
success()
}
})
}
const success = () => {
open.value = false
emit('success', (res) => {
if (res) {
resume()
}
})
}
</script>
<template>
<!-- <UPopover v-model:open="open" class="h-full" :popper="{ placement: 'top-end' }"> -->
<Button variant="secondary" :disabled="counter > 0" :loading="loading" @click.prevent="verify">
发送验证码{{ count }}
</Button>
<!-- <template #panel>
<Captcha width="300px" height="240px" :max-dot="5" @success="success" />
</template>
</UPopover> -->
</template>

View File

@ -0,0 +1,42 @@
<script setup lang="ts">
import { type EChartsOption, type EChartsType, init, registerMap } from 'echarts'
import china from '@/assets/china.json'
import 'echarts-gl'
const props = defineProps<{
options: EChartsOption
}>()
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
registerMap('china', china)
const chartEle = shallowRef<HTMLDivElement>()
const chart = shallowRef<EChartsType>()
let listener: any
onMounted(() => {
chart.value = init(chartEle.value!)
listener = window.addEventListener('resize', () => {
chart.value?.resize()
})
})
effect(() => {
console.log('chart options update', props.options)
props.options && chart.value?.setOption(props.options)
})
onDeactivated(() => {
window.removeEventListener('resize', listener)
})
</script>
<template>
<div style="height:100%">
<div ref="chartEle" style="height: 100%;" />
</div>
</template>

View File

@ -0,0 +1,46 @@
<script setup lang="ts">
import colors from '#tailwind-config/theme/colors'
const appConfig = useAppConfig()
const colorMode = useColorMode()
// Computed
const primaryColors = computed(() => appConfig.ui.colors.filter(color => color !== 'primary').map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] })))
const primary = computed({
get() {
return primaryColors.value.find(option => option.value === appConfig.ui.primary)
},
set(option) {
appConfig.ui.primary = option?.value?? 'orange'
window.localStorage.setItem('nuxt-ui-primary', appConfig.ui.primary)
},
})
const grayColors = computed(() => ['slate', 'cool', 'zinc', 'neutral', 'stone'].map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] })))
const gray = computed({
get() {
return grayColors.value.find(option => option.value === appConfig.ui.gray)
},
set(option) {
appConfig.ui.gray = option?.value ?? 'slate'
window.localStorage.setItem('nuxt-ui-gray', appConfig.ui.gray)
},
})
</script>
<template>
<div class="p-2">
<div class="grid grid-cols-5 gap-px">
<ColorPickerPill v-for="color in primaryColors" :key="color.value" :color="color" :selected="primary" @select="primary = color" />
</div>
<hr class="my-2 border-gray-200 dark:border-gray-800">
<div class="grid grid-cols-5 gap-px">
<ColorPickerPill v-for="color in grayColors" :key="color.value" :color="color" :selected="gray" @select="gray = color" />
</div>
</div>
</template>

View File

@ -0,0 +1,29 @@
<script setup lang="ts">
defineProps<{ color: { value: string, hex: string }, selected?: {
value: string;
text: string;
hex: any;
} }>()
defineEmits(['select'])
</script>
<template>
<UTooltip :text="color.value" class="capitalize" :open-delay="500">
<UButton
color="white"
square
:ui="{
color: {
white: {
solid: 'ring-0 bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800',
ghost: 'hover:bg-gray-50 dark:hover:bg-gray-800/50',
},
},
}"
:variant="color.value === selected?.value ? 'solid' : 'ghost'"
@click.stop.prevent="$emit('select')"
>
<span class="inline-block h-3 w-3 rounded-full" :style="{ backgroundColor: color.hex }" />
</UButton>
</UTooltip>
</template>

View File

@ -0,0 +1,56 @@
<script setup lang="ts">
import colors from '#tailwind-config/theme/colors'
const appConfig = useAppConfig()
const colorMode = useColorMode()
// Computed
const primaryColors = computed(() => appConfig.ui.colors.filter(color => color !== 'primary').map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] })))
const primary = computed({
get() {
return primaryColors.value.find(option => option.value === appConfig.ui.primary)
},
set(option) {
appConfig.ui.primary = option?.value?? 'orange'
window.localStorage.setItem('nuxt-ui-primary', appConfig.ui.primary)
},
})
const grayColors = computed(() => ['slate', 'cool', 'zinc', 'neutral', 'stone'].map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] })))
const gray = computed({
get() {
return grayColors.value.find(option => option.value === appConfig.ui.gray)
},
set(option) {
appConfig.ui.gray = option?.value ?? 'slate'
window.localStorage.setItem('nuxt-ui-gray', appConfig.ui.gray)
},
})
</script>
<template>
<UPopover mode="hover" :popper="{ strategy: 'absolute' }" :ui="{ width: 'w-[156px]' }">
<template #default="{ open }">
<UButton color="gray" variant="ghost" square :class="[open && 'bg-gray-50 dark:bg-gray-800']" aria-label="Color picker">
<UIcon name="i-heroicons-swatch-20-solid" class="text-primary-500 dark:text-primary-400 h-5 w-5" />
</UButton>
</template>
<template #panel>
<div class="p-2">
<div class="grid grid-cols-5 gap-px">
<ColorPickerPill v-for="color in primaryColors" :key="color.value" :color="color" :selected="primary" @select="primary = color" />
</div>
<hr class="my-2 border-gray-200 dark:border-gray-800">
<div class="grid grid-cols-5 gap-px">
<ColorPickerPill v-for="color in grayColors" :key="color.value" :color="color" :selected="gray" @select="gray = color" />
</div>
</div>
</template>
</UPopover>
</template>

View File

@ -0,0 +1,60 @@
<script setup lang="ts">
defineProps<{
title: string
value: number
}>()
</script>
<template>
<div class="h-[77px] relative w-[140px]">
<div class="w-full text-center text-[#E4F3FF] text-[14px] scale-95 opacity-70">
{{ title }}
</div>
<div class="bottom-0 h-[50px] flex justify-center items-center w-full absolute text-[24px] text-[#00F8F4]">
{{ value }}
</div>
<svg
class="absolute bottom-0" viewBox="0 0 140 75" fill="none" xmlns="http://www.w3.org/2000/svg"
xmlns:anim="http://www.w3.org/2000/anim" anim="" anim:transform-origin="50% 50%" anim:duration="1"
anim:ease="ease-in-out"
>
<g id="1" clip-path="url(#clip0_0_11080)">
<path
id="BG" opacity="0.39908" fill-rule="evenodd" clip-rule="evenodd"
d="M3 33.1563L9.02438 25H129.615L137.242 33.1563L137.36 66L129.615 74.3253L9.02438 74.0097L3 66L3 33.1563Z"
fill="url(#paint0_radial_0_11080)"
/>
<path id="路径 41" opacity="0.245359" d="M6.91697 70.52H133.553" stroke="white" stroke-width="0.6" />
<path
id="路径 31" opacity="0.4" d="M3.23047 32.6844L9.25485 24.6747H129.846L137.591 32.6844"
stroke="white"
/>
<path
id="路径 31_2" opacity="0.4" d="M3.23047 65.6747L9.25485 73.6844H129.846L137.591 65.6747"
stroke="white"
/>
<rect id="矩形" y="48" width="4.94438" height="2.7" rx="1" fill="#2EF9CB" />
<rect id="矩形_2" x="135" y="48" width="4.94438" height="2.7" rx="1" fill="#2EF9CB" />
<g id="四边">
<path id="路径 32" d="M3.23047 32.831L9.25485 24.6747H15.5446" stroke="#BFE2FB" />
<path id="路径 32_2" d="M136.544 32.831L130.52 24.6747H124.23" stroke="#BFE2FB" />
<path id="路径 32_3" d="M3.23047 65.6747L9.25485 73.831H15.5446" stroke="#BFE2FB" />
<path id="路径 32_4" d="M136.544 65.6747L130.52 73.831H124.23" stroke="#BFE2FB" />
</g>
</g>
<defs>
<radialGradient
id="paint0_radial_0_11080" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
gradientTransform="translate(66.7454 74.3253) rotate(91.4485) scale(67.9372 416.887)"
>
<stop stop-color="#67A4E1" />
<stop offset="1" stop-color="#67A4E1" stop-opacity="0.01" />
</radialGradient>
<clipPath id="clip0_0_11080">
<rect width="139.944" height="74.6747" fill="white" />
</clipPath>
</defs>
</svg>
</div>
</template>

View File

@ -0,0 +1,60 @@
<script setup lang="ts">
defineProps<{
title: string
value: number
}>()
</script>
<template>
<div class="h-[77px] relative w-[140px]">
<div class="w-full text-center text-[#E4F3FF] text-[14px] scale-95 opacity-70">
{{ title }}
</div>
<div class="bottom-0 h-[50px] flex justify-center items-center w-full absolute text-[24px] text-[#00F8F4]">
{{ value }}
</div>
<svg
class="absolute bottom-0" viewBox="0 0 140 75" fill="none" xmlns="http://www.w3.org/2000/svg"
xmlns:anim="http://www.w3.org/2000/anim" anim="" anim:transform-origin="50% 50%" anim:duration="1"
anim:ease="ease-in-out"
>
<g id="1" clip-path="url(#clip0_0_11080)">
<path
id="BG" opacity="0.39908" fill-rule="evenodd" clip-rule="evenodd"
d="M3 33.1563L9.02438 25H129.615L137.242 33.1563L137.36 66L129.615 74.3253L9.02438 74.0097L3 66L3 33.1563Z"
fill="url(#paint0_radial_0_11080)"
/>
<path id="路径 41" opacity="0.245359" d="M6.91697 70.52H133.553" stroke="white" stroke-width="0.6" />
<path
id="路径 31" opacity="0.4" d="M3.23047 32.6844L9.25485 24.6747H129.846L137.591 32.6844"
stroke="white"
/>
<path
id="路径 31_2" opacity="0.4" d="M3.23047 65.6747L9.25485 73.6844H129.846L137.591 65.6747"
stroke="white"
/>
<rect id="矩形" y="48" width="4.94438" height="2.7" rx="1" fill="#2EF9CB" />
<rect id="矩形_2" x="135" y="48" width="4.94438" height="2.7" rx="1" fill="#2EF9CB" />
<g id="四边">
<path id="路径 32" d="M3.23047 32.831L9.25485 24.6747H15.5446" stroke="#BFE2FB" />
<path id="路径 32_2" d="M136.544 32.831L130.52 24.6747H124.23" stroke="#BFE2FB" />
<path id="路径 32_3" d="M3.23047 65.6747L9.25485 73.831H15.5446" stroke="#BFE2FB" />
<path id="路径 32_4" d="M136.544 65.6747L130.52 73.831H124.23" stroke="#BFE2FB" />
</g>
</g>
<defs>
<radialGradient
id="paint0_radial_0_11080" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
gradientTransform="translate(66.7454 74.3253) rotate(91.4485) scale(67.9372 416.887)"
>
<stop stop-color="#67A4E1" />
<stop offset="1" stop-color="#67A4E1" stop-opacity="0.01" />
</radialGradient>
<clipPath id="clip0_0_11080">
<rect width="139.944" height="74.6747" fill="white" />
</clipPath>
</defs>
</svg>
</div>
</template>

View File

@ -0,0 +1,211 @@
<script setup lang="ts">
defineProps<{
title: string
value: number
unit: string
icon: string
}>()
</script>
<template>
<div class=" w-[196px] h-[75px] relative">
<div class="absolute top-0 left-20 opacity-60 text-[14px]">
{{ title }}
</div>
<div class="absolute top-6 left-[88px] flex justify-center items-center gap-1">
<span class=" text-[22px]">{{ value }}</span>
<span class="opacity-60 text-[14px]">{{ unit }}</span>
</div>
<div class="">
<Iconify :icon="icon" class=" w-[32px] h-[32px] absolute top-[12px] left-[12px]" />
</div>
<svg
viewBox="0 0 196 75" fill="none" class="absolute top-0 left-0 -z-[1]" xmlns="http://www.w3.org/2000/svg"
xmlns:anim="http://www.w3.org/2000/anim" anim="" anim:transform-origin="50% 50%" anim:duration="1"
anim:ease="ease-in-out"
>
<g id="1" clip-path="url(#clip0_0_11033)">
<path
id="矩形"
d="M75.5131 27.0529H188.544L173.645 57.7836H60.9809C59.8423 57.7836 59.1188 56.5649 59.6641 55.5653L75.0742 27.3134C75.1618 27.1528 75.3302 27.0529 75.5131 27.0529Z"
fill="url(#paint0_radial_0_11033)" fill-opacity="0.2" stroke="url(#paint1_linear_0_11033)"
/>
<path
id="å¡«å……" fill-rule="evenodd" clip-rule="evenodd"
d="M80.4048 24.1894C80.58 23.8681 80.9167 23.6682 81.2827 23.6682H195.112L179.728 55.399H66.7504C65.2323 55.399 64.2677 53.774 64.9946 52.4413L80.4048 24.1894Z"
fill="url(#paint2_linear_0_11033)" fill-opacity="0.5"
/>
<g id="倒影">
<mask
id="mask0_0_11033" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="59" y="58" width="106"
height="17"
>
<path
id="矩形_2" fill-rule="evenodd" clip-rule="evenodd"
d="M59.5352 58.2836H164.343V74.6298H59.5352V58.2836Z" fill="url(#paint3_linear_0_11033)"
/>
</mask>
<g mask="url(#mask0_0_11033)">
<path
id="å¡«å……_2" fill-rule="evenodd" clip-rule="evenodd"
d="M76.5586 88.5317C76.7338 88.853 77.0705 89.0529 77.4365 89.0529H191.266L175.881 57.3221H62.9042C61.3861 57.3221 60.4215 58.947 61.1484 60.2798L76.5586 88.5317Z"
fill="url(#paint4_linear_0_11033)" fill-opacity="0.5"
/>
</g>
</g>
<path
id="边框"
d="M81.2827 24.1682H194.314L179.414 54.899H66.7504C65.6118 54.899 64.8884 53.6803 65.4336 52.6807L80.8437 24.4288C80.9313 24.2682 81.0997 24.1682 81.2827 24.1682Z"
stroke="url(#paint5_linear_0_11033)"
/>
<g id="编组 4">
<g id="座">
<g id="底部模糊" opacity="0.87" filter="url(#filter0_f_0_11033)">
<path
fill-rule="evenodd" clip-rule="evenodd"
d="M27.9334 39.7012C28.2471 39.5159 28.6367 39.5159 28.9504 39.7012L45.243 49.3224C46.5537 50.0964 46.5537 51.9927 45.243 52.7667L28.9504 62.3879C28.6367 62.5731 28.2471 62.5732 27.9334 62.3879L11.6408 52.7667C10.3301 51.9927 10.3301 50.0964 11.6408 49.3224L27.9334 39.7012Z"
fill="url(#paint6_linear_0_11033)"
/>
</g>
<path
id="深层" fill-rule="evenodd" clip-rule="evenodd"
d="M28.0706 28.5854C28.3843 28.4001 28.7739 28.4001 29.0876 28.5854L53.8416 43.2032C55.1523 43.9772 55.1523 45.8735 53.8416 46.6475L29.0876 61.2654C28.7739 61.4506 28.3843 61.4506 28.0706 61.2654L3.31665 46.6475C2.00594 45.8735 2.00594 43.9772 3.31665 43.2032L28.0706 28.5854Z"
fill="url(#paint7_linear_0_11033)" fill-opacity="0.6"
/>
<path
id="基层"
d="M28.935 23.0745L53.689 37.6923C54.8031 38.3502 54.8031 39.9621 53.689 40.62L28.935 55.2378C28.7155 55.3675 28.4427 55.3675 28.2232 55.2378L3.4692 40.62C2.3551 39.9621 2.35509 38.3502 3.4692 37.6923L28.2232 23.0745C28.4427 22.9448 28.7155 22.9448 28.935 23.0745Z"
fill="url(#paint8_radial_0_11033)" stroke="url(#paint9_linear_0_11033)" stroke-width="0.6"
/>
</g>
<g id="椭圆形" filter="url(#filter1_df_0_11033)">
<ellipse
cx="27.9068" cy="36.0023" rx="11.1538" ry="7.88462" fill="url(#paint10_linear_0_11033)"
fill-opacity="0.4"
/>
</g>
</g>
</g>
<defs>
<filter
id="filter0_f_0_11033" x="-5.65198" y="23.2526" width="68.1877" height="55.584"
filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="8.15485" result="effect1_foregroundBlur_0_11033" />
</filter>
<filter
id="filter1_df_0_11033" x="11.7529" y="25.3994" width="30.3076" height="27.4875"
filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dx="-1" dy="5" />
<feGaussianBlur stdDeviation="2" />
<feColorMatrix type="matrix" values="0 0 0 0 0.0700061 0 0 0 0 0.128382 0 0 0 0 0.238646 0 0 0 0.5 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_11033" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_11033" result="shape" />
<feGaussianBlur stdDeviation="1.35914" result="effect2_foregroundBlur_0_11033" />
</filter>
<radialGradient
id="paint0_radial_0_11033" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
gradientTransform="translate(66.8013 53.1895) rotate(90) scale(14.2635 86.1863)"
>
<stop stop-color="#466C90" />
<stop offset="1" stop-color="#183F64" stop-opacity="0.01" />
</radialGradient>
<linearGradient
id="paint1_linear_0_11033" x1="106.274" y1="66.5687" x2="107.89" y2="42.6304"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="white" stop-opacity="0.08" />
<stop offset="0.49066" stop-color="white" stop-opacity="0.3" />
<stop offset="1" stop-color="white" stop-opacity="0.01" />
</linearGradient>
<linearGradient
id="paint2_linear_0_11033" x1="37.4551" y1="6.637" x2="26.2427" y2="80.4089"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#031518" />
<stop offset="0.404838" stop-color="#446D9C" />
<stop offset="1" stop-color="#2E6376" stop-opacity="0.01" />
</linearGradient>
<linearGradient
id="paint3_linear_0_11033" x1="77.816" y1="58.2836" x2="77.816" y2="68.9275"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#EEEEEE" />
<stop offset="1" stop-color="#D8D8D8" stop-opacity="0.01" />
</linearGradient>
<linearGradient
id="paint4_linear_0_11033" x1="33.6089" y1="106.084" x2="22.3965" y2="32.3122"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#031518" />
<stop offset="0.404838" stop-color="#446D9C" />
<stop offset="1" stop-color="#2E6376" stop-opacity="0.01" />
</linearGradient>
<linearGradient
id="paint5_linear_0_11033" x1="199.904" y1="46.5626" x2="196.423" y2="10.3945"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="white" stop-opacity="0.01" />
<stop offset="0.830588" stop-color="white" stop-opacity="0.3" />
<stop offset="1" stop-color="white" stop-opacity="0.01" />
</linearGradient>
<linearGradient
id="paint6_linear_0_11033" x1="19.3075" y1="55.1144" x2="12.6648" y2="73.8499"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#6AD4D1" />
<stop offset="1" stop-color="#5E96B8" stop-opacity="0.01" />
</linearGradient>
<linearGradient
id="paint7_linear_0_11033" x1="15.5249" y1="50.7417" x2="6.03154" y2="77.5172"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#1B2D4F" />
<stop offset="1" stop-color="#1B2D4F" stop-opacity="0.01" />
</linearGradient>
<radialGradient
id="paint8_radial_0_11033" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
gradientTransform="translate(28.9127 49.7191) rotate(109.522) scale(28.4087)"
>
<stop stop-color="#67A4E1" />
<stop offset="1" stop-color="#67A4E1" stop-opacity="0.01" />
</radialGradient>
<linearGradient
id="paint9_linear_0_11033" x1="12.0455" y1="23.1229" x2="12.8914" y2="54.368"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#DDFFFF" stop-opacity="0.01" />
<stop offset="1" stop-color="#DDFFFF" />
</linearGradient>
<linearGradient
id="paint10_linear_0_11033" x1="16.7529" y1="28.1177" x2="16.7529" y2="43.8869"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#DDFFFF" stop-opacity="0.01" />
<stop offset="1" stop-color="#80BAF3" />
</linearGradient>
<linearGradient
id="paint11_linear_0_11033" x1="12.0304" y1="10.0516" x2="12.0304" y2="37.1307"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="white" />
<stop offset="1" stop-color="#6AF6FA" />
</linearGradient>
<clipPath id="clip0_0_11033">
<rect width="194.712" height="74.0144" fill="white" transform="translate(0.400391 0.615356)" />
</clipPath>
</defs>
</svg>
</div>
</template>

View File

@ -0,0 +1,102 @@
<script setup lang="ts">
defineProps<{
title: string
value: number
icon: string
}>()
</script>
<template>
<div class="w-[257px] h-[89px] relative">
<div class="flex w-full h-full">
<div class="flex justify-center items-center w-[82px] h-full">
<Iconify :icon="icon" class="w-[50px] h-[50px]" />
</div>
<div class="flex flex-col items-start pl-[20px] justify-center flex-1 w-full h-full">
<div class="text-[#00F8F4] text-[34px] inline-block align-baseline">
{{ value }}
</div>
<div class="text-[#A5D8FC] text-[16px] -mt-3 pl-1">
{{ title }}
</div>
</div>
</div>
<svg viewBox="0 0 257 89" fill="none" class="absolute top-0 left-0 -z-[1]" xmlns="http://www.w3.org/2000/svg" xmlns:anim="http://www.w3.org/2000/anim" anim="" anim:transform-origin="50% 50%" anim:duration="1" anim:ease="ease-in-out">
<g id="box1">
<g id="å¡«å……">
<g filter="url(#filter0_i_0_11197)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H257V88H0V0Z" fill="#263343" fill-opacity="0.5" />
</g>
<path d="M0.5 0.5H256.5V87.5H0.5V0.5Z" stroke="#9DA3AF" stroke-opacity="0.5" />
</g>
<g id="å¡«å……_2" opacity="0.2">
<rect width="82" height="88" fill="#577B95" fill-opacity="0.75" />
<rect width="82" height="88" fill="url(#paint0_linear_0_11197)" />
</g>
<g id="高光" opacity="0.45">
<rect width="82" height="88" fill="url(#paint1_radial_0_11197)" />
<rect width="82" height="88" fill="url(#paint2_radial_0_11197)" />
</g>
<g id="编组 92">
<path id="路径 43" opacity="0.663407" fill-rule="evenodd" clip-rule="evenodd" d="M0 64.2831V88.2831H26L0 64.2831Z" fill="#253645" />
<path id="路径 48" opacity="0.0416248" fill-rule="evenodd" clip-rule="evenodd" d="M15 85.2831L35.5479 64.2831L38 85.2831H15Z" fill="#FEFFFF" />
<path id="路径 49" opacity="0.0482428" fill-rule="evenodd" clip-rule="evenodd" d="M29 87.2831L52 75.2831L48.029 87.2831H29Z" fill="#FEFFFF" />
<path id="路径 58" opacity="0.1" fill-rule="evenodd" clip-rule="evenodd" d="M78.1507 58.2831L68 86.2831L82 72.2831L78.1507 58.2831Z" fill="#FEFFFF" />
<path id="路径 59" opacity="0.135917" fill-rule="evenodd" clip-rule="evenodd" d="M59.1277 73.2831L57 87.2831H74L59.1277 73.2831Z" fill="#FEFFFF" />
</g>
<rect id="边框" opacity="0.66428" x="0.75" y="0.75" width="80.5" height="86.5" stroke="url(#paint3_linear_0_11197)" stroke-width="1.5" />
<rect id="矩形" width="4" height="4" fill="#FFF9FF" />
<rect id="矩形_2" y="84" width="4" height="4" fill="#FFF9FF" />
<rect id="矩形_3" x="253" width="4" height="4" fill="#FFF9FF" />
<rect id="矩形_4" x="253" y="84" width="4" height="4" fill="#FFF9FF" />
</g>
<defs>
<filter id="filter0_i_0_11197" x="0" y="0" width="257" height="88" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset />
<feGaussianBlur stdDeviation="2.5" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix type="matrix" values="0 0 0 0 0.577123 0 0 0 0 0.766759 0 0 0 0 0.884641 0 0 0 0.3 0" />
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_0_11197" />
</filter>
<filter id="filter1_d_0_11197" x="14" y="24" width="52" height="50.0909" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dy="2" />
<feGaussianBlur stdDeviation="1" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_11197" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_11197" result="shape" />
</filter>
<linearGradient id="paint0_linear_0_11197" x1="0" y1="0" x2="0" y2="88" gradientUnits="userSpaceOnUse">
<stop stop-color="#467591" />
<stop offset="1" stop-color="#84C3D5" />
</linearGradient>
<radialGradient id="paint1_radial_0_11197" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(41 -16.5514) rotate(90) scale(79.4466 95.9972)">
<stop stop-color="#1D3B53" />
<stop offset="0.179646" stop-color="#1E3D55" stop-opacity="0.910177" />
<stop offset="1" stop-color="#416C89" stop-opacity="0.5" />
</radialGradient>
<radialGradient id="paint2_radial_0_11197" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(76.0746 82.7784) rotate(90) scale(79.4466 74.0297)">
<stop stop-color="#ACDDE4" />
<stop offset="1" stop-color="#305877" stop-opacity="0.01" />
</radialGradient>
<linearGradient id="paint3_linear_0_11197" x1="-39.4221" y1="47.3867" x2="48.1169" y2="125.818" gradientUnits="userSpaceOnUse">
<stop stop-color="#FEFFFF" />
<stop offset="0.986769" stop-color="#FEFFFF" stop-opacity="0.01" />
</linearGradient>
<linearGradient id="paint4_linear_0_11197" x1="16.3103" y1="24.0877" x2="16.3103" y2="69.5826" gradientUnits="userSpaceOnUse">
<stop stop-color="white" />
<stop offset="1" stop-color="#9FC3C4" />
</linearGradient>
<clipPath id="clip0_0_11197">
<rect width="81" height="62" fill="white" transform="translate(103 12)" />
</clipPath>
</defs>
</svg>
</div>
</template>

View File

@ -0,0 +1,128 @@
<script setup lang="ts">
defineProps<{
title: string
value: number
unit: string
icon: string
}>()
</script>
<template>
<div class="w-[420px] h-[92px] relative">
<div class="flex items-center h-full gap-10 pt-1 pl-8">
<div class="opacity-60 text-[14px]">
{{ title }}
</div>
<div class="flex items-center gap-2">
<div class="text-[24px] text-[#00F8F4]">
{{ value }}
</div>
<div class="opacity-60 text-[14px]">
{{ unit }}
</div>
</div>
</div>
<Iconify :icon="icon" class="absolute top-1/2 right-9 -translate-y-1/2 w-[45px] h-[45px]" />
<svg
viewBox="0 0 420 92" fill="none" class="absolute top-0 left-0 -z-[1]" xmlns="http://www.w3.org/2000/svg"
xmlns:anim="http://www.w3.org/2000/anim" anim="" anim:transform-origin="50% 50%" anim:duration="1"
anim:ease="ease-in-out"
>
<g id="园区总人数" clip-path="url(#clip0_0_10780)">
<rect id="矩形" opacity="0.39908" width="420" height="92" rx="46" fill="url(#paint0_linear_0_10780)" />
<g id="车位 ‡å¿—">
<circle
id="椭圆形" opacity="0.5" cx="360.745" cy="45.9989" r="35.2706"
stroke="url(#paint1_linear_0_10780)"
/>
<path
id="形状" opacity="0.5"
d="M383.869 65.3988L383.226 66.1646L383.992 66.8077L384.635 66.0419L383.869 65.3988ZM390.928 45.9989H391.928V44.9989H390.928V45.9989ZM383.103 64.7557L382.337 64.1126L381.694 64.8784L382.46 65.5215L383.103 64.7557ZM389.928 45.9989V44.9989H388.928V45.9989H389.928ZM332.909 34.3088L333.297 33.3869L332.375 32.9993L331.988 33.9212L332.909 34.3088ZM333.831 34.6964L334.753 35.084L335.141 34.1621L334.219 33.7745L333.831 34.6964ZM332.295 52.5294L332.518 53.5042L333.493 53.2814L333.27 52.3065L332.295 52.5294ZM331.321 52.7522L330.346 52.975L330.568 53.9499L331.543 53.7271L331.321 52.7522ZM384.512 64.6331L384.359 64.5044L383.073 66.036L383.226 66.1646L384.512 64.6331ZM383.899 64.1186L383.746 63.9899L382.46 65.5215L382.613 65.6501L383.899 64.1186ZM383.869 65.3988C384.095 65.13 384.316 64.8572 384.532 64.5806L382.957 63.3483C382.755 63.6067 382.548 63.8615 382.337 64.1126L383.869 65.3988ZM386.346 61.9942C386.718 61.3999 387.07 60.7917 387.401 60.1705L385.636 59.2302C385.327 59.8101 384.998 60.3779 384.651 60.9329L386.346 61.9942ZM388.738 57.3075C389.001 56.6583 389.241 55.9979 389.459 55.3272L387.557 54.7097C387.354 55.3356 387.129 55.9519 386.884 56.5578L388.738 57.3075ZM390.275 52.2744C390.42 51.5907 390.541 50.8986 390.638 50.1989L388.658 49.9231C388.567 50.5764 388.453 51.2225 388.318 51.8606L390.275 52.2744ZM390.91 47.0521C390.922 46.7025 390.928 46.3514 390.928 45.9989H388.928C388.928 46.3285 388.923 46.6567 388.911 46.9835L390.91 47.0521ZM389.928 46.9989H390.128V44.9989H389.928V46.9989ZM390.728 46.9989H390.928V44.9989H390.728V46.9989ZM389.928 45.9989C389.928 46.3402 389.922 46.68 389.911 47.0184L391.91 47.087C391.922 46.7258 391.928 46.363 391.928 45.9989H389.928ZM389.648 50.0623C389.554 50.7388 389.436 51.4078 389.296 52.0686L391.253 52.4825C391.402 51.7762 391.528 51.0611 391.629 50.3382L389.648 50.0623ZM388.508 55.0189C388.297 55.667 388.065 56.3052 387.811 56.9326L389.665 57.6823C389.937 57.0117 390.185 56.3294 390.41 55.6365L388.508 55.0189ZM386.519 59.6999C386.199 60.3004 385.859 60.8885 385.499 61.4631L387.194 62.5244C387.578 61.9104 387.942 61.282 388.284 60.6402L386.519 59.6999ZM383.745 63.9643C383.535 64.2319 383.321 64.4957 383.103 64.7557L384.635 66.0419C384.868 65.7642 385.096 65.4824 385.32 65.1965L383.745 63.9643ZM331.988 33.9212C331.828 34.3016 331.675 34.6859 331.53 35.0738L333.403 35.7746C333.539 35.4117 333.682 35.0522 333.831 34.6964L331.988 33.9212ZM330.438 38.6289C330.245 39.4278 330.082 40.2388 329.951 41.0603L331.926 41.3747C332.049 40.606 332.201 39.8473 332.382 39.1L330.438 38.6289ZM329.586 44.7598C329.57 45.1709 329.562 45.584 329.562 45.9989H331.562C331.562 45.61 331.57 45.223 331.585 44.8379L329.586 44.7598ZM329.562 45.9989C329.562 46.47 329.572 46.9387 329.593 47.4049L331.591 47.3161C331.572 46.8795 331.562 46.4404 331.562 45.9989H329.562ZM330.063 51.5964C330.147 52.0596 330.241 52.5192 330.346 52.975L332.295 52.5294C332.198 52.1029 332.11 51.6729 332.031 51.2396L330.063 51.5964ZM331.543 53.7271L331.738 53.6825L331.293 51.7328L331.098 51.7773L331.543 53.7271ZM332.323 53.5488L332.518 53.5042L332.073 51.5545L331.878 51.5991L332.323 53.5488ZM333.27 52.3065C333.176 51.8947 333.091 51.4794 333.015 51.0608L331.047 51.4176C331.128 51.866 331.22 52.3109 331.321 52.7522L333.27 52.3065ZM332.59 47.2712C332.571 46.8495 332.562 46.4254 332.562 45.9989H330.562C330.562 46.4549 330.572 46.9087 330.592 47.3599L332.59 47.2712ZM332.562 45.9989C332.562 45.6233 332.569 45.2496 332.584 44.8777L330.585 44.7996C330.57 45.1975 330.562 45.5973 330.562 45.9989H332.562ZM332.914 41.5332C333.032 40.7908 333.179 40.0582 333.354 39.3365L331.41 38.8655C331.223 39.6387 331.065 40.4236 330.938 41.2189L332.914 41.5332ZM334.34 36.1252C334.471 35.7747 334.609 35.4276 334.753 35.084L332.909 34.3088C332.755 34.677 332.607 35.0489 332.466 35.4244L334.34 36.1252ZM334.219 33.7745L334.034 33.697L333.259 35.5407L333.444 35.6182L334.219 33.7745ZM333.481 33.4645L333.297 33.3869L332.522 35.2306L332.706 35.3081L333.481 33.4645Z"
fill="url(#paint2_linear_0_10780)"
/>
<path
id="形状_2"
d="M371.334 85.4858L369.39 85.959L369.864 87.9022L371.807 87.429L371.334 85.4858ZM399.915 60.689L401.771 61.4335L402.516 59.5773L400.66 58.8327L399.915 60.689ZM371.097 84.5142L370.624 82.571L368.681 83.0441L369.154 84.9874L371.097 84.5142ZM398.987 60.3167L399.732 58.4604L397.875 57.7159L397.131 59.5721L398.987 60.3167ZM397.096 24.2053L398.815 23.1837L397.794 21.4643L396.074 22.4859L397.096 24.2053ZM400.99 57.6733L400.393 59.5819L402.301 60.1795L402.899 58.2709L400.99 57.6733ZM400.036 57.3745L398.127 56.7768L397.53 58.6855L399.438 59.2831L400.036 57.3745ZM396.236 24.7161L395.215 22.9967L393.495 24.0182L394.517 25.7377L396.236 24.7161ZM371.807 87.429C385.485 84.0984 396.616 74.2871 401.771 61.4335L398.059 59.9444C393.378 71.6138 383.269 80.5213 370.86 83.5426L371.807 87.429ZM369.154 84.9874L369.39 85.959L373.277 85.0126L373.04 84.041L369.154 84.9874ZM397.131 59.5721C392.569 70.9455 382.714 79.627 370.624 82.571L371.57 86.4574C384.931 83.2041 395.806 73.6188 400.843 61.0612L397.131 59.5721ZM400.66 58.8327L399.732 58.4604L398.242 62.1729L399.171 62.5452L400.66 58.8327ZM404.873 45.3005C404.873 37.2265 402.664 29.661 398.815 23.1837L395.376 25.2269C398.868 31.103 400.873 37.9653 400.873 45.3005H404.873ZM402.899 58.2709C404.182 54.1724 404.873 49.8147 404.873 45.3005H400.873C400.873 49.4047 400.245 53.3595 399.082 57.0757L402.899 58.2709ZM399.438 59.2831L400.393 59.5819L401.588 55.7647L400.634 55.4658L399.438 59.2831ZM399.873 45.3005C399.873 49.3022 399.261 53.1563 398.127 56.7768L401.945 57.9721C403.198 53.9692 403.873 49.7122 403.873 45.3005H399.873ZM394.517 25.7377C397.919 31.4635 399.873 38.15 399.873 45.3005H403.873C403.873 37.4112 401.715 30.0215 397.956 23.6945L394.517 25.7377ZM396.074 22.4859L395.215 22.9967L397.258 26.4355L398.117 25.9247L396.074 22.4859Z"
fill="url(#paint3_linear_0_10780)"
/>
<path
id="形状_3"
d="M350.428 6.51419L352.372 6.04102L351.898 4.0978L349.955 4.57097L350.428 6.51419ZM321.847 31.311L319.991 30.5665L319.246 32.4227L321.102 33.1673L321.847 31.311ZM350.665 7.4858L351.138 9.42902L353.081 8.95585L352.608 7.01263L350.665 7.4858ZM322.775 31.6833L322.03 33.5396L323.887 34.2841L324.631 32.4279L322.775 31.6833ZM324.666 67.7947L322.947 68.8163L323.968 70.5357L325.688 69.5141L324.666 67.7947ZM320.772 34.3267L321.369 32.4181L319.461 31.8205L318.863 33.7291L320.772 34.3267ZM321.726 34.6255L323.635 35.2232L324.232 33.3145L322.324 32.7169L321.726 34.6255ZM325.526 67.2839L326.547 69.0033L328.267 67.9818L327.245 66.2623L325.526 67.2839ZM349.955 4.57097C336.277 7.90163 325.146 17.7129 319.991 30.5665L323.703 32.0556C328.384 20.3862 338.493 11.4787 350.902 8.45741L349.955 4.57097ZM352.608 7.01263L352.372 6.04102L348.485 6.98736L348.722 7.95897L352.608 7.01263ZM324.631 32.4279C329.193 21.0545 339.048 12.373 351.138 9.42902L350.192 5.54258C336.831 8.79591 325.956 18.3812 320.919 30.9388L324.631 32.4279ZM321.102 33.1673L322.03 33.5396L323.519 29.8271L322.591 29.4548L321.102 33.1673ZM316.889 46.6995C316.889 54.7735 319.098 62.339 322.947 68.8163L326.385 66.7731C322.894 60.897 320.889 54.0347 320.889 46.6995H316.889ZM318.863 33.7291C317.58 37.8276 316.889 42.1853 316.889 46.6995H320.889C320.889 42.5953 321.517 38.6405 322.68 34.9243L318.863 33.7291ZM322.324 32.7169L321.369 32.4181L320.174 36.2353L321.128 36.5342L322.324 32.7169ZM321.889 46.6995C321.889 42.6978 322.501 38.8437 323.635 35.2232L319.817 34.0279C318.564 38.0308 317.889 42.2878 317.889 46.6995H321.889ZM327.245 66.2623C323.843 60.5365 321.889 53.85 321.889 46.6995H317.889C317.889 54.5888 320.047 61.9785 323.806 68.3055L327.245 66.2623ZM325.688 69.5141L326.547 69.0033L324.504 65.5645L323.645 66.0753L325.688 69.5141Z"
fill="url(#paint4_linear_0_10780)"
/>
</g>
</g>
<defs>
<filter
id="filter0_f_0_10780" x="330.537" y="23.7524" width="62.4582" height="52.2006"
filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="8.15485" result="effect1_foregroundBlur_0_10780" />
</filter>
<linearGradient
id="paint0_linear_0_10780" x1="0.360291" y1="73.2461" x2="419.678" y2="56.3299"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#67A4E1" stop-opacity="0.33" />
<stop offset="0.195436" stop-color="#67A4E1" stop-opacity="0.01" />
<stop offset="0.535357" stop-color="#67A4E1" stop-opacity="0.01" />
<stop offset="1" stop-color="#67A4E1" stop-opacity="0.9" />
</linearGradient>
<linearGradient
id="paint1_linear_0_10780" x1="317.83" y1="30.6611" x2="344.44" y2="79.3588"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#DFF7FF" stop-opacity="0.01" />
<stop offset="0.260824" stop-color="#DFF7FF" />
<stop offset="0.623539" stop-color="#DFF7FF" />
<stop offset="1" stop-color="#DFF7FF" stop-opacity="0.01" />
</linearGradient>
<linearGradient
id="paint2_linear_0_10780" x1="336.552" y1="15.3195" x2="333.307" y2="57.9034"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#DFF7FF" stop-opacity="0.01" />
<stop offset="0.280256" stop-color="#DFF7FF" />
<stop offset="0.634379" stop-color="#DFF7FF" />
<stop offset="1" stop-color="#DFF7FF" stop-opacity="0.01" />
</linearGradient>
<linearGradient
id="paint3_linear_0_10780" x1="364.727" y1="41.5865" x2="400.264" y2="73.2964"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="white" />
<stop offset="0.262294" stop-color="#DFF7FF" />
<stop offset="1" stop-color="#DFF7FF" stop-opacity="0.01" />
</linearGradient>
<linearGradient
id="paint4_linear_0_10780" x1="357.035" y1="50.4135" x2="321.498" y2="18.7036"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="white" />
<stop offset="0.262294" stop-color="#DFF7FF" />
<stop offset="1" stop-color="#DFF7FF" stop-opacity="0.01" />
</linearGradient>
<linearGradient
id="paint5_linear_0_10780" x1="340.699" y1="26.5818" x2="340.699" y2="62.2006"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="white" />
<stop offset="1" stop-color="#6AF6FA" stop-opacity="0.2" />
</linearGradient>
<linearGradient
id="paint6_linear_0_10780" x1="353.959" y1="53.3313" x2="348.281" y2="69.3448"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#6AD4D1" />
<stop offset="1" stop-color="#5E96B8" stop-opacity="0.01" />
</linearGradient>
<clipPath id="clip0_0_10780">
<rect width="420" height="92" fill="white" />
</clipPath>
</defs>
</svg>
</div>
</template>

View File

@ -0,0 +1,47 @@
<script setup lang="ts">
defineProps<{
title: string
value: number
icon: string
}>()
</script>
<template>
<div class="w-[96px] h-[66px] relative">
<div class="space-y-3 ">
<div class="flex items-end gap-3">
<Iconify :icon="icon" class="w-[38px] h-[38px] " />
<div class="text-[24px] text-shadow">
{{ value }}
</div>
</div>
<div>
{{ title }}
</div>
</div>
<svg viewBox="0 0 96 66" class="absolute top-0 left-0 w-full h-full " fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:anim="http://www.w3.org/2000/anim" anim="" anim:transform-origin="50% 50%" anim:duration="1" anim:ease="ease-in-out">
<g id="编组 12" clip-path="url(#clip0_0_10738)">
<g id="icon_bar" opacity="0.5">
<path id="直线 20" d="M0.646341 44.0854H88.5488" stroke="white" stroke-linecap="square" />
<circle id="椭圆形" cx="92.4268" cy="44.0853" r="2.73171" stroke="white" />
</g>
</g>
<defs>
<linearGradient id="paint0_linear_0_10738" x1="4.02103" y1="0.0608013" x2="4.02103" y2="31.9686" gradientUnits="userSpaceOnUse">
<stop stop-color="white" />
<stop offset="1" stop-color="#6AF6FA" />
</linearGradient>
<clipPath id="clip0_0_10738">
<rect width="95.6585" height="66" fill="white" />
</clipPath>
</defs>
</svg>
</div>
</template>
<style scoped>
.text-shadow{
text-shadow: 0px 1px 6px #84F8FB;
}
</style>

View File

@ -0,0 +1,32 @@
<script setup lang="ts">
defineProps<{
error:any
}>()
const emit = defineEmits<{
(e:'retry'):Promise<any>
}>()
const { isPending, start, stop } = useTimeoutFn(async () => {
await emit('retry')
}, 3000,{
immediate:false
})
</script>
<template>
<div class=" flex flex-col items-center justify-center gap-2 rounded bg-gray-600/5 py-5">
<UIcon name="i-heroicons-exclamation-triangle-16-solid" class=" h-10 w-10 text-rose-500 dark:text-rose-700 " />
<p class="max-w-[500px] text-center font-semibold typography-muted">
{{ error }}
</p>
<UButton variant="soft" :loading="isPending" @click="start()">
<span class="tracking-[5px]">{{ $t('try-again') }}</span>
</UButton>
</div>
</template>

View File

@ -0,0 +1,95 @@
<script setup lang="ts">
const emit = defineEmits<{
(e: 'submit', value: {
Token: string
Remember: boolean
}): void
}>()
const { data, refresh } = useAsyncData(authApi.captcha)
const schema = z.object({
UserName: z.string({
required_error: '账号不能为空',
}),
Password: z.string({
required_error: '密码不能为空',
}),
Code: z.string({
required_error: '验证码不能为空',
}),
Remember: z.boolean().optional(),
})
const onSubmit: any = async (values: ZodInfer<typeof schema>) => {
try {
const res = await authApi.login({ ...values, UUID: data.value?.uuid ?? '' })
//
if (res.Token) {
emit('submit', { ...res, Remember: !!values.Remember })
}
else {
//
// modal.open(Security, {
// initial: res,
// onSubmit(result) {
// emit('submit', { ...result, Remember: event.data.Remember })
// }
// })
}
}
catch (err: any) {
toast.error('登录失败', {
description: err.message,
})
}
refresh()
}
</script>
<template>
<AutoForm
class="space-y-4" :schema="schema" :field-config="{
UserName: {
hideLabel: true,
inputProps: {
placeholder: '请填写账号',
},
},
Password: {
hideLabel: true,
inputProps: {
type: 'password',
placeholder: '请填写密码',
},
},
Code: {
hideLabel: true,
inputProps: {
placeholder: '请填写验证码',
},
},
Remember: {
label: '记住登录',
inputProps: {
required: true,
},
},
}" @submit="onSubmit"
>
<template #Code="slotProps">
<div class="flex items-start gap-3">
<AutoFormField v-bind="slotProps" />
<Button
class="w-[150px] bg-no-repeat" type="button"
:style="{ backgroundSize: '100% 100%', backgroundImage: `url(data:image/png;base64,${data?.imgByte})` }"
@click="refresh"
/>
</div>
</template>
<Button type="submit" class="w-full font-bold">
登录
</Button>
</AutoForm>
</template>

View File

@ -0,0 +1,80 @@
<script setup lang="ts">
const emit = defineEmits<{
(e: 'submit'): void
}>()
const verifyName = verify.test(
// @ts-ignore
new RegExp(/^[a-z]{1}(?=.*[a-z])[a-z\d\-]{4,18}[a-z\d]{1}$/)
)
const schema = z.object({
UserName: z.string({
required_error: '账号不能为空'
})
.refine(verifyName, '名称应该以6-20位的小写字母组成允许其中包含数字、连接符-')
.refine(str => str !== 'root' && str !== 'jwanfs', '不允许起名关键字root、jwanfs'),
Email: z.string({
required_error: '请填写邮箱'
}).email('邮箱格式有误'),
Code: z.string({
required_error: '验证码不能为空'
}),
})
const { data, refresh } = useAsyncData(authApi.captcha)
const onSubmit: any = async (values: ZodInfer<typeof schema>) => {
try {
await authApi.register({ ...event, RandomPass: true, Uuid: data.value?.uuid ?? '' })
emit('submit')
} catch (err: any) {
toast.error("操作失败", {
description: err.message
})
}
refresh()
}
</script>
<template>
<AutoForm class="space-y-4" :schema="schema" :field-config="{
UserName: {
hideLabel: true,
inputProps: {
placeholder: '请填写账号',
},
},
Email: {
hideLabel: true,
inputProps: {
type: 'email',
placeholder: '请填写邮箱',
},
},
Code: {
hideLabel: true,
inputProps: {
placeholder: '请填写验证码',
},
},
}" @submit="onSubmit">
<template #Code="slotProps">
<div class="flex items-start gap-3">
<AutoFormField v-bind="slotProps" />
<Button class="w-[150px] bg-no-repeat" type="button"
:style="{ backgroundSize: '100% 100%', backgroundImage: `url(data:image/png;base64,${data?.imgByte})` }"
@click="refresh" />
</div>
</template>
<Button type="submit" class="font-bold w-full">
登录
</Button>
</AutoForm>
</template>

View File

@ -0,0 +1,109 @@
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
const emit = defineEmits<{
(e: 'submit', value: {
Token: string
Remember: boolean
}): void
}>()
const schema = z.object({
Email: z.string({
required_error: '请填写邮箱',
}).email('请填写正确的邮箱'),
Code: z.string({
required_error: '请填写验证码',
}).length(4, '请填写4个字符的验证码'),
Remember: z.boolean().optional(),
})
const form = useForm({
validationSchema: toTypedSchema(schema),
})
const state = reactive({
Email: undefined,
Code: undefined,
Remember: false,
})
//
const loading = ref(false)
async function handleSuccessAndSendMSG() {
loading.value = true
//
try {
await authApi.sendCode({
Email: state.Email,
})
toast.success('验证码发送成功')
}
catch (err: any) {
toast.error(err.message)
}
loading.value = false
}
async function verifyEmail(res: (open: boolean) => void) {
try {
//
await form.validateField('Email')
res(true)
}
catch (err: any) {
if (!err.message.includes('Email'))
res(true)
}
}
const onSubmit: any = async (event: ZodInfer<typeof schema>) => {
try {
const res = await authApi.loginByDevice(event)
emit('submit', { ...res, Remember: !!event.Remember })
}
catch (err: any) {
toast.error('登录失败', {
description: err.message,
})
}
}
</script>
<template>
<AutoForm
class="space-y-4" :form="form" :schema="schema" :field-config="{
Email: {
hideLabel: true,
inputProps: {
placeholder: '请填写邮箱',
},
},
Code: {
hideLabel: true,
inputProps: {
placeholder: '请填写验证码',
},
},
Remember: {
label: '记住登录',
inputProps: {
required: true,
},
},
}" @submit="onSubmit"
>
<template #Code="slotProps">
<div class="flex items-start gap-3">
<AutoFormField v-bind="slotProps" />
<CaptchaVerifyButton :loading="loading" @verify="verifyEmail" @success="handleSuccessAndSendMSG" />
</div>
</template>
<Button type="submit" class="w-full font-bold">
登录
</Button>
</AutoForm>
</template>

View File

@ -0,0 +1,111 @@
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
const emit = defineEmits<{
(e: 'submit', value: {
Token: string
Remember: boolean
}): void
}>()
const schema = z.object({
Phone: z.string({
required_error: '请填写手机',
}).refine(str => verify.phone(str), '请填写正确的手机号'),
Code: z.string({
required_error: '请填写验证码',
}).length(4, '请填写4个字符的验证码'),
Remember: z.boolean().optional(),
})
const form = useForm({
validationSchema: toTypedSchema(schema),
})
const state = reactive({
Phone: undefined,
Code: undefined,
Remember: false,
})
//
const loading = ref(false)
async function handleSuccessAndSendMSG() {
loading.value = true
//
try {
await authApi.sendCode({
Phone: state.Phone,
})
toast.success('验证码发送成功')
}
catch (err: any) {
toast.error('验证码发送失败', {
description: err.message,
})
}
loading.value = false
}
async function verifyPhone(res: (open: boolean) => void) {
try {
//
await form.validateField('Phone')
res(true)
}
catch (err: any) {
if (!err.message.includes('Phone'))
res(true)
}
}
const onSubmit: any = async (event: ZodInfer<typeof schema>) => {
try {
const res = await authApi.loginByDevice(event)
emit('submit', { ...res, Remember: !!event.Remember })
}
catch (err: any) {
toast.error('登录失败', {
description: err.message,
})
}
}
</script>
<template>
<AutoForm
class="space-y-4" :form="form" :schema="schema" :field-config="{
Phone: {
hideLabel: true,
inputProps: {
placeholder: '请填写手机',
},
},
Code: {
hideLabel: true,
inputProps: {
placeholder: '请填写验证码',
},
},
Remember: {
label: '记住登录',
inputProps: {
required: true,
},
},
}" @submit="onSubmit"
>
<template #Code="slotProps">
<div class="flex items-start gap-3">
<AutoFormField v-bind="slotProps" />
<CaptchaVerifyButton :loading="loading" @verify="verifyPhone" @success="handleSuccessAndSendMSG" />
</div>
</template>
<Button type="submit" class="w-full font-bold">
登录
</Button>
</AutoForm>
</template>

View File

@ -0,0 +1,118 @@
<script setup lang="ts">
const emit = defineEmits<{
(e: 'submit'): void
}>()
const verifyName = verify.test(
new RegExp(/^[a-z]{1}(?=.*[a-z])[a-z\d\-]{4,18}[a-z\d]{1}$/)
)
const { data: auditors, pending } = useAsyncData(authApi.auditors, {
default: () => [],
})
const enumAuditors = computed(() => auditors.value.map(i => String(i.Id)) ?? [''])
const schema = computed(() => z.object({
UserName: z.string({
required_error: '账号不能为空'
})
.refine(verifyName, '名称应该以6-20位的小写字母组成允许其中包含数字、连接符-')
.refine(str => str !== 'root' && str !== 'jwanfs', '不允许起名关键字root、jwanfs'),
Email: z.string({
required_error: '请填写邮箱'
}).email('邮箱格式有误'),
Password: z.string({
required_error: '密码不能为空'
}),
Confirm: z.string({
required_error: '确认密码不能为空'
}),
SysUserId: z.enum(enumAuditors.value as [string],{
required_error: '请选择审核人'
}),
Code: z.string({
required_error: '验证码不能为空'
})
}).refine(({ Confirm, Password }) => Confirm === Password, {
message: '两次密码输入不一致',
path: ['Confirm']
}))
const { data, refresh } = useAsyncData(authApi.captcha)
const toast = useToastHandle()
async function onSubmit(event: ZodInfer<typeof schema.value>) {
try {
await authApi.register({ ...event, Uuid: data.value?.uuid ?? '' })
emit('submit')
} catch (err: any) {
toast.error(err.message)
}
}
</script>
<template>
<AutoForm class="space-y-4" :schema="schema" :field-config="{
UserName: {
hideLabel: true,
inputProps: {
placeholder: '请填写账号',
},
},
Email: {
hideLabel: true,
inputProps: {
type: 'email',
placeholder: '请填写邮箱',
},
},
Password: {
hideLabel: true,
inputProps: {
type: 'password',
placeholder: '请填写密码',
},
},
SysUserId: {
loading: pending,
options: auditors.map(item => ({ label: item.UserName, value: item.Id })),
hideLabel: true,
inputProps: {
placeholder: '请选择审核人',
},
},
Confirm: {
hideLabel: true,
inputProps: {
type: 'password',
placeholder: '请二次确认密码',
},
},
Code: {
hideLabel: true,
inputProps: {
placeholder: '请填写验证码',
},
},
}" @submit="onSubmit">
<template #Code="slotProps">
<div class="flex items-start gap-3">
<AutoFormField v-bind="slotProps" />
<Button class="w-[150px] bg-no-repeat" type="button"
:style="{ backgroundSize: '100% 100%', backgroundImage: `url(data:image/png;base64,${data?.imgByte})` }"
@click="refresh" />
</div>
</template>
<template #SysUserId="slotProps">
<AutoFormField v-bind="slotProps" />
</template>
<Button type="submit" class="font-bold w-full">
登录
</Button>
</AutoForm>
</template>

View File

@ -0,0 +1,141 @@
<script setup lang="ts">
import type { FormSubmitEvent } from '#ui/types'
import type { output } from 'zod'
const model = defineModel({
type: Boolean
})
const form = ref()
const emit = defineEmits<{
(e: 'submit', res: {
Token: string;
}): void
}>()
const { initial } = defineProps<{
initial: {
Email?: string
Phone?: string
UserName?: string
}
}>()
const schema = z.object({
CodeType: z.string({
required_error: '请选择认证方式'
}),
Code: z.string({
required_error: '请填写验证码'
}).length(4, '请填写4个字符的验证码')
})
type Schema = output<typeof schema>
const state = reactive({
CodeType: undefined,
Code: undefined,
})
const toast = useToastHandle()
//
const loading = ref(false)
async function handleSuccessAndSendMSG(res: (open: boolean) => void) {
loading.value = true
//
try {
const params = state.CodeType === 'email' ? {
Email: initial.Email,
} : {
Phone: initial.Phone
}
await authApi.sendCode(params)
res(true)
toast.success('验证码发送成功')
} catch (err: any) {
toast.error(err.message)
}
loading.value = false
}
async function verify(res: (open: boolean) => void) {
res(true)
}
async function onSubmit(event: FormSubmitEvent<Schema>) {
loading.value = true
try {
const res = await authApi.loginByDevice({ ...event.data })
emit('submit', res)
} catch (err: any) {
toast.error(err.message)
}
model.value = false
loading.value = false
}
const options = computed(() => {
const list = []
!!initial.Email &&
list.push({
id: 'email',
name: '邮箱: ' + emailShield(initial.Email)
})
!!initial.Phone &&
list.push({
id: 'phone',
name: '手机号: ' + phoneShield(initial.Phone)
})
!!initial.UserName &&
list.push({
id: 'totp',
name: '安全验证器: ' + initial.UserName
})
return list
})
</script>
<template>
<UDashboardModal v-model="model" title="安全校验" description="请选择一种方式进行安全校验" :ui="{ width: 'sm:max-w-md' }">
<UForm ref="form" :schema="schema" :state="state" class="space-y-4 md:space-y-6" @submit="onSubmit">
<UFormGroup name="CodeType">
<USelectMenu
v-model="state.CodeType"
:options="options"
placeholder="选择认证方式"
value-attribute="id"
option-attribute="name"
/>
</UFormGroup>
<UFormGroup name="Code">
<UButtonGroup orientation="horizontal" class="w-full">
<UInput v-model="state.Code" :maxlength="4" class="flex-1" placeholder="请输入验证码" />
<VerifyButton
v-if="state.CodeType !== 'totp'"
:loading="loading"
@verify="verify"
@success="handleSuccessAndSendMSG"
/>
</UButtonGroup>
</UFormGroup>
<div class="flex justify-end">
<UButton type="submit" class="px-4">
确认
</UButton>
</div>
</UForm>
</UDashboardModal>
</template>

View File

@ -0,0 +1,121 @@
<script setup lang="ts">
import type { FormSubmitEvent } from '#ui/types'
import type { output } from 'zod'
const model = defineModel({
type: Boolean
})
const form = ref()
const emit = defineEmits<{
(e: 'submit'): void
}>()
const schema = z.object({
CodeType: z.string({
required_error: '请选择认证方式'
}),
Code: z.string({
required_error: '请填写验证码'
}).length(4, '请填写4个字符的验证码')
})
type Schema = output<typeof schema>
const state = reactive({
CodeType: undefined,
Code: undefined,
})
const toast = useToastHandle()
//
const loading = ref(false)
async function handleSuccessAndSendMSG(res: (open: boolean) => void) {
loading.value = true
//
try {
await authApi.sendSafeCode({
Type: state.CodeType!,
})
res(true)
toast.success('验证码发送成功')
} catch (err: any) {
toast.error(err.message)
}
loading.value = false
}
async function verify(res: (open: boolean) => void) {
res(true)
}
async function onSubmit(event: FormSubmitEvent<Schema>) {
loading.value = true
try {
await authApi.postSafeCheck({ ...event.data })
emit('submit')
} catch (err: any) {
toast.error(err.message)
}
model.value = false
loading.value = false
}
const { userInfo } = useUserStore()
const options = computed(() => {
const list = []
!!userInfo.Email &&
list.push({
id: 'email',
name: '邮箱: ' + emailShield(userInfo.Email)
})
!!userInfo.Phone &&
list.push({
id: 'phone',
name: '手机号: ' + phoneShield(userInfo.Phone)
})
!!userInfo.IsSafeCheck &&
list.push({
id: 'totp',
name: '安全验证器: ' + userInfo.NickName
})
return list
})
</script>
<template>
<UDashboardModal v-model="model" title="安全校验" description="请选择一种方式进行安全校验" :ui="{ width: 'sm:max-w-md' }">
<UForm ref="form" :schema="schema" :state="state" class="space-y-4 md:space-y-6" @submit="onSubmit">
<UFormGroup name="CodeType">
<USelectMenu
v-model="state.CodeType"
:options="options"
placeholder="选择认证方式"
value-attribute="id"
option-attribute="name"
/>
</UFormGroup>
<UFormGroup name="Code">
<UButtonGroup orientation="horizontal" class="w-full">
<UInput v-model="state.Code" :maxlength="4" class="flex-1" placeholder="请输入验证码" />
<VerifyButton v-if="state.CodeType !== 'totp'" :loading="loading" @verify="verify" @success="handleSuccessAndSendMSG" />
</UButtonGroup>
</UFormGroup>
<div class="flex justify-end">
<UButton type="submit" class="px-4">
确认
</UButton>
</div>
</UForm>
</UDashboardModal>
</template>

View File

@ -0,0 +1,42 @@
<script setup lang="ts">
defineProps<{
src: string
width?: string
height?: string
icon?: string
title?: string
}>()
</script>
<template>
<div class="relative overflow-hidden rounded ">
<img :src="src" :height="height" :width="width" class="object-cover w-full h-full min-h-[100px] outline-none">
<div class="absolute bottom-0 px-2 left-0 flex w-full z-10 justify-between items-center h-[26px]">
<div>
{{ title }}
</div>
<Iconify v-if="icon" :icon="icon" />
</div>
<svg viewBox="0 0 210 26" class="absolute bottom-0 w-full " fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:anim="http://www.w3.org/2000/anim" anim="" anim:transform-origin="50% 50%" anim:duration="1" anim:ease="ease-in-out">
<g id="accident_tiltle_bg1" opacity="0.577746" filter="url(#filter0_i_0_10627)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.108154 0.51355H209.302V22C209.302 24.2092 207.511 26 205.302 26H4.10816C1.89902 26 0.108154 24.2092 0.108154 22V0.51355Z" fill="url(#paint0_linear_0_10627)" />
</g>
<defs>
<filter id="filter0_i_0_10627" x="0.108154" y="0.51355" width="209.194" height="25.4865" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dy="1" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix type="matrix" values="0 0 0 0 0.654596 0 0 0 0 0.854219 0 0 0 0 1 0 0 0 1 0" />
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_0_10627" />
</filter>
<linearGradient id="paint0_linear_0_10627" x1="0.108154" y1="26" x2="209.302" y2="26" gradientUnits="userSpaceOnUse">
<stop stop-color="#09D4BC" />
<stop offset="1" stop-color="#233344" />
</linearGradient>
</defs>
</svg>
</div>
</template>

View File

@ -0,0 +1,76 @@
<template>
<svg class=" w-[460px] h-[44px] " viewBox="0 0 460 44" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:anim="http://www.w3.org/2000/anim" anim="" anim:transform-origin="50% 50%" anim:duration="1" anim:ease="ease-in-out">
<g id="Frame" clip-path="url(#clip0_69_2)">
<rect id="å¡«å……" opacity="0.516949" x="82" y="13.3857" width="378" height="27.632" fill="url(#paint0_radial_69_2)" />
<rect id="下白线" opacity="0.687887" x="82" y="39.9823" width="377.723" height="1.03544" fill="url(#paint1_linear_69_2)" />
<g id="底部模糊" opacity="0.24333" filter="url(#filter0_f_69_2)">
<ellipse cx="117.685" cy="34.566" rx="89.6854" ry="9.06599" fill="#2BA6FF" />
</g>
<g id="矩形" opacity="0.198007">
<path d="M259.868 31.5874H447.072V33.1761H250.261H250.083L249.946 33.2881L246.609 36H79.743H79.6177L79.5071 36.0592L66.2898 43.132H21.5738L10.7345 35.0983L10.6018 35H10.4368H0.5V26.5903L13.239 16.5H63.3892H247.842L259.474 31.3951L259.624 31.5874H259.868Z" stroke="url(#paint2_linear_69_2)" />
<path d="M259.868 31.5874H447.072V33.1761H250.261H250.083L249.946 33.2881L246.609 36H79.743H79.6177L79.5071 36.0592L66.2898 43.132H21.5738L10.7345 35.0983L10.6018 35H10.4368H0.5V26.5903L13.239 16.5H63.3892H247.842L259.474 31.3951L259.624 31.5874H259.868Z" stroke="url(#paint3_radial_69_2)" />
</g>
<g id="å¡«å……_2">
<path d="M311.386 28.5874L313.227 30.1761H250.261H250.083L249.946 30.2881L246.609 33H79.743H79.6177L79.5071 33.0592L66.2898 40.132H21.5738L10.7345 32.0983L10.6018 32H10.4368H0.5V23.5903L13.239 13.5H63.3892H247.842L259.474 28.3951L259.624 28.5874H259.868H311.386Z" fill="url(#paint4_linear_69_2)" />
<path d="M311.386 28.5874L313.227 30.1761H250.261H250.083L249.946 30.2881L246.609 33H79.743H79.6177L79.5071 33.0592L66.2898 40.132H21.5738L10.7345 32.0983L10.6018 32H10.4368H0.5V23.5903L13.239 13.5H63.3892H247.842L259.474 28.3951L259.624 28.5874H259.868H311.386Z" stroke="url(#paint5_linear_69_2)" />
<path d="M311.386 28.5874L313.227 30.1761H250.261H250.083L249.946 30.2881L246.609 33H79.743H79.6177L79.5071 33.0592L66.2898 40.132H21.5738L10.7345 32.0983L10.6018 32H10.4368H0.5V23.5903L13.239 13.5H63.3892H247.842L259.474 28.3951L259.624 28.5874H259.868H311.386Z" stroke="url(#paint6_radial_69_2)" />
</g>
<path id="边框" opacity="0.673995" fill-rule="evenodd" clip-rule="evenodd" d="M13.065 13H63.3892H248.086L259.868 28.0874H311.572L314.572 30.6761H250.261L246.787 33.5H79.743L66.4152 40.632H21.4087L10.4368 32.5H0V23.3485L13.065 13Z" stroke="url(#paint7_radial_69_2)" stroke-width="0.8" />
<path id="右下填充" opacity="0.280772" fill-rule="evenodd" clip-rule="evenodd" d="M80.5067 33.588H247.176L250.261 30.6761H314.572L321 36.5H74.2219L80.5067 33.588Z" fill="url(#paint8_radial_69_2)" />
<path id="右侧的边框" opacity="0.537975" d="M314.379 31.1761L319.703 36H76.4904L80.617 34.088H247.176H247.375L247.519 33.9516L250.46 31.1761H314.379Z" stroke="url(#paint9_linear_69_2)" />
</g>
<defs>
<filter id="filter0_f_69_2" x="14.4086" y="11.9086" width="206.554" height="45.3148" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="6.7957" result="effect1_foregroundBlur_69_2" />
</filter>
<radialGradient id="paint0_radial_69_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(342.17 47.8361) rotate(-90.0012) scale(43.4067 2195.92)">
<stop stop-color="#2E7E8B" />
<stop offset="1" stop-color="#031518" stop-opacity="0.01" />
</radialGradient>
<linearGradient id="paint1_linear_69_2" x1="270.862" y1="41.5354" x2="270.867" y2="39.4646" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.01" />
<stop offset="0.50583" stop-color="white" />
<stop offset="1" stop-color="white" stop-opacity="0.01" />
</linearGradient>
<linearGradient id="paint2_linear_69_2" x1="-22.8032" y1="37.1017" x2="26.2181" y2="158.013" gradientUnits="userSpaceOnUse">
<stop stop-color="#4B6160" />
<stop offset="1" stop-color="#4B6160" stop-opacity="0.01" />
</linearGradient>
<radialGradient id="paint3_radial_69_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(25.3302 43.632) rotate(90) scale(27.4693 743.408)">
<stop stop-color="white" stop-opacity="0.75" />
<stop offset="1" stop-color="white" stop-opacity="0.01" />
</radialGradient>
<linearGradient id="paint4_linear_69_2" x1="0" y1="42.8618" x2="365.343" y2="42.8618" gradientUnits="userSpaceOnUse">
<stop stop-color="#031518" />
<stop offset="0.373849" stop-color="#446D9C" />
<stop offset="1" stop-color="#2E6376" stop-opacity="0.01" />
</linearGradient>
<linearGradient id="paint5_linear_69_2" x1="-16.027" y1="34.1017" x2="44.9086" y2="139.738" gradientUnits="userSpaceOnUse">
<stop stop-color="#4B6160" />
<stop offset="1" stop-color="#4B6160" stop-opacity="0.01" />
</linearGradient>
<radialGradient id="paint6_radial_69_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(17.8031 40.632) rotate(90) scale(27.4693 522.498)">
<stop stop-color="white" stop-opacity="0.75" />
<stop offset="1" stop-color="white" stop-opacity="0.01" />
</radialGradient>
<radialGradient id="paint7_radial_69_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(222.887 31.9359) rotate(90) scale(27.4693 825.023)">
<stop stop-color="white" stop-opacity="0.75" />
<stop offset="1" stop-color="white" stop-opacity="0.01" />
</radialGradient>
<radialGradient id="paint8_radial_69_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(274.857 33.588) rotate(88.0875) scale(111.464 6866.67)">
<stop stop-color="#8FD4D2" stop-opacity="0.4" />
<stop offset="1" stop-color="#8FD4D2" stop-opacity="0.01" />
</radialGradient>
<linearGradient id="paint9_linear_69_2" x1="448.845" y1="32.7171" x2="448.8" y2="26.0457" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.08" />
<stop offset="0.461587" stop-color="white" stop-opacity="0.3" />
<stop offset="1" stop-color="white" stop-opacity="0.01" />
</linearGradient>
<clipPath id="clip0_69_2">
<rect width="460" height="43.632" fill="white" />
</clipPath>
</defs>
</svg>
</template>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
defineProps<{
title: string
}>()
</script>
<template>
<div class="w-[460px] h-fit relative my-2">
<div class="absolute z-10 text-2xl tracking-wide -top-1 left-14 text-cyan-200">
{{ title }}
</div>
<FrameV1HeaderSvg class="absolute " />
<div class="px-4 pt-16">
<slot />
</div>
</div>
</template>

View File

@ -0,0 +1,18 @@
<template>
<div class="relative w-full h-[211px]">
<div class="w-full h-full p-3">
<slot />
</div>
<svg viewBox="0 0 431 211" class="absolute top-0 left-0 " fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:anim="http://www.w3.org/2000/anim" anim="" anim:transform-origin="50% 50%" anim:duration="1" anim:ease="ease-in-out">
<g id="框">
<path id="矩形" opacity="0.214979" d="M1 0.5H0.5V1V210V210.5H1H430H430.5V210V1V0.5H430H1Z" stroke="#94CAEB" />
<g id="高亮边框">
<path id="路径 8" d="M1 10.3191V1H10.7924" stroke="#94CAEB" />
<path id="路径 8_2" d="M430 10.3191V1H420.208" stroke="#94CAEB" />
<path id="路径 8_3" d="M1 200.681V210H10.7924" stroke="#94CAEB" />
<path id="路径 8_4" d="M430 200.681V210H420.208" stroke="#94CAEB" />
</g>
</g>
</svg>
</div>
</template>

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
defineProps<{
label?:string
}>()
</script>
<template>
<h1 class="flex items-center gap-1.5 font-semibold text-gray-900 dark:text-white min-w-0">
<span class="truncate">
<slot>
{{ label }}
</slot>
</span>
</h1>
</template>

View File

@ -0,0 +1,7 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
</script>
<template>
<Icon />
</template>

View File

@ -0,0 +1,28 @@
<template>
<header class="relative flex w-full">
<Background class="z-10" />
<div class="absolute top-0 left-0 z-10 flex items-end h-full pl-[150px] gap-[100px] pb-[32px]">
<div class="pb-[12px]">
<h1 class="text-3xl font-bold">
农村废弃物特征数据库
</h1>
<p class="opacity-60">
Rural waste characteristics database
</p>
</div>
<Tabs
:links="[
{ name: '首页', to: '/', icon: 'solar:airbuds-right-bold-duotone' },
{ name: '资源化技术', to: '/resource_technology', icon: 'solar:airbuds-right-bold-duotone' },
{ name: '政策', to: '/policy', icon: 'solar:airbuds-right-bold-duotone' },
{
name: '新闻', to: '/news', icon: 'solar:airbuds-right-bold-duotone',
},
{
name: '标准', to: '/standard', icon: 'solar:airbuds-right-bold-duotone',
},
]"
/>
</div>
</header>
</template>

View File

@ -0,0 +1,74 @@
<script setup lang="ts">
import Help from '~/components/common/modal/header/help.vue'
import Info from '~/components/common/modal/header/info.vue'
import Version from '~/components/common/modal/header/version/version.vue'
const { t } = useI18n()
const infoModal = useModal(Info)
const helpModal = useModal(Help)
const versionModal = useModal(Version)
const items = [
[{
label: 'ben@example.com',
slot: 'account',
disabled: true,
}],
[{
label: t('menu.setting'),
icon: 'i-carbon-settings',
click: () => infoModal.open(), // isInfoModal.value = true,
}],
[{
label: t('menu.help'),
icon: 'i-carbon-help',
click: () => helpModal.open(),
}, {
label: t('menu.version'),
icon: 'i-carbon-account',
click: () => versionModal.open(),
}],
[{
label: t('menu.system.user'),
icon: 'i-carbon-user-multiple',
}, {
label: t('menu.system.admin'),
icon: 'i-carbon-user-profile',
to: '/admin',
}, {
label: t('menu.system.jwanfs'),
icon: 'i-carbon-ibm-cloud-bare-metal-server',
}, {
label: t('menu.logout'),
icon: 'i-heroicons-arrow-left-on-rectangle',
to: '/login',
}],
]
</script>
<template>
<UDropdown
:items="items" :ui="{ item: { disabled: 'cursor-text select-text' } }"
:popper="{ placement: 'bottom-start' }"
>
<UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" />
<template #account="{ item }">
<div class="text-left">
<p>
Signed in as
</p>
<p class="truncate font-medium text-gray-900 dark:text-white">
{{ item.label }}
</p>
</div>
</template>
<template #item="{ item }">
<span class="truncate">{{ item.label }}</span>
<UIcon :name="item.icon" class="ms-auto h-4 w-4 flex-shrink-0 text-gray-400 dark:text-gray-500" />
</template>
</UDropdown>
</template>

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
defineProps<{
sider: any[]
}>()
</script>
<template>
<CommonModal blur placement="right" class="dark">
<template #title>
<div class="flex items-center justify-between">
<CommonLogo dark />
</div>
</template>
<div class="h-[calc(100vh-200px)] w-[300px] overflow-auto">
<CommonLayoutSider static :sider="sider" class="w-full" />
</div>
</CommonModal>
</template>

View File

@ -0,0 +1,56 @@
<script setup lang="ts">
const { data, error, pending } = useAsyncData(
'user.storage', () => dashboardApi.storage({
isAll: 1
}), {
default: () => ({
Name: '',
FileNum: 0,
TotalSize: 0,
QuotaSize: 0,
UsagePercent: 0,
DocumentTypeSize: 0,
MusicTypeSize: 0,
PictureTypeSize: 0,
VedioTypeSize: 0,
OtherTypeSize: 0,
}),
})
const size = computed(() => {
return [
{ name: '文档', value: data.value.DocumentTypeSize, icon: 'i-heroicons-document-text', color: 'indigo' },
{ name: '音频', value: data.value.MusicTypeSize, icon: 'i-heroicons-musical-note', color: 'green' },
{ name: '图片', value: data.value.PictureTypeSize, icon: 'i-heroicons-photo', color: 'red' },
{ name: '视频', value: data.value.VedioTypeSize, icon: 'i-heroicons-film', color: 'blue' },
{ name: '其他', value: data.value.OtherTypeSize, icon: 'i-heroicons-document', color: 'gray' },
]
})
</script>
<template>
<UCard class="!bg-gray/5 ">
<UMeterGroup :max="data.QuotaSize">
<template #indicator>
<div class="flex justify-between gap-1.5 text-sm">
<p class="text-gray-500 dark:text-gray-400">
{{ byteTrans.merticKB(data.TotalSize) }} 已使用
</p>
<p class="text-gray-500 dark:text-gray-400">
{{ byteTrans.merticKB(data.QuotaSize) }} 配额空间
</p>
</div>
</template>
<UMeter
v-for="item in size"
:key="item.name"
:value="item.value"
:color="item.color"
:label="item.name"
:icon="item.icon"
/>
</UMeterGroup>
</UCard>
</template>

View File

@ -0,0 +1,49 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
defineProps<{
sider?: Config['userSider']
}>()
</script>
<template>
<div class="flex flex-col h-full max-h-screen gap-2">
<div class="flex h-18 items-center border-b px-4 lg:h-[60px] lg:px-6">
<NuxtLink to="/user/data/dashboard" class="flex justify-center w-full">
<Logo />
</NuxtLink>
</div>
<div class="flex-1">
<div v-for="item in sider" :key="item.group" class="px-4 mb-8">
<div class="pb-4 text-sm font-bold text-gray-500 opacity-40 dark:text-gray-100">
{{ item.group }}
</div>
<!-- <UDashboardSidebarLinks :links="item.list" /> -->
<div v-for="(link, index) in item.list" :key="index">
<NuxtLink :to="link.to" class="block w-full" :target="link.target">
<Button
:variant="$route.fullPath.includes(link.to) ? 'secondary' : 'ghost'"
class="justify-start w-full gap-2 my-1"
:trailing="false"
>
<Icon :icon="link.icon" />
{{ link.label }}
</Button>
</NuxtLink>
<div>
<slot :name="link.key" />
</div>
</div>
</div>
</div>
<div class="p-4 mt-auto">
<div class="w-full md:space-y-4">
<LayoutSiderStorage class="hidden xl:block" />
<UDivider class="sticky bottom-0" />
<!-- ~/components/UserDropdown.vue -->
<!-- <LayoutSiderUser /> -->
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,80 @@
<script setup lang="ts">
const { userInfo } = useUserStore()
const { t } = useI18n()
const items = [
[{
label: userInfo?.Email,
slot: 'account',
}],
[{
label: t('menu.setting'),
icon: 'i-carbon-settings',
to: '/user/settings',
}],
// [{
// label: t('menu.help'),
// icon: 'i-carbon-help',
// to: '/user/settings/help/getting-started',
// }, {
// label: t('menu.version'),
// icon: 'i-carbon-account',
// // click: () => versionModal.open(),
// }],
[
// {
// label: t('menu.system.user'),
// icon: 'i-carbon-user-multiple',
// }, {
// label: t('menu.system.admin'),
// icon: 'i-carbon-user-profile',
// to: '/admin',
// },
// {
// label: t('menu.system.jwanfs'),
// icon: 'i-carbon-ibm-cloud-bare-metal-server',
// },
{
label: t('menu.logout'),
icon: 'i-heroicons-arrow-left-on-rectangle',
to: '/login',
}],
]
</script>
<template>
<UDropdown mode="hover" :items="items" class="w-full">
<template #default="{ open }">
<UButton
color="gray"
variant="ghost"
class="w-full"
:label="userInfo?.NickName"
:class="[open && 'bg-gray-50 dark:bg-gray-800']"
>
<template #leading>
<UAvatar src="" :alt="userInfo?.NickName" size="2xs" />
</template>
</UButton>
</template>
<template #account>
<div class="w-full ">
<div class="text-left pb-2">
<p>
登录于
</p>
<p class="truncate font-medium text-gray-900 dark:text-white">
{{ userInfo?.Email }}
</p>
</div>
<div>
<LayoutSiderStorage class="xl:hidden block" />
</div>
</div>
</template>
</UDropdown>
</template>

View File

@ -0,0 +1,27 @@
<script setup lang="ts">
import { useImage } from '@vueuse/core'
const props = defineProps<{
src?: string
}>()
const { isLoading } = useImage({ src: props.src })
</script>
<template>
<AspectRatio :ratio="16 / 9">
<UIcon v-if="isLoading" name="i-mdi-loading " class="text-4xl animate-spin" />
<img
v-else
:src="src"
alt="Image"
class="rounded-md object-cover h-full w-full"
>
</AspectRatio>
</template>

View File

@ -0,0 +1,6 @@
<template>
<div class="h-28 w-38">
<img class="h-full w-full hidden dark:block" src="/osca_bg.svg" />
<img class="block h-full w-full dark:hidden" src="/osca_bg.svg" />
</div>
</template>

View File

@ -0,0 +1,40 @@
<script setup lang="ts">
const { blur } = defineProps<{
title: string
subtitle?: string
hiddenClose?: boolean
blur?: boolean
}>()
const isExternalOpen = defineModel<boolean>()
const overlayBg = blur ? 'bg-slate-950/10 backdrop-blur-sm' : 'bg-slate-950/10 backdrop-blur-none'
</script>
<template>
<UModal v-model="isExternalOpen" :ui="{ overlay: { background: overlayBg } }">
<UCard :ui="{ ring: '', divide: subtitle ? 'divide-y' : 'divide-y-0' }">
<template #header>
<div class="flex items-center justify-between">
<div>
<div class="flex justify-between">
<h4 class="typography-h4">
{{ title }}
</h4>
</div>
<p v-if="subtitle" class="typography-muted">
{{ subtitle }}
</p>
</div>
<UButton v-if="!hiddenClose" color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="isExternalOpen = false" />
</div>
</template>
<slot />
<template #footer>
<div class="flex items-center justify-end">
<slot name="footer" />
</div>
</template>
</UCard>
</UModal>
</template>

View File

@ -0,0 +1,83 @@
<script setup lang="tsx">
const menus = [
{
name: '邮箱',
value: '1124124@qq.com',
icon: 'i-carbon-email',
},
]
</script>
<template>
<CommonModal :title="$t('menu.setting')" :subtitle="$t('menu.setting.desc')" prevent-close blur>
<div>
<InlineComponent :tset="1" />
<div class="flex flex-col items-center justify-center gap-2">
<UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="2xl" alt="Avatar" />
<div class="flex items-center">
<p class="font-bold typography-lead">
admin
</p>
<UButton icon="i-heroicons-pencil-square" size="sm" variant="soft" />
</div>
</div>
</div>
<template #footer>
<div class="grid grid-cols-2 w-full gap-2">
<UCard v-for="item in menus" :key="item.name" class="col-span-1">
<UIcon :name="item.icon" class="float-right h-8 w-8" />
<p class="font-bold typography-large">
{{ item.name }}
</p>
<p class="typography-muted">
{{ item.value }}
</p>
</UCard>
<UCard class="col-span-1">
<div class="i-solar-mailbox-bold-duotone float-right h-8 w-8" />
<p class="font-bold typography-large">
邮箱
</p>
<p class="typography-muted">
123456789@qq.com
</p>
<small class="font-bold opacity-50 typography-small">
修改
</small>
</UCard>
<UCard class="col-span-1">
<div class="i-solar-smartphone-2-bold-duotone float-right h-8 w-8" />
<p class="font-bold typography-large">
手机
</p>
<p class="typography-muted">
123456789@qq.com
</p>
<small class="font-bold opacity-50 typography-small">
修改
</small>
</UCard>
<UCard class="col-span-1">
<div class="i-solar-mailbox-bold-duotone float-right h-8 w-8" />
<p class="font-bold typography-large">
双因素认证
</p>
<p class="typography-muted">
已开启
</p>
<UToggle on-icon="i-heroicons-check-20-solid" off-icon="i-heroicons-x-mark-20-solid" size="sm" />
</UCard>
<UCard class="col-span-1">
<div class="i-solar-mailbox-bold-duotone float-right h-8 w-8" />
<p class="font-bold typography-large">
高能所统一认证
</p>
<p class="typography-muted">
已绑定
</p>
<UToggle on-icon="i-heroicons-check-20-solid" off-icon="i-heroicons-x-mark-20-solid" size="sm" />
</UCard>
</div>
</template>
</CommonModal>
</template>

View File

@ -0,0 +1,35 @@
<script setup lang="ts">
const items = [{
label: 'V1.0.1-12',
icon: 'i-heroicons-information-circle',
defaultOpen: true,
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.',
}, {
label: 'V1.0.1-4',
icon: 'i-heroicons-arrow-down-tray',
disabled: true,
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.',
}, {
label: 'V1.0.1-0',
icon: 'i-heroicons-eye-dropper',
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.',
}, {
label: 'V1.0.0-0 (大版本)',
icon: 'i-heroicons-rectangle-group',
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.',
}, {
label: 'V0.0.1-4',
icon: 'i-heroicons-square-3-stack-3d',
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.',
}, {
label: 'V0.0.1-1',
icon: 'i-heroicons-wrench-screwdriver',
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.',
}]
</script>
<template>
<CommonModal title="功能介绍">
<UAccordion :items="items" class="max-w-[500px]" />
</CommonModal>
</template>

View File

@ -0,0 +1,10 @@
<template>
<CommonModal title="反馈">
<UTextarea placeholder="Search..." class="min-w-[300px]" />
<template #footer>
<UButton>
提交
</UButton>
</template>
</CommonModal>
</template>

View File

@ -0,0 +1,35 @@
<script setup lang="ts">
import Feedback from '~/components/common/modal/header/version/feedback.vue'
import Feature from '~/components/common/modal/header/version/feature.vue'
const featureModal = useModal(Feature)
const feedbackModal = useModal(Feedback)
</script>
<template>
<CommonModal>
<div class="h-[100px] min-w-[300px] flex flex-col items-center justify-center gap-4">
<div class="flex justify-between">
<h4 class="typography-h4">
{{ $t("menu.version") }}
</h4>
</div>
<p class="typography-muted">
{{ $t("menu.version.desc") }}
</p>
<div class="flex gap-2">
<UButton variant="link" @click="featureModal.open()">
功能介绍
</UButton>
<UDivider orientation="vertical" />
<UButton variant="link" @click="feedbackModal.open()">
反馈
</UButton>
</div>
</div>
</CommonModal>
<!-- <UserModalHeaderVersionFeedback v-model="isFeedbackOpen" />
<UserModalHeaderVersionFeature v-model="isFeatureOpen" /> -->
</template>

View File

@ -0,0 +1,144 @@
<script setup lang="ts">
import { VueFinalModal } from 'vue-final-modal'
import { breakpointsTailwind } from '@vueuse/core'
const { blur, placement = 'center', preventClose, hideOverlay } = defineProps<{
title?: string
subtitle?: string
hiddenClose?: boolean
blur?: boolean
placement?: 'bottom' | 'center' | 'top' | 'right' | 'left'
preventClose?: boolean
fullScreen?: boolean
blank?: boolean
hideOverlay?: boolean
}>()
const emit = defineEmits<{
(e: 'update:modelValue', modelValue: boolean): void
}>()
const breakpoints = useBreakpoints(breakpointsTailwind)
const placementRef = computed(() => {
if (placement !== 'center')
return placement
if (breakpoints.smaller('sm').value)
return 'bottom'
else
return 'center'
})
function getInitialValues() {
const opts = {
teleportTo: 'body',
modelValue: false,
displayDirective: 'if' as 'if' | 'show' | 'visible',
hideOverlay: !!hideOverlay,
overlayTransition: 'vfm-fade',
contentTransition: 'vfm-fade',
clickToClose: !preventClose,
escToClose: true,
background: 'non-interactive' as 'non-interactive' | 'interactive',
lockScroll: true,
reserveScrollBarGap: true,
swipeToClose: 'none' as 'right' | 'left' | 'none' | 'down' | 'up',
}
return opts
}
const options = ref(getInitialValues())
effect(() => {
switch (placementRef.value) {
case 'bottom':
options.value.swipeToClose = 'down'
options.value.contentTransition = 'vfm-slide-down'
break
case 'top':
options.value.swipeToClose = 'up'
options.value.contentTransition = 'vfm-slide-up'
break
case 'right':
options.value.swipeToClose = 'right'
options.value.contentTransition = 'vfm-slide-right'
break
case 'left':
options.value.swipeToClose = 'left'
options.value.contentTransition = 'vfm-slide-left'
break
default:
options.value.swipeToClose = 'none'
options.value.contentTransition = 'vfm-fade'
break
}
})
const contentClass = {
center: 'absolute top-1/2 left-1/2 -translate-1/2 max-h-screen',
bottom: 'absolute bottom-0 w-screen',
top: 'absolute top-0 w-screen',
right: 'absolute right-0 h-screen',
left: 'absolute left-0 h-screen',
}
const cardClass = {
center: '',
bottom: 'w-screen max-h-[90vh] overflow-y-auto rounded-b-none',
top: ' w-screen rounded-t-none max-h-[90vh] overflow-y-auto ',
right: ' h-screen rounded-r-none max-w-[90vh] overflow-w-auto ',
left: 'h-screen rounded-l-none max-w-[90vh] overflow-w-auto ',
}
</script>
<template>
<VueFinalModal
class="overflow-y-auto"
:content-class="[contentClass[placementRef]]"
:overlay-class="['bg-black/20 fixed bottom-0 h-screen', blur && 'backdrop-blur-sm ']"
:teleport-to="options.teleportTo"
:display-directive="options.displayDirective"
:hide-overlay="options.hideOverlay"
:overlay-transition="options.overlayTransition"
:content-transition="options.contentTransition"
:click-to-close="options.clickToClose"
:esc-to-close="options.escToClose"
:background="options.background"
:lock-scroll="options.lockScroll"
:reserve-scroll-bar-gap="options.reserveScrollBarGap"
:swipe-to-close="options.swipeToClose"
@update:model-value="val => emit('update:modelValue', val)"
>
<div :class="[cardClass[placementRef], fullScreen ? 'w-screen h-screen ' : 'md:py-5']">
<UCard class="h-full w-full" :ui="{ ring: '', divide: subtitle ? 'divide-y' : 'divide-y-0' }">
<template v-if="!blank" #header>
<slot name="header">
<div class="du flex items-center justify-between">
<div>
<slot name="title">
<div class="flex justify-between">
<h4 class="typography-h4">
{{ title }}
</h4>
</div>
<p v-if="subtitle" class="typography-muted">
{{ subtitle }}
</p>
</slot>
</div>
<UButton v-if="!hiddenClose" color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @pointerup="emit('update:modelValue', false)" />
</div>
</slot>
</template>
<div>
<slot />
</div>
<template v-if="!blank" #footer>
<div class="flex items-center justify-end">
<slot name="footer" />
</div>
</template>
</UCard>
</div>
</VueFinalModal>
</template>

View File

@ -0,0 +1,27 @@
<script setup lang="ts">
const emit = defineEmits<{
(e: 'update:modelValue', modelValue: boolean): void
(e: 'success'): void
}>()
const selected = ref(false)
const onSuccess = () => {
emit('success')
emit('update:modelValue', false)
}
</script>
<template>
<ModalBase title="服务条款" prevent-close @update:model-value="val => emit('update:modelValue', val)">
<ContentDoc path="/privacy" class="prose-primary my-2 rounded bg-gray/10 p-6 prose dark:prose-invert" />
<UCheckbox v-model="selected" name="notifications" label="我同意并接受以上条款服务内容" />
<template #footer>
<UTooltip :popper="{ placement: 'top' }" :text="selected ? '进入系统' : '请先确认您已经同意了条款服务内容'">
<UButton size="lg" class="px-10" :disabled="!selected" @click="onSuccess">
确认
</UButton>
</UTooltip>
</template>
</ModalBase>
</template>

View File

@ -0,0 +1,72 @@
<script lang="tsx" setup>
const { as = 'h1', title } = defineProps<{
icon?: string
breadcrumb?: any[]
title?: string
as?: 'h1' | 'h2' | 'h3' | 'h4'
description?: string
link?: string
}>()
const space = {
h1: 'space-y-3',
h2: 'space-y-2',
h3: 'space-y-1',
h4: 'space-y-0',
}
function Title() {
switch (as) {
case 'h1':
return (
<h1 class="typography-h1">
{title }
</h1>
)
case 'h2':
return (
<h2 class="typography-h2">
{title }
</h2>
)
case 'h3':
return (
<h3 class="typography-h3">
{title }
</h3>
)
case 'h4':
return (
<h4 class="typography-h4">
{title }
</h4>
)
}
}
</script>
<template>
<div class="flex items-center justify-between px-5 my-6">
<div class="flex items-center gap-4">
<slot name="icon">
{{ icon }}
</slot>
<div :class="space[as]">
<slot name="headline">
<UBreadcrumb :links="breadcrumb" />
</slot>
<slot name="title">
<Title />
</slot>
<slot name="description">
<p class="-z-1 typography-muted">
{{ description }}
</p>
</slot>
</div>
</div>
<div>
<slot name="actions" />
</div>
</div>
</template>

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
defineProps<{
title?: string
badge?: string|number
}>()
</script>
<template>
<UDashboardToolbar class=" border-0 my-1 mt-2">
<template #left>
<HeadingH1>
{{ title }}
</HeadingH1>
<UBadge variant="soft">
{{ badge }}
</UBadge>
</template>
<template #right>
<slot
name="right"
/>
</template>
</UDashboardToolbar>
</template>

View File

@ -0,0 +1,137 @@
<script>
export default {
// props: ['dataList'],
data() {
return {
dataList:[{
thumb:'https://img.yzcdn.cn/vant/apple-1.jpg',
title:'Apple iPhone 12 (A2228) 64GB 黑色 移动联通电信4G手机',
subtitle:'Apple iPhone 12 (A2228) 64GB 黑色 移动联通电信4G手机',
marketprice:5999,
id:1
},{
thumb:'https://img.yzcdn.cn/vant/apple-2.jpg',
title:'Apple iPhone 12 (A2228) 64GB 黑色 移动联通电信4G手机',
subtitle:'Apple iPhone 12 (A2228) 64GB 黑色 移动联通电信4G手机',
marketprice:5999,
id:2
},{
thumb:'https://img.yzcdn.cn/vant/apple-3.jpg',
title:'Apple iPhone 12 (A2228) 64GB 黑色 移动联通电信4G手机',
subtitle:'Apple iPhone 12 (A2228) 64GB 黑色 移动联通电信4G手机',
marketprice:5999,
id:3
},{
thumb:'https://img.yzcdn.cn/vant/apple-4.jpg',
title:'Apple iPhone 12 (A2228) 64GB 黑色 移动联通电信4G手机',
subtitle:'Apple iPhone 12 (A2228) 64GB 黑色 移动联通电信4G手机',
marketprice:5999,
id:4
}],
calleft: 0,
speed:1
}
},
computed: {
widthData(){
return 240 * Number(this.dataList.length*2)
}
},
created() {
this.move()
},
mounted() {
const imgBox = document.getElementsByClassName('imgBoxoul')[0]
imgBox.innerHTML += imgBox.innerHTML
},
methods: {
//
move() {
this.timer = setInterval(this.starmove, 20)
},
//
starmove() {
this.calleft -= 1.2//*this.speed
if (this.calleft <= -1150) {
this.calleft = 0
}
},
//
stopmove() {
clearInterval(this.timer)
},
}
}
</script>
<template>
<div class="threeImg">
<div class="Containt">
<ul :style="{'left':calleft + 'px', width: widthData + 'px'} " class="imgBoxoul" @mouseover="stopmove()" @mouseout="move()">
<li v-for="(item,index) in dataList" :key="index" @click="gotodetails(item.id)">
<img :src="item.thumb">
<div class="item-content">
<p class="item-title">
{{ item.title }}
</p>
<div class="item-detail line-2">
{{ item.subtitle }}
</div>
<p class="item-price">
{{ item.marketprice }}
</p>
</div>
</li>
</ul>
</div>
</div>
</template>
<style>
.threeImg {
position: relative;
}
.threeImg .Containt ul {
margin: 0 auto;
width: 2400px;
position: absolute;
left: 0px;
cursor: pointer;
height: 100%;
z-index: 10;
}
.threeImg .Containt ul li {
float: left;
width: 220px;
height: 350px;
margin-right: 20px;
border-radius: 10px;
overflow: hidden;
background-color: #ffffff;
}
.threeImg .Containt ul li img {
width: 100%;
height: 263px;
}
.Containt {
position: relative;
padding: 60px 0;
overflow-y: auto;
width: 1200px;
height: 365px;
overflow: hidden;
margin: 0 auto;
}
</style>

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
const { toggleDashboardSearch } = useUIState()
const { metaSymbol } = useShortcuts()
</script>
<template>
<UButton color="gray" icon="i-heroicons-magnifying-glass-16-solid" variant="solid" @click="toggleDashboardSearch()">
<div class="w-[100px] text-left hidden md:block">
{{ $t('search') }}
</div>
<template #trailing>
<div class=" items-center gap-0.5 hidden md:flex">
<UKbd>{{ metaSymbol }}</UKbd>
<UKbd>K</UKbd>
</div>
</template>
</UButton>
</template>

View File

@ -0,0 +1,45 @@
<script setup lang="ts">
const { size, unit = 'KB' } = defineProps<{
size: number
unit?: 'B' | 'KB' | 'MB' | 'GB' | 'TB'
}>()
const colors = {
Null: 'slate',
B: 'lime',
KB: 'emerald',
MB: 'sky',
GB: 'indigo',
TB: 'purple',
PB: 'pink',
EB: 'rose',
ZB: 'orange',
YB: 'yellow',
}
type Unit = keyof typeof colors
const unitFunc = {
B:mertic,
KB:merticKB,
MB:merticMB,
GB:merticGB,
TB:merticTB,
}
const { color, value } = unitFunc[unit]<{
color: string
value: string
}>(Number(size), (num, unit: Unit) => {
return {
color: colors[unit],
value: num.toFixed(0) + unit,
}
})
</script>
<template>
<UBadge :color="color" variant="solid" class="shadow-sm shadow-inner opacity-90">
{{ value }}
</UBadge>
</template>

View File

@ -0,0 +1,59 @@
<script setup lang="ts">
const props = defineProps<{
title: string
value: number
max?: number
}>()
const percent = computed(() => {
return (props.value / (props.max ?? 100)) * 100
})
</script>
<template>
<div class="h-[32px] flex items-center justify-between">
<div class="w-[140px] opacity-60 truncate">
{{ title }}
</div>
<div class="relative w-[333px] h-[4px]">
<div class="absolute text-[#E4F3FF] text-[15px] -top-2 " :style="`padding-left:${percent}%`">
<span class="pl-2">{{ value }}</span>
</div>
<svg
viewBox="0 0 333 4" class="h-[4px] absolute top-0 left-0 z-10" :style="`width:${percent}%`" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns:anim="http://www.w3.org/2000/anim" anim=""
anim:transform-origin="50% 50%" anim:duration="1" anim:ease="ease-in-out"
>
<rect
id="矩形" opacity="0.7" x="0.00170898" y="0.529053" width="333" height="3" rx="1.5"
fill="url(#paint0_linear_0_10711)" stroke="url(#paint1_linear_0_10711)"
/>
<defs>
<linearGradient
id="paint0_linear_0_10711" x1="164.549" y1="0.551191" x2="164.346" y2="7.98045"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#A8DEFF" />
<stop offset="1" stop-color="#284257" stop-opacity="0.5" />
</linearGradient>
<linearGradient
id="paint1_linear_0_10711" x1="225.089" y1="1.99811" x2="0.00170898" y2="1.99811"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="white" stop-opacity="0.598517" />
<stop offset="1" stop-color="white" stop-opacity="0.01" />
</linearGradient>
</defs>
</svg>
<svg
viewBox="0 0 333 4" class="absolute top-0 left-0 -z-[1]" fill="none" xmlns="http://www.w3.org/2000/svg"
xmlns:anim="http://www.w3.org/2000/anim" anim="" anim:transform-origin="50% 50%" anim:duration="1"
anim:ease="ease-in-out"
>
<rect id="矩形" opacity="0.7" x="0.00170898" y="0.529053" width="333" height="3" rx="1.5" fill="#2D4D67" />
</svg>
</div>
</div>
</template>

View File

@ -0,0 +1,106 @@
<script setup lang="ts">
import type {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
} from '@tanstack/vue-table'
import {
FlexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useVueTable,
} from '@tanstack/vue-table'
import { ref } from 'vue'
import type { Task } from '../data/schema'
import DataTablePagination from './DataTablePagination.vue'
import DataTableToolbar from './DataTableToolbar.vue'
import { valueUpdater } from '@/lib/utils'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/lib/registry/new-york/ui/table'
interface DataTableProps {
columns: ColumnDef<Task, any>[]
data: Task[]
}
const props = defineProps<DataTableProps>()
const sorting = ref<SortingState>([])
const columnFilters = ref<ColumnFiltersState>([])
const columnVisibility = ref<VisibilityState>({})
const rowSelection = ref({})
const table = useVueTable({
get data() { return props.data },
get columns() { return props.columns },
state: {
get sorting() { return sorting.value },
get columnFilters() { return columnFilters.value },
get columnVisibility() { return columnVisibility.value },
get rowSelection() { return rowSelection.value },
},
enableRowSelection: true,
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection),
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
})
</script>
<template>
<div class="space-y-4">
<DataTableToolbar :table="table" />
<div class="rounded-md border">
<Table>
<TableHeader>
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<TableHead v-for="header in headerGroup.headers" :key="header.id">
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header" :props="header.getContext()" />
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<template v-if="table.getRowModel().rows?.length">
<TableRow
v-for="row in table.getRowModel().rows"
:key="row.id"
:data-state="row.getIsSelected() && 'selected'"
>
<TableCell v-for="cell in row.getVisibleCells()" :key="cell.id">
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
</TableCell>
</TableRow>
</template>
<TableRow v-else>
<TableCell
:colspan="columns.length"
class="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<DataTablePagination :table="table" />
</div>
</template>

View File

@ -0,0 +1,69 @@
<script setup lang="ts">
import type { Column } from '@tanstack/vue-table'
import type { Task } from '../data/schema'
import ArrowDownIcon from '~icons/radix-icons/arrow-down'
import ArrowUpIcon from '~icons/radix-icons/arrow-up'
import CaretSortIcon from '~icons/radix-icons/caret-sort'
import EyeNoneIcon from '~icons/radix-icons/eye-none'
import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/lib/registry/new-york/ui/dropdown-menu'
interface DataTableColumnHeaderProps {
column: Column<Task, any>
title: string
}
defineProps<DataTableColumnHeaderProps>()
</script>
<script lang="ts">
export default {
inheritAttrs: false,
}
</script>
<template>
<div v-if="column.getCanSort()" :class="cn('flex items-center space-x-2', $attrs.class ?? '')">
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button
variant="ghost"
size="sm"
class="-ml-3 h-8 data-[state=open]:bg-accent"
>
<span>{{ title }}</span>
<ArrowDownIcon v-if="column.getIsSorted() === 'desc'" class="ml-2 h-4 w-4" />
<ArrowUpIcon v-else-if=" column.getIsSorted() === 'asc'" class="ml-2 h-4 w-4" />
<CaretSortIcon v-else class="ml-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem @click="column.toggleSorting(false)">
<ArrowUpIcon class="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Asc
</DropdownMenuItem>
<DropdownMenuItem @click="column.toggleSorting(true)">
<ArrowDownIcon class="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Desc
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem @click="column.toggleVisibility(false)">
<EyeNoneIcon class="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Hide
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div v-else :class="$attrs.class">
{{ title }}
</div>
</template>

View File

@ -0,0 +1,136 @@
<script setup lang="ts">
import type { Column } from '@tanstack/vue-table'
import type { Component } from 'vue'
import { computed } from 'vue'
import type { Task } from '../data/schema'
import PlusCircledIcon from '~icons/radix-icons/plus-circled'
import CheckIcon from '~icons/radix-icons/check'
import { Badge } from '@/lib/registry/new-york/ui/badge'
import { Button } from '@/lib/registry/new-york/ui/button'
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from '@/lib/registry/new-york/ui/command'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/lib/registry/new-york/ui/popover'
import { Separator } from '@/lib/registry/new-york/ui/separator'
import { cn } from '@/lib/utils'
interface DataTableFacetedFilter {
column?: Column<Task, any>
title?: string
options: {
label: string
value: string
icon?: Component
}[]
}
const props = defineProps<DataTableFacetedFilter>()
const facets = computed(() => props.column?.getFacetedUniqueValues())
const selectedValues = computed(() => new Set(props.column?.getFilterValue() as string[]))
</script>
<template>
<Popover>
<PopoverTrigger as-child>
<Button variant="outline" size="sm" class="h-8 border-dashed">
<PlusCircledIcon class="mr-2 h-4 w-4" />
{{ title }}
<template v-if="selectedValues.size > 0">
<Separator orientation="vertical" class="mx-2 h-4" />
<Badge
variant="secondary"
class="rounded-sm px-1 font-normal lg:hidden"
>
{{ selectedValues.size }}
</Badge>
<div class="hidden space-x-1 lg:flex">
<Badge
v-if="selectedValues.size > 2"
variant="secondary"
class="rounded-sm px-1 font-normal"
>
{{ selectedValues.size }} selected
</Badge>
<template v-else>
<Badge
v-for="option in options
.filter((option) => selectedValues.has(option.value))"
:key="option.value"
variant="secondary"
class="rounded-sm px-1 font-normal"
>
{{ option.label }}
</Badge>
</template>
</div>
</template>
</Button>
</PopoverTrigger>
<PopoverContent class="w-[200px] p-0" align="start">
<Command
:filter-function="(list: DataTableFacetedFilter['options'], term) => list.filter(i => i.label.toLowerCase()?.includes(term)) "
>
<CommandInput :placeholder="title" />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
<CommandItem
v-for="option in options"
:key="option.value"
:value="option"
@select="(e) => {
console.log(e.detail.value)
const isSelected = selectedValues.has(option.value)
if (isSelected) {
selectedValues.delete(option.value)
}
else {
selectedValues.add(option.value)
}
const filterValues = Array.from(selectedValues)
column?.setFilterValue(
filterValues.length ? filterValues : undefined,
)
}"
>
<div
:class="cn(
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
selectedValues.has(option.value)
? 'bg-primary text-primary-foreground'
: 'opacity-50 [&_svg]:invisible',
)"
>
<CheckIcon :class="cn('h-4 w-4')" />
</div>
<component :is="option.icon" v-if="option.icon" class="mr-2 h-4 w-4 text-muted-foreground" />
<span>{{ option.label }}</span>
<span v-if="facets?.get(option.value)" class="ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs">
{{ facets.get(option.value) }}
</span>
</CommandItem>
</CommandGroup>
<template v-if="selectedValues.size > 0">
<CommandSeparator />
<CommandGroup>
<CommandItem
:value="{ label: 'Clear filters' }"
class="justify-center text-center"
@select="column?.setFilterValue(undefined)"
>
Clear filters
</CommandItem>
</CommandGroup>
</template>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</template>

View File

@ -0,0 +1,93 @@
<script setup lang="ts">
import type { Table } from '@tanstack/vue-table'
import type { Task } from '../data/schema'
import ChevronLeftIcon from '~icons/radix-icons/chevron-left'
import ChevronRightIcon from '~icons/radix-icons/chevron-right'
import DoubleArrowLeftIcon from '~icons/radix-icons/double-arrow-left'
import DoubleArrowRightIcon from '~icons/radix-icons/double-arrow-right'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/lib/registry/new-york/ui/select'
interface DataTablePaginationProps {
table: Table<Task>
}
defineProps<DataTablePaginationProps>()
</script>
<template>
<div class="flex items-center justify-between px-2">
<div class="flex-1 text-sm text-muted-foreground">
{{ table.getFilteredSelectedRowModel().rows.length }} of
{{ table.getFilteredRowModel().rows.length }} row(s) selected.
</div>
<div class="flex items-center space-x-6 lg:space-x-8">
<div class="flex items-center space-x-2">
<p class="text-sm font-medium">
Rows per page
</p>
<Select
:model-value="`${table.getState().pagination.pageSize}`"
@update:model-value="table.setPageSize"
>
<SelectTrigger class="h-8 w-[70px]">
<SelectValue :placeholder="`${table.getState().pagination.pageSize}`" />
</SelectTrigger>
<SelectContent side="top">
<SelectItem v-for="pageSize in [10, 20, 30, 40, 50]" :key="pageSize" :value="`${pageSize}`">
{{ pageSize }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="flex w-[100px] items-center justify-center text-sm font-medium">
Page {{ table.getState().pagination.pageIndex + 1 }} of
{{ table.getPageCount() }}
</div>
<div class="flex items-center space-x-2">
<Button
variant="outline"
class="hidden h-8 w-8 p-0 lg:flex"
:disabled="!table.getCanPreviousPage()"
@click="table.setPageIndex(0)"
>
<span class="sr-only">Go to first page</span>
<DoubleArrowLeftIcon class="h-4 w-4" />
</Button>
<Button
variant="outline"
class="h-8 w-8 p-0"
:disabled="!table.getCanPreviousPage()"
@click="table.previousPage()"
>
<span class="sr-only">Go to previous page</span>
<ChevronLeftIcon class="h-4 w-4" />
</Button>
<Button
variant="outline"
class="h-8 w-8 p-0"
:disabled="!table.getCanNextPage()"
@click="table.nextPage()"
>
<span class="sr-only">Go to next page</span>
<ChevronRightIcon class="h-4 w-4" />
</Button>
<Button
variant="outline"
class="hidden h-8 w-8 p-0 lg:flex"
:disabled="!table.getCanNextPage()"
@click="table.setPageIndex(table.getPageCount() - 1)"
>
<span class="sr-only">Go to last page</span>
<DoubleArrowRightIcon class="h-4 w-4" />
</Button>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,65 @@
<script setup lang="ts">
import type { Row } from '@tanstack/vue-table'
import { computed } from 'vue'
import { labels } from '../data/data'
import { taskSchema } from '../data/schema'
import type { Task } from '../data/schema'
import DotsHorizontalIcon from '~icons/radix-icons/dots-horizontal'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@/lib/registry/new-york/ui/dropdown-menu'
interface DataTableRowActionsProps {
row: Row<Task>
}
const props = defineProps<DataTableRowActionsProps>()
const task = computed(() => taskSchema.parse(props.row.original))
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button
variant="ghost"
class="flex h-8 w-8 p-0 data-[state=open]:bg-muted"
>
<DotsHorizontalIcon class="h-4 w-4" />
<span class="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" class="w-[160px]">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Make a copy</DropdownMenuItem>
<DropdownMenuItem>Favorite</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>Labels</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup :value="task.label">
<DropdownMenuRadioItem v-for="label in labels" :key="label.value" :value="label.value">
{{ label.label }}
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem>
Delete
<DropdownMenuShortcut></DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>

View File

@ -0,0 +1,56 @@
<script setup lang="ts">
import type { Table } from '@tanstack/vue-table'
import { computed } from 'vue'
import type { Task } from '../data/schema'
import { priorities, statuses } from '../data/data'
import DataTableFacetedFilter from './DataTableFacetedFilter.vue'
import DataTableViewOptions from './DataTableViewOptions.vue'
import Cross2Icon from '~icons/radix-icons/cross-2'
import { Button } from '@/lib/registry/new-york/ui/button'
import { Input } from '@/lib/registry/new-york/ui/input'
interface DataTableToolbarProps {
table: Table<Task>
}
const props = defineProps<DataTableToolbarProps>()
const isFiltered = computed(() => props.table.getState().columnFilters.length > 0)
</script>
<template>
<div class="flex items-center justify-between">
<div class="flex flex-1 items-center space-x-2">
<Input
placeholder="Filter tasks..."
:model-value="(table.getColumn('title')?.getFilterValue() as string) ?? ''"
class="h-8 w-[150px] lg:w-[250px]"
@input="table.getColumn('title')?.setFilterValue($event.target.value)"
/>
<DataTableFacetedFilter
v-if="table.getColumn('status')"
:column="table.getColumn('status')"
title="Status"
:options="statuses"
/>
<DataTableFacetedFilter
v-if="table.getColumn('priority')"
:column="table.getColumn('priority')"
title="Priority"
:options="priorities"
/>
<Button
v-if="isFiltered"
variant="ghost"
class="h-8 px-2 lg:px-3"
@click="table.resetColumnFilters()"
>
Reset
<Cross2Icon class="ml-2 h-4 w-4" />
</Button>
</div>
<DataTableViewOptions :table="table" />
</div>
</template>

View File

@ -0,0 +1,57 @@
<script setup lang="ts">
import type { Table } from '@tanstack/vue-table'
import { computed } from 'vue'
import type { Task } from '../data/schema'
import MixerHorizontalIcon from '~icons/radix-icons/mixer-horizontal'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/lib/registry/new-york/ui/dropdown-menu'
interface DataTableViewOptionsProps {
table: Table<Task>
}
const props = defineProps<DataTableViewOptionsProps>()
const columns = computed(() => props.table.getAllColumns()
.filter(
column =>
typeof column.accessorFn !== 'undefined' && column.getCanHide(),
))
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button
variant="outline"
size="sm"
class="ml-auto hidden h-8 lg:flex"
>
<MixerHorizontalIcon class="mr-2 h-4 w-4" />
View
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" class="w-[150px]">
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem
v-for="column in columns"
:key="column.id"
class="capitalize"
:checked="column.getIsVisible()"
@update:checked="(value) => column.toggleVisibility(!!value)"
>
{{ column.id }}
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
</template>

View File

@ -0,0 +1,64 @@
<script setup lang="ts">
import {
Avatar,
AvatarFallback,
AvatarImage,
} from '@/lib/registry/new-york/ui/avatar'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from '@/lib/registry/new-york/ui/dropdown-menu'
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" class="relative h-8 w-8 rounded-full">
<Avatar class="h-9 w-9">
<AvatarImage src="/avatars/03.png" alt="@shadcn" />
<AvatarFallback>SC</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent class="w-56" align="end">
<DropdownMenuLabel class="font-normal flex">
<div class="flex flex-col space-y-1">
<p class="text-sm font-medium leading-none">
shadcn
</p>
<p class="text-xs leading-none text-muted-foreground">
m@example.com
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
Profile
<DropdownMenuShortcut>P</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Billing
<DropdownMenuShortcut>B</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Settings
<DropdownMenuShortcut>S</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>New Team</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
Log out
<DropdownMenuShortcut>Q</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>

View File

@ -0,0 +1,127 @@
<script setup lang="ts" generic="T extends { [key: string]: any;}">
const { list, columns, menus, refresh, defaultPageSizes = [10, 20, 30, 40] } = defineProps<{
defaultPageSizes?: number[]
list: T[]
pageTotal: number
columns: {
key: keyof T
label?: string
sortable?: boolean
}[]
menus?: (row: T) => any[][]
refresh?: () => Promise<any>
loading?: boolean
searchText?: string
error?: any
}>()
defineSlots<{
'selected': any
'filter': any
[k: string]: (scope: { row: T }) => any
}>()
const { query, selected, pageSize, pageIndex, sort } = defineModels<{
query: string
selected?: T[]
pageSize: number
pageIndex: number
sort?: {
clumn: keyof T
direction: 'asc' | 'desc'
}
}>()
const attrs = useAttrs()
onMounted(() => {
pageSize.value = defaultPageSizes[0]
pageIndex.value = 1
})
if (menus) {
// eslint-disable-next-line vue/no-mutating-props
columns.push({
label: '',
key: 'opt-actions',
})
}
const showColumns = ref<{ key: any; label?: string }[]>([...columns])
const selectedColumns: any = computed(() => columns.filter(c => showColumns.value.some(sc => sc.key === c.key)))
const slots = useSlots()
const invoices = [
{
invoice: 'INV001',
paymentStatus: 'Paid',
totalAmount: '$250.00',
paymentMethod: 'Credit Card',
},
{
invoice: 'INV002',
paymentStatus: 'Pending',
totalAmount: '$150.00',
paymentMethod: 'PayPal',
},
{
invoice: 'INV003',
paymentStatus: 'Unpaid',
totalAmount: '$350.00',
paymentMethod: 'Bank Transfer',
},
{
invoice: 'INV004',
paymentStatus: 'Paid',
totalAmount: '$450.00',
paymentMethod: 'Credit Card',
},
{
invoice: 'INV005',
paymentStatus: 'Paid',
totalAmount: '$550.00',
paymentMethod: 'PayPal',
},
{
invoice: 'INV006',
paymentStatus: 'Pending',
totalAmount: '$200.00',
paymentMethod: 'Bank Transfer',
},
{
invoice: 'INV007',
paymentStatus: 'Unpaid',
totalAmount: '$300.00',
paymentMethod: 'Credit Card',
},
]
</script>
<template>
<Table>
<TableHeader>
<TableRow>
<TableHead class="w-[100px]">
Invoice
</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead class="text-right">
Amount
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell class="font-medium">
INV001
</TableCell>
<TableCell>Paid</TableCell>
<TableCell>Credit Card</TableCell>
<TableCell class="text-right">
$250.00
</TableCell>
</TableRow>
</TableBody>
</Table>
</template>

View File

@ -0,0 +1,89 @@
import type { ColumnDef } from '@tanstack/vue-table'
import { h } from 'vue'
import { labels, priorities, statuses } from '../data/data'
import type { Task } from '../data/schema'
import DataTableColumnHeader from './DataTableColumnHeader.vue'
import DataTableRowActions from './DataTableRowActions.vue'
import { Checkbox } from '@/lib/registry/new-york/ui/checkbox'
import { Badge } from '@/lib/registry/new-york/ui/badge'
export const columns: ColumnDef<Task>[] = [
{
id: 'select',
header: ({ table }) => h(Checkbox, {
'checked': table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate'),
'onUpdate:checked': value => table.toggleAllPageRowsSelected(!!value),
'ariaLabel': 'Select all',
'class': 'translate-y-0.5',
}),
cell: ({ row }) => h(Checkbox, { 'checked': row.getIsSelected(), 'onUpdate:checked': value => row.toggleSelected(!!value), 'ariaLabel': 'Select row', 'class': 'translate-y-0.5' }),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: 'id',
header: ({ column }) => h(DataTableColumnHeader, { column, title: 'Task' }),
cell: ({ row }) => h('div', { class: 'w-20' }, row.getValue('id')),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: 'title',
header: ({ column }) => h(DataTableColumnHeader, { column, title: 'Title' }),
cell: ({ row }) => {
const label = labels.find(label => label.value === row.original.label)
return h('div', { class: 'flex space-x-2' }, [
label ? h(Badge, { variant: 'outline' }, () => label.label) : null,
h('span', { class: 'max-w-[500px] truncate font-medium' }, row.getValue('title')),
])
},
},
{
accessorKey: 'status',
header: ({ column }) => h(DataTableColumnHeader, { column, title: 'Status' }),
cell: ({ row }) => {
const status = statuses.find(
status => status.value === row.getValue('status'),
)
if (!status)
return null
return h('div', { class: 'flex w-[100px] items-center' }, [
status.icon && h(status.icon, { class: 'mr-2 h-4 w-4 text-muted-foreground' }),
h('span', status.label),
])
},
filterFn: (row, id, value) => {
return value.includes(row.getValue(id))
},
},
{
accessorKey: 'priority',
header: ({ column }) => h(DataTableColumnHeader, { column, title: 'Priority' }),
cell: ({ row }) => {
const priority = priorities.find(
priority => priority.value === row.getValue('priority'),
)
if (!priority)
return null
return h('div', { class: 'flex items-center' }, [
priority.icon && h(priority.icon, { class: 'mr-2 h-4 w-4 text-muted-foreground' }),
h('span', {}, priority.label),
])
},
filterFn: (row, id, value) => {
return value.includes(row.getValue(id))
},
},
{
id: 'actions',
cell: ({ row }) => h(DataTableRowActions, { row }),
},
]

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
defineProps<{
links: {
to: string
name: string
icon: string
}[]
}>()
</script>
<template>
<div class="inline-flex justify-end ">
<TabsItem v-for="(link, i) in links" :key="link.to + i" v-bind="link" />
</div>
</template>

Some files were not shown because too many files have changed in this diff Show More