├── src ├── store │ ├── type.ts │ ├── useUserStore.ts │ └── useProjectStore.ts ├── shims-vue.d.ts ├── vite-env.d.ts ├── service │ ├── file │ │ ├── type.ts │ │ └── index.ts │ ├── axios │ │ ├── config.ts │ │ ├── http.ts │ │ ├── type.ts │ │ └── axios.ts │ ├── user │ │ ├── type.ts │ │ └── index.ts │ ├── blog │ │ ├── type.ts │ │ └── index.ts │ └── project │ │ ├── type.ts │ │ └── index.ts ├── components │ ├── LoginCom │ │ ├── type.ts │ │ ├── config.ts │ │ └── index.vue │ ├── PageFooter │ │ └── index.vue │ ├── Empty │ │ └── index.vue │ ├── PageHeader │ │ ├── type.ts │ │ └── index.vue │ └── MdEditor │ │ ├── config.ts │ │ └── index.vue ├── assets │ ├── dark.png │ ├── light.png │ ├── add_image.gif │ ├── no_project.gif │ └── char.ts ├── view │ ├── Admin │ │ └── index.vue │ ├── Mirror │ │ └── index.vue │ ├── Blog │ │ ├── config.ts │ │ ├── List │ │ │ └── index.vue │ │ ├── Create │ │ │ └── index.vue │ │ ├── Update │ │ │ └── index.vue │ │ └── Item │ │ │ └── index.vue │ ├── Home │ │ └── index.vue │ ├── Project │ │ ├── config.ts │ │ └── index.vue │ └── NotFound │ │ ├── index.vue │ │ └── config.ts ├── libComponents │ ├── JDialog │ │ ├── type.ts │ │ └── index.vue │ ├── JForm │ │ ├── config.ts │ │ ├── type.ts │ │ └── index.vue │ └── JFormItem │ │ ├── JSelect │ │ └── index.vue │ │ └── JUpload │ │ └── index.vue ├── global │ ├── index.ts │ ├── global.scss │ └── registerElements.ts ├── theme │ ├── light.scss │ └── dark.scss ├── utils │ ├── cache.ts │ ├── logChar.ts │ └── message.ts ├── main.ts ├── hooks │ ├── usePermission.ts │ ├── useBlogItem.ts │ ├── useBlogList.ts │ └── useThree.ts ├── router │ ├── index.ts │ └── config.ts └── App.vue ├── .npmrc ├── env.d.ts ├── public ├── three │ ├── LeslieXin.ply │ └── disturb.jpg └── logo.svg ├── tsconfig.json ├── tsconfig.app.json ├── index.html ├── .gitignore ├── tsconfig.node.json ├── vite.config.ts ├── README.md ├── package.json └── .github └── workflows └── deploy.yml /src/store/type.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | 18.17.0 2 | engine-strict=true 3 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@types/three' 2 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/service/file/type.ts: -------------------------------------------------------------------------------- 1 | export interface UploadImage { 2 | imageUrl: string 3 | } 4 | -------------------------------------------------------------------------------- /src/components/LoginCom/type.ts: -------------------------------------------------------------------------------- 1 | export type CaseType = 'Register' | 'Login' | 'Logout' 2 | -------------------------------------------------------------------------------- /src/assets/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leslieXin92/ThousandSunny/HEAD/src/assets/dark.png -------------------------------------------------------------------------------- /src/assets/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leslieXin92/ThousandSunny/HEAD/src/assets/light.png -------------------------------------------------------------------------------- /public/three/LeslieXin.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leslieXin92/ThousandSunny/HEAD/public/three/LeslieXin.ply -------------------------------------------------------------------------------- /public/three/disturb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leslieXin92/ThousandSunny/HEAD/public/three/disturb.jpg -------------------------------------------------------------------------------- /src/assets/add_image.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leslieXin92/ThousandSunny/HEAD/src/assets/add_image.gif -------------------------------------------------------------------------------- /src/assets/no_project.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leslieXin92/ThousandSunny/HEAD/src/assets/no_project.gif -------------------------------------------------------------------------------- /src/service/axios/config.ts: -------------------------------------------------------------------------------- 1 | export const baseURLMap = { 2 | development: '/api', 3 | production: '/api' 4 | } 5 | -------------------------------------------------------------------------------- /src/view/Admin/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /src/libComponents/JDialog/type.ts: -------------------------------------------------------------------------------- 1 | export type OperateType = 'cancel' | 'confirm' 2 | 3 | export interface TitleContextMenuItem { 4 | label: string 5 | callback: () => void 6 | } 7 | -------------------------------------------------------------------------------- /src/view/Mirror/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /src/global/index.ts: -------------------------------------------------------------------------------- 1 | import registerElements from './registerElements' 2 | import type { App } from 'vue' 3 | 4 | export function globalRegister(app: App): void { 5 | registerElements(app) 6 | } 7 | -------------------------------------------------------------------------------- /src/theme/light.scss: -------------------------------------------------------------------------------- 1 | @forward '../../node_modules/element-plus/theme-chalk/src/common/var' with ( 2 | $colors: ( 3 | 'primary': ( 4 | 'base': darkcyan 5 | ), 6 | ) 7 | ); 8 | 9 | @use '../../node_modules/element-plus/theme-chalk/src/index' as *; 10 | -------------------------------------------------------------------------------- /src/theme/dark.scss: -------------------------------------------------------------------------------- 1 | @forward '../../node_modules/element-plus/theme-chalk/src/dark/var' with ( 2 | $colors: ( 3 | 'primary': ( 4 | 'base': darkcyan 5 | ) 6 | ), 7 | ); 8 | 9 | @use '../../node_modules/element-plus/theme-chalk/src/dark/css-vars' as *; 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "compilerOptions": { 4 | "types": ["node", "element-plus/global"], 5 | "baseUrl": "./" 6 | }, 7 | "references": [ 8 | { 9 | "path": "./tsconfig.node.json" 10 | }, 11 | { 12 | "path": "./tsconfig.app.json" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/service/file/index.ts: -------------------------------------------------------------------------------- 1 | import http from '@/service/axios/http' 2 | import type { Res } from '@/service/axios/type' 3 | import type { UploadImage } from './type' 4 | 5 | export function uploadImages(data: FormData) { 6 | return http.post>({ 7 | url: '/file/image', 8 | data 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /src/components/PageFooter/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /src/libComponents/JForm/config.ts: -------------------------------------------------------------------------------- 1 | import JSelect from '@/libComponents/JFormItem/JSelect/index.vue' 2 | import JUpload from '@/libComponents/JFormItem/JUpload/index.vue' 3 | 4 | export const formItemMap = { 5 | 'input': 'el-input', 6 | 'select': JSelect, 7 | 'datePicker': 'ElDatePicker', 8 | 'switch': 'ElSwitch', 9 | 'upload': JUpload 10 | } 11 | -------------------------------------------------------------------------------- /src/components/Empty/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /src/view/Blog/config.ts: -------------------------------------------------------------------------------- 1 | import type { Schema } from '@/libComponents/JForm/type' 2 | 3 | export const schema: Schema[] = [ 4 | { 5 | component: 'switch', 6 | key: 'visibility', 7 | itemAttrs: { 8 | label: 'public' 9 | }, 10 | attrs: { 11 | inactiveValue: 'private', 12 | activeValue: 'public' 13 | } 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "exclude": ["src/**/__tests__/*", "**/node_modules/**", "**/dist/**"], 5 | "compilerOptions": { 6 | "composite": true, 7 | "baseUrl": ".", 8 | "paths": { 9 | "@/*": ["./src/*"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/view/Home/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 18 | -------------------------------------------------------------------------------- /src/components/PageHeader/type.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | 3 | export interface ContextmenuItem { 4 | label: string 5 | condition: boolean | Ref 6 | routePath?: string 7 | customClickCallback?: () => void 8 | } 9 | 10 | export interface MenuItem { 11 | label: string 12 | routePath: string 13 | condition: boolean | Ref 14 | contextmenuList?: ContextmenuItem[] 15 | } 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Thousand Sunny 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | front 14 | dist-ssr 15 | coverage 16 | *.local 17 | 18 | /cypress/videos/ 19 | /cypress/screenshots/ 20 | 21 | # Editor directories and files 22 | .vscode/* 23 | !.vscode/extensions.json 24 | .idea 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*", 7 | "nightwatch.conf.*", 8 | "playwright.config.*" 9 | ], 10 | "compilerOptions": { 11 | "composite": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Bundler", 14 | "types": ["node"], 15 | "allowSyntheticDefaultImports": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/service/user/type.ts: -------------------------------------------------------------------------------- 1 | export type UserPermissionType = 'normal' | 'admin' | 'superAdmin' 2 | 3 | export interface LoginParams { 4 | username: string 5 | password: string 6 | } 7 | 8 | export interface LoginRes { 9 | id: string 10 | username: string 11 | token: string 12 | permission: UserPermissionType 13 | } 14 | 15 | export interface RegisterParams { 16 | username: string 17 | password: string 18 | confirmPassword: string 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/cache.ts: -------------------------------------------------------------------------------- 1 | class LocalCache { 2 | set(key: string, value: any) { 3 | window.localStorage.setItem(key, JSON.stringify(value)) 4 | } 5 | 6 | get(key: string) { 7 | const value = window.localStorage.getItem(key) 8 | if (value) return JSON.parse(value) 9 | } 10 | 11 | delete(key: string) { 12 | window.localStorage.removeItem(key) 13 | } 14 | 15 | clear() { 16 | window.localStorage.clear() 17 | } 18 | } 19 | 20 | export const localCache = new LocalCache() 21 | -------------------------------------------------------------------------------- /src/utils/logChar.ts: -------------------------------------------------------------------------------- 1 | import { leslie } from '@/assets/char' 2 | 3 | const love = () => { 4 | const begin = new Date('2022-01-18').getTime() 5 | const now = new Date().getTime() 6 | const beenTogether = parseInt(String((now - begin) / 1000 / 60 / 60 / 24)) + 1 7 | return `Leslie and Cabbage have been together for \x1b[41m\x1b[30m\x1b[1m ${beenTogether} \x1b[0m days!` 8 | } 9 | 10 | const logChar = () => { 11 | console.log(leslie) 12 | console.log(love()) 13 | } 14 | 15 | export default logChar 16 | -------------------------------------------------------------------------------- /src/components/MdEditor/config.ts: -------------------------------------------------------------------------------- 1 | import type { Footers, ToolbarNames } from 'md-editor-v3' 2 | 3 | export const toolbars: ToolbarNames[] = [ 4 | '-', 5 | 'title', 6 | 'quote', 7 | '-', 8 | 'bold', 9 | 'italic', 10 | 'strikeThrough', 11 | '-', 12 | 'orderedList', 13 | 'unorderedList', 14 | '-', 15 | 'codeRow', 16 | 'code', 17 | '-', 18 | 'link', 19 | 'image', 20 | '-', 21 | 'table', 22 | '=', 23 | 'catalog' 24 | ] 25 | 26 | export const footers: Footers[] = ['markdownTotal'] 27 | -------------------------------------------------------------------------------- /src/utils/message.ts: -------------------------------------------------------------------------------- 1 | import { ElMessage } from 'element-plus' 2 | 3 | class Message { 4 | success(message: string) { 5 | return ElMessage({ 6 | message, 7 | type: 'success' 8 | }) 9 | } 10 | 11 | error(message: string) { 12 | return ElMessage({ 13 | message, 14 | type: 'error' 15 | }) 16 | } 17 | 18 | warning(message: string) { 19 | return ElMessage({ 20 | message, 21 | type: 'warning' 22 | }) 23 | } 24 | } 25 | 26 | export default new Message() 27 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import { resolve } from 'path' 4 | 5 | export default defineConfig({ 6 | server: { 7 | host: '0.0.0.0', 8 | proxy: { 9 | '/api': { 10 | target: 'http://127.0.0.1:8000', // dev 11 | // target: 'https://124.221.235.145:8000', // prod 12 | changeOrigin: true, 13 | rewrite: (path) => path.replace(/^\/api/, '') 14 | } 15 | } 16 | }, 17 | plugins: [vue()], 18 | resolve: { 19 | alias: { 20 | '@': resolve(__dirname, 'src') 21 | } 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /src/libComponents/JFormItem/JSelect/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 26 | 27 | 29 | -------------------------------------------------------------------------------- /src/service/axios/http.ts: -------------------------------------------------------------------------------- 1 | import { storeToRefs } from 'pinia' 2 | import { useUserStore } from '@/store/useUserStore' 3 | import Http from './axios' 4 | import { baseURLMap } from './config' 5 | import type { EnvType } from '@/service/axios/type' 6 | 7 | export default new Http({ 8 | baseURL: baseURLMap[import.meta.env.MODE as EnvType], 9 | timeout: 10000, 10 | interceptors: { 11 | requestInterceptor(config) { 12 | const userStore = useUserStore() 13 | const { isLogin, token } = storeToRefs(userStore) 14 | if (isLogin.value && token.value) config.headers!.authorization = 'Bearer ' + token.value 15 | return config 16 | } 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { createPinia } from 'pinia' 3 | import piniaPluginPersistedState from 'pinia-plugin-persistedstate' 4 | import App from '@/App.vue' 5 | import router from '@/router' 6 | import { globalRegister } from '@/global' 7 | import logChar from '@/utils/logChar' 8 | import '@/global/global.scss' 9 | import '@/theme/light.scss' 10 | import '@/theme/dark.scss' 11 | 12 | const pinia = createPinia() 13 | pinia.use(piniaPluginPersistedState) 14 | 15 | const app = createApp(App) 16 | app.use(globalRegister) 17 | app.use(router) 18 | app.use(pinia) 19 | 20 | app.mount('#app') 21 | 22 | logChar() 23 | 24 | console.log('Automatic Deploy!!!!') 25 | -------------------------------------------------------------------------------- /src/hooks/usePermission.ts: -------------------------------------------------------------------------------- 1 | import { storeToRefs } from 'pinia' 2 | import { useUserStore } from '@/store/useUserStore' 3 | import type { UserPermissionType } from '@/service/user/type' 4 | 5 | const usePermission = (permissionType: UserPermissionType) => { 6 | const userStore = useUserStore() 7 | const { isLogin, token, permission } = storeToRefs(userStore) 8 | 9 | if (!isLogin.value || !token.value) return false 10 | if (!permission.value) return false 11 | else if (permission.value === 'superAdmin') return true 12 | else if (permission.value === 'admin') return permissionType !== 'superAdmin' 13 | return permissionType === permission.value 14 | } 15 | 16 | export default usePermission 17 | -------------------------------------------------------------------------------- /src/service/blog/type.ts: -------------------------------------------------------------------------------- 1 | export type VisibilityType = '' | 'public' | 'private' 2 | 3 | export interface BlogItem { 4 | id: number 5 | visibility: VisibilityType 6 | title: string 7 | content: string 8 | createdAt: string 9 | } 10 | 11 | export interface GetBlogListParams { 12 | visibility?: VisibilityType 13 | page: number 14 | } 15 | 16 | export interface GetBlogListRes { 17 | blogList: Omit[] 18 | totalCount: number 19 | } 20 | 21 | export interface CreateBlogParams { 22 | title: string 23 | content: string 24 | type: 'public' | 'private' 25 | } 26 | 27 | export interface UpdateBlogParams { 28 | id: number 29 | title: string 30 | content: string 31 | type: 'public' | 'private' 32 | } 33 | -------------------------------------------------------------------------------- /src/service/axios/type.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosRequestConfig, AxiosResponse } from 'axios' 2 | 3 | export interface Res { 4 | code: 0 | 1 5 | data: D 6 | msg: string 7 | } 8 | 9 | export interface HttpInterceptors { 10 | requestInterceptor?: (config: AxiosRequestConfig) => AxiosRequestConfig 11 | requestInterceptorCatch?: (error: Error) => any 12 | responseInterceptor?: (res: T) => T 13 | responseInterceptorCatch?: (error: Error) => any 14 | } 15 | 16 | export interface HttpConfig extends AxiosRequestConfig { 17 | interceptors?: HttpInterceptors 18 | loading?: boolean 19 | showSuccessMsg?: boolean 20 | showErrorMsg?: boolean 21 | } 22 | 23 | export type EnvType = 'development' | 'production' 24 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import usePermission from '@/hooks/usePermission' 3 | import { normalRoutes, authRoutes } from './config' 4 | import type { RouterScrollBehavior } from 'vue-router' 5 | 6 | const scrollBehavior: RouterScrollBehavior = (to, _, savedPosition) => { 7 | if (savedPosition && to.meta.keepAlive) return savedPosition 8 | return { left: 0, top: 0 } 9 | } 10 | 11 | const router = createRouter({ 12 | history: createWebHistory(import.meta.env.BASE_URL), 13 | scrollBehavior, 14 | routes: [...normalRoutes, ...authRoutes] 15 | }) 16 | 17 | router.beforeEach(to => { 18 | const auth = usePermission('normal') 19 | if (!auth && authRoutes.some(route => route.path === to.path)) return '/NotFound' 20 | }) 21 | 22 | export default router 23 | -------------------------------------------------------------------------------- /src/service/user/index.ts: -------------------------------------------------------------------------------- 1 | import http from '@/service/axios/http' 2 | import type { Res } from '@/service/axios/type' 3 | import type { LoginParams, LoginRes, RegisterParams } from './type' 4 | 5 | export function register(data: RegisterParams): Promise { 6 | return http.post({ 7 | url: '/user/register', 8 | data, 9 | showSuccessMsg: true, 10 | showErrorMsg: true 11 | }) 12 | } 13 | 14 | export function login(data: LoginParams) { 15 | return http.post>({ 16 | url: '/user/login', 17 | data, 18 | showSuccessMsg: true, 19 | showErrorMsg: true 20 | }) 21 | } 22 | 23 | export function autoLogin(data: any) { 24 | return http.post>({ 25 | url: '/user/autoLogin', 26 | data, 27 | showSuccessMsg: false, 28 | showErrorMsg: false 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /src/service/project/type.ts: -------------------------------------------------------------------------------- 1 | export type ProjectStatus = 'pending' | 'doing' | 'done' 2 | 3 | export interface ProjectItem { 4 | id: number 5 | name: string 6 | coverIcon: string 7 | description: string 8 | technologyStack: string[] 9 | status: ProjectStatus 10 | startAt: string 11 | doneAt: string 12 | createdAt: string 13 | updatedAt: string 14 | } 15 | 16 | export interface CreateProjectParams { 17 | name: string 18 | coverIcon: string 19 | description?: string 20 | technologyStack?: string[] 21 | status?: ProjectStatus 22 | codeAddress?: string 23 | onlineAddress?: string 24 | startAt?: string 25 | doneAt?: string 26 | } 27 | 28 | export interface UpdateProjectParams { 29 | id: number 30 | name: string 31 | coverIcon: string 32 | description?: string 33 | technologyStack?: string[] 34 | status?: ProjectStatus 35 | codeAddress?: string 36 | onlineAddress?: string 37 | startAt?: string 38 | doneAt?: string 39 | } 40 | -------------------------------------------------------------------------------- /src/assets/char.ts: -------------------------------------------------------------------------------- 1 | export const leslie = ` 2 | /\\\\\\ /\\\\\\\\\\\\\\\\\\\\ /\\\\\\\\\\\\\\\\\\\\ /\\\\\\ /\\\\\\\\\\\\\\\\ /\\\\\\\\\\\\\\\\\\\\ 3 | \\/\\\\\\ \\/\\\\\\_____/ /\\\\\\_______/ \\/\\\\\\ \\/_/\\\\\\_/ \\/\\\\\\_____/ 4 | \\/\\\\\\ \\/\\\\\\ \\/\\\\\\ \\/\\\\\\ \\/\\\\\\ \\/\\\\\\ 5 | \\/\\\\\\ \\/\\\\\\\\\\\\\\\\\\\\ \\/\\\\\\\\\\\\\\\\\\\\ \\/\\\\\\ \\/\\\\\\ \\/\\\\\\\\\\\\\\\\\\\\ 6 | \\/\\\\\\ \\/\\\\\\_____/ \\/_______/\\\\\\ \\/\\\\\\ \\/\\\\\\ \\/\\\\\\_____/ 7 | \\/\\\\\\ \\/\\\\\\ \\/\\\\\\ \\/\\\\\\ \\/\\\\\\ \\/\\\\\\ 8 | \\/\\\\\\\\\\\\\\\\\\\\ \\/\\\\\\\\\\\\\\\\\\\\ /\\\\\\\\\\\\\\\\\\/ \\/\\\\\\\\\\\\\\\\\\\\ /\\\\\\\\\\\\\\\\ \\/\\\\\\\\\\\\\\\\\\\\ 9 | \\/________/ \\/________/ \\/_______/ \\/________/ \\/______/ \\/________/ 10 | ` 11 | -------------------------------------------------------------------------------- /src/libComponents/JForm/type.ts: -------------------------------------------------------------------------------- 1 | import { formItemMap } from './config' 2 | import type { 3 | FormItemProps, 4 | InputProps, 5 | DatePickerProps, 6 | InputEmits, 7 | ISelectProps, 8 | SwitchProps, 9 | UploadProps 10 | } from 'element-plus' 11 | 12 | type SchemaComType = keyof typeof formItemMap 13 | 14 | export interface SchemaAttrs { 15 | 'input': Partial & Partial | any // TODO - type 16 | 'select': Partial 17 | 'datePicker': Partial 18 | 'switch': Partial 19 | 'upload': Partial 20 | } 21 | 22 | export interface Schema { 23 | component: SchemaComType 24 | key: string 25 | itemAttrs?: Partial 26 | attrs?: SchemaAttrs[SchemaComType] 27 | custom?: boolean 28 | hide?: boolean 29 | } 30 | 31 | export interface JFormRef { 32 | getFormData: () => Record 33 | setFormData: (key: string, value: unknown) => void 34 | reset: () => void 35 | validate: () => Promise 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Thousand Sunny 2 | 3 | > 万里阳光号 4 | 5 | #### [线上地址](https://www.leslie.xin/home) 6 | 7 | #### [后端仓库地址](https://github.com/leslieXin92/GoingMerry) 8 | 9 | ### ~~一些废话:~~ 10 | 11 | 此项目为个人网站前端,于`2022-12-20`立项,命名为`万里阳光号`,灵感来自于海贼王。万里阳光号是草帽海贼团的载体,承载了一路以来的经历,见证了一路上的成长喜怒哀乐,借以至此。 12 | 13 | ### 技术选型: 14 | 15 | - [vue3](https://github.com/vuejs/core)、 16 | [vue-router](https://github.com/vuejs/router)、 17 | [pinia](https://github.com/vuejs/pinia) 18 | 搭建前端页面 19 | 20 | - [axios](https://github.com/axios/axios) 21 | 实现前后端通讯 22 | 23 | - [element-plus](https://github.com/element-plus/element-plus) 24 | 作为 UI 组件库 25 | 26 | - [lodash](https://github.com/lodash/lodash)、 27 | [vueuse](https://github.com/vueuse/vueuse)、 28 | [dayjs](https://github.com/iamkun/dayjs) 29 | 做扩展支撑 30 | 31 | - [three.js](https://github.com/mrdoob/three.js) 32 | 用来~~装逼~~ 33 | 34 | ### ~~引以为傲~~ 35 | 36 | 1. axios 的 class 封装 37 | 2. JForm、JDialog 等类库输出 38 | 3. useBlogList、useBlogItem 等策略模式 39 | 4. 无敌巧妙的产品思路 40 | 5. 无敌优美的前端页面 41 | 6. ~~用来装逼的 three.js~~ 42 | -------------------------------------------------------------------------------- /src/hooks/useBlogItem.ts: -------------------------------------------------------------------------------- 1 | import { ref, watch, computed } from 'vue' 2 | import { useRoute } from 'vue-router' 3 | import { getBlogItem } from '@/service/blog' 4 | 5 | const useBlogItem = (type: 'create' | 'show' | 'update') => { 6 | const route = useRoute() 7 | 8 | const title = ref('') 9 | const content = ref('') 10 | const formData = ref({ 11 | visibility: 'public' 12 | }) 13 | 14 | const loading = ref(false) 15 | 16 | const id = computed(() => { 17 | return Number(route.params.id) 18 | }) 19 | 20 | watch( 21 | id, 22 | async (newId) => { 23 | if (type === 'create' || !newId) return 24 | loading.value = true 25 | try { 26 | const { data } = await getBlogItem(newId) 27 | title.value = data.title 28 | formData.value.visibility = data.visibility 29 | content.value = data.content 30 | } finally { 31 | loading.value = false 32 | } 33 | }, 34 | { immediate: true } 35 | ) 36 | 37 | return { 38 | id, 39 | title, 40 | content, 41 | formData, 42 | loading 43 | } 44 | } 45 | 46 | export default useBlogItem 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thousand-sunny", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "run-p type-check \"build-only {@}\" --", 8 | "preview": "vite preview", 9 | "build-only": "vite build", 10 | "type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false" 11 | }, 12 | "dependencies": { 13 | "@vueuse/core": "^10.5.0", 14 | "axios": "^1.5.1", 15 | "dayjs": "^1.11.10", 16 | "element-plus": "^2.4.0", 17 | "gsap": "^3.12.2", 18 | "lodash": "^4.17.21", 19 | "md-editor-v3": "^4.12.3", 20 | "pinia": "^2.1.6", 21 | "pinia-plugin-persistedstate": "^3.2.0", 22 | "qs": "^6.11.2", 23 | "three": "^0.148.0", 24 | "vue": "^3.3.4", 25 | "vue-router": "^4.2.4" 26 | }, 27 | "devDependencies": { 28 | "@tsconfig/node18": "^18.2.2", 29 | "@types/node": "^18.18.6", 30 | "@types/three": "^0.148.0", 31 | "@vitejs/plugin-vue": "^4.3.4", 32 | "@vue/tsconfig": "^0.4.0", 33 | "npm-run-all2": "^6.0.6", 34 | "sass": "^1.69.3", 35 | "typescript": "~5.2.0", 36 | "vite": "^4.5.3", 37 | "vue-tsc": "^1.8.11" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/global/global.scss: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | font-family: Georgia, 'Microsoft YaHei', serif; 6 | } 7 | 8 | li { 9 | list-style: none; 10 | } 11 | 12 | a { 13 | text-decoration: none; 14 | font-family: Arial, sans-serif; 15 | color: #bcbcbc; 16 | } 17 | 18 | a:hover { 19 | text-decoration: underline #ccc; 20 | } 21 | 22 | /* 滚动条相关 */ 23 | // 滚动条有滑块的轨道部分 24 | ::-webkit-scrollbar-track-piece { 25 | background-color: transparent; 26 | border-radius: 0; 27 | } 28 | 29 | // 整个滚动条 30 | ::-webkit-scrollbar { 31 | width: 4px; 32 | height: 4px; 33 | display: block; 34 | } 35 | 36 | // 滚动条竖向滑块 37 | ::-webkit-scrollbar-thumb:vertical { 38 | background-color: #999; 39 | border-radius: 2px; 40 | 41 | &:hover { 42 | background-color: #666; 43 | border-radius: 4px; 44 | } 45 | } 46 | 47 | // 滚动条横向滑块 48 | ::-webkit-scrollbar-thumb:horizontal { 49 | background-color: #999; 50 | border-radius: 2px; 51 | 52 | &:hover { 53 | background-color: #666; 54 | border-radius: 4px; 55 | } 56 | } 57 | 58 | // 同时有垂直和水平滚动条时交汇的部分 59 | ::-webkit-scrollbar-corner { 60 | display: block; // 修复交汇时出现的白块 61 | } 62 | -------------------------------------------------------------------------------- /src/global/registerElements.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { 3 | ElContainer, 4 | ElHeader, 5 | ElMain, 6 | ElFooter, 7 | ElButton, 8 | ElDialog, 9 | ElForm, 10 | ElFormItem, 11 | ElInput, 12 | ElSelect, 13 | ElOption, 14 | ElDatePicker, 15 | ElTable, 16 | ElTableColumn, 17 | ElMenu, 18 | ElSubMenu, 19 | ElMenuItemGroup, 20 | ElMenuItem, 21 | ElCalendar, 22 | ElCard, 23 | ElTooltip, 24 | ElAffix, 25 | ElPageHeader, 26 | ElSwitch, 27 | ElDivider, 28 | ElRow, 29 | ElCol, 30 | ElUpload, 31 | ElTag 32 | } from 'element-plus' 33 | 34 | const components = [ 35 | ElContainer, 36 | ElHeader, 37 | ElMain, 38 | ElFooter, 39 | ElButton, 40 | ElDialog, 41 | ElForm, 42 | ElFormItem, 43 | ElInput, 44 | ElSelect, 45 | ElOption, 46 | ElTable, 47 | ElTableColumn, 48 | ElMenu, 49 | ElSubMenu, 50 | ElMenuItemGroup, 51 | ElMenuItem, 52 | ElCalendar, 53 | ElCard, 54 | ElDatePicker, 55 | ElTooltip, 56 | ElAffix, 57 | ElPageHeader, 58 | ElSwitch, 59 | ElDivider, 60 | ElRow, 61 | ElCol, 62 | ElUpload, 63 | ElTag 64 | ] 65 | 66 | export default function (app: App) { 67 | for (const cpn of components) { 68 | app.component(cpn.name, cpn) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/service/project/index.ts: -------------------------------------------------------------------------------- 1 | import http from '@/service/axios/http' 2 | import type { Res } from '@/service/axios/type' 3 | import type { ProjectItem, CreateProjectParams, UpdateProjectParams } from './type' 4 | 5 | export async function getProjectList() { 6 | return await http.get>({ 7 | url: '/project', 8 | showSuccessMsg: false, 9 | showErrorMsg: true 10 | }) 11 | } 12 | 13 | export async function getProjectItem(projectId: number) { 14 | return await http.get>({ 15 | url: `/project/${projectId}`, 16 | showSuccessMsg: false, 17 | showErrorMsg: true 18 | }) 19 | } 20 | 21 | export async function createProject(data: CreateProjectParams) { 22 | return http.post({ 23 | url: '/project', 24 | data, 25 | showSuccessMsg: true, 26 | showErrorMsg: true 27 | }) 28 | } 29 | 30 | export async function updateProject(data: UpdateProjectParams) { 31 | return http.patch({ 32 | url: `/project/${data.id}`, 33 | data, 34 | showSuccessMsg: true, 35 | showErrorMsg: true 36 | }) 37 | } 38 | 39 | export async function deleteProject(projectId: number) { 40 | return http.delete({ 41 | url: `/project/${projectId}`, 42 | showSuccessMsg: true, 43 | showErrorMsg: true 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /src/service/blog/index.ts: -------------------------------------------------------------------------------- 1 | import http from '@/service/axios/http' 2 | import type { Res } from '@/service/axios/type' 3 | import type { 4 | BlogItem, 5 | GetBlogListParams, 6 | GetBlogListRes, 7 | CreateBlogParams, 8 | UpdateBlogParams 9 | } from '@/service/blog/type' 10 | 11 | export function getBlogList(params: GetBlogListParams) { 12 | return http.get>({ 13 | url: '/blog', 14 | params, 15 | showSuccessMsg: false, 16 | showErrorMsg: true 17 | }) 18 | } 19 | 20 | export function getBlogItem(blogId: number) { 21 | return http.get>({ 22 | url: `/blog/${blogId}`, 23 | showSuccessMsg: false, 24 | showErrorMsg: true 25 | }) 26 | } 27 | 28 | export function createBlog(data: CreateBlogParams) { 29 | return http.post({ 30 | url: '/blog', 31 | data, 32 | showSuccessMsg: true, 33 | showErrorMsg: true 34 | }) 35 | } 36 | 37 | export function updateBlog(data: UpdateBlogParams) { 38 | return http.patch({ 39 | url: `/blog/${data.id}`, 40 | data, 41 | showSuccessMsg: true, 42 | showErrorMsg: true 43 | }) 44 | } 45 | 46 | export function deleteBlog(blogId: number) { 47 | return http.delete({ 48 | url: `/blog/${blogId}`, 49 | showSuccessMsg: true, 50 | showErrorMsg: true 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /src/router/config.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | 3 | export const normalRoutes: RouteRecordRaw[] = [ 4 | { 5 | path: '/', 6 | redirect: '/home' 7 | }, 8 | { 9 | path: '/home', 10 | name: 'home', 11 | component: () => import('@/view/Home/index.vue') 12 | }, 13 | { 14 | path: '/blog', 15 | name: 'blog', 16 | component: () => import('@/view/Blog/List/index.vue'), 17 | meta: { 18 | keepAlive: true 19 | } 20 | }, 21 | { 22 | path: '/blog/:id', 23 | name: 'blogItem', 24 | component: () => import('@/view/Blog/Item/index.vue') 25 | }, 26 | { 27 | path: '/project', 28 | name: 'project', 29 | component: () => import('@/view/Project/index.vue') 30 | }, 31 | { 32 | path: '/mirror', 33 | name: 'mirror', 34 | component: () => import('@/view/Mirror/index.vue') 35 | }, 36 | { 37 | path: '/:pathMatch(.*)', 38 | name: 'NotFound', 39 | component: () => import('@/view/NotFound/index.vue') 40 | } 41 | ] 42 | 43 | export const authRoutes: RouteRecordRaw[] = [ 44 | { 45 | path: '/blog/create', 46 | name: 'createBlog', 47 | component: () => import('@/view/Blog/Create/index.vue') 48 | }, 49 | { 50 | path: '/blog/update/:id', 51 | name: 'updateBlog', 52 | component: () => import('@/view/Blog/Update/index.vue') 53 | }, 54 | { 55 | path: '/admin', 56 | name: 'admin', 57 | component: () => import('@/view/Admin/index.vue') 58 | } 59 | ] 60 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Set Sail 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | types: 8 | - closed 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | node-version: [ 18.x ] 16 | 17 | steps: 18 | - name: Checkout Code 19 | uses: actions/checkout@v4 20 | 21 | - name: Setup Node ${{ matrix.node-version }} 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | 26 | - name: Install Dependencies 27 | run: npm install 28 | 29 | - name: Build And Package 30 | run: npm run build 31 | 32 | - name: Archive artifacts 33 | uses: actions/upload-artifact@v4 34 | with: 35 | name: ThousandSunny 36 | path: dist 37 | retention-days: 7 38 | 39 | deploy: 40 | needs: build 41 | runs-on: ubuntu-latest 42 | 43 | steps: 44 | - name: Checkout Code 45 | uses: actions/checkout@v4 46 | 47 | - name: Download artifacts 48 | uses: actions/download-artifact@v4 49 | with: 50 | name: ThousandSunny 51 | 52 | - name: Deploy To Server 53 | uses: easingthemes/ssh-deploy@main 54 | with: 55 | SSH_PRIVATE_KEY: ${{ secrets.FTP_PRIVATE_KEY }} 56 | ARGS: "-rlgoDzvc -i --delete" 57 | SOURCE: "/" 58 | REMOTE_HOST: ${{ secrets.FTP_HOST }} 59 | REMOTE_USER: ${{ secrets.FTP_USER }} 60 | TARGET: ${{ secrets.FTP_TARGET }} 61 | EXCLUDE: "/dist/, /node_modules/" 62 | -------------------------------------------------------------------------------- /src/libComponents/JFormItem/JUpload/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 56 | 57 | 65 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 49 | 50 | 83 | -------------------------------------------------------------------------------- /src/components/MdEditor/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 67 | 68 | 73 | -------------------------------------------------------------------------------- /src/view/Blog/List/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 33 | 34 | 85 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/store/useUserStore.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { useRouter } from 'vue-router' 3 | import { defineStore } from 'pinia' 4 | import { autoLogin, login, register } from '@/service/user' 5 | import { localCache } from '@/utils/cache' 6 | import type { LoginParams, LoginRes, RegisterParams, UserPermissionType } from '@/service/user/type' 7 | 8 | export const useUserStore = defineStore( 9 | 'user', 10 | () => { 11 | const router = useRouter() 12 | 13 | const userInfo = ref>() 14 | 15 | const isLogin = ref(false) 16 | const token = ref() 17 | const permission = ref() 18 | 19 | const changeIsLogin = (loginState: boolean) => { 20 | isLogin.value = loginState 21 | } 22 | 23 | const handleRegister = async (registerParams: RegisterParams) => { 24 | await register(registerParams) 25 | await handleLogin(registerParams) 26 | } 27 | 28 | const handleLogin = async (loginParams: LoginParams) => { 29 | const { data } = await login(loginParams) 30 | userInfo.value = data 31 | changeIsLogin(true) 32 | token.value = data.token 33 | permission.value = data.permission 34 | } 35 | 36 | const handleLogout = async () => { 37 | userInfo.value = undefined 38 | changeIsLogin(false) 39 | permission.value = undefined 40 | await router.push('/home') 41 | } 42 | 43 | const handleAutoLogin = async () => { 44 | if (isLogin.value) return 45 | const userCache = localCache.get('userStore') 46 | if (!userCache?.userInfo) return 47 | const { code, data } = await autoLogin({ 48 | id: userCache.userInfo.id, 49 | username: userCache.userInfo.username, 50 | permission: userCache.userInfo.permission 51 | }) 52 | if (!code) { 53 | changeIsLogin(true) 54 | token.value = data.token 55 | } 56 | } 57 | 58 | return { 59 | userInfo, 60 | isLogin, 61 | token, 62 | permission, 63 | changeIsLogin, 64 | handleRegister, 65 | handleLogin, 66 | handleLogout, 67 | handleAutoLogin 68 | } 69 | }, 70 | { 71 | persist: { 72 | key: 'userStore', 73 | storage: localStorage 74 | } 75 | } 76 | ) 77 | -------------------------------------------------------------------------------- /src/components/LoginCom/config.ts: -------------------------------------------------------------------------------- 1 | import type { FormRules } from 'element-plus' 2 | import type { Schema } from '@/libComponents/JForm/type' 3 | 4 | export const registerSchema: Schema[] = [ 5 | { 6 | component: 'input', 7 | key: 'username', 8 | itemAttrs: { 9 | label: 'username', 10 | labelWidth: 90 11 | }, 12 | attrs: { 13 | type: 'text' 14 | } 15 | }, 16 | { 17 | component: 'input', 18 | key: 'password', 19 | itemAttrs: { 20 | label: 'password', 21 | labelWidth: 90 22 | }, 23 | attrs: { 24 | type: 'text', 25 | showPassword: true 26 | } 27 | }, 28 | { 29 | component: 'input', 30 | key: 'confirmPassword', 31 | itemAttrs: { 32 | label: 'confirm', 33 | labelWidth: 90 34 | }, 35 | attrs: { 36 | type: 'text', 37 | showPassword: true 38 | } 39 | } 40 | ] 41 | 42 | export const registerRules: FormRules = { 43 | username: [ 44 | { 45 | required: true, 46 | message: 'Please Input Activity Username', 47 | trigger: 'blur' 48 | } 49 | ], 50 | password: [ 51 | { 52 | required: true, 53 | message: 'Please Input Activity Password', 54 | trigger: 'blur' 55 | } 56 | ], 57 | confirmPassword: [ 58 | { 59 | required: true, 60 | message: 'Please Input Activity Password', 61 | trigger: 'blur' 62 | } 63 | ] 64 | } 65 | 66 | export const loginSchema: Schema[] = [ 67 | { 68 | component: 'input', 69 | key: 'username', 70 | itemAttrs: { 71 | label: 'username', 72 | labelWidth: 90 73 | }, 74 | attrs: { 75 | type: 'text' 76 | } 77 | }, 78 | { 79 | component: 'input', 80 | key: 'password', 81 | itemAttrs: { 82 | label: 'password', 83 | labelWidth: 90 84 | }, 85 | attrs: { 86 | type: 'password', 87 | showPassword: true 88 | } 89 | } 90 | ] 91 | 92 | export const loginRules: FormRules = { 93 | username: [ 94 | { 95 | required: true, 96 | message: 'Please Input Activity Username', 97 | trigger: 'blur' 98 | } 99 | ], 100 | password: [ 101 | { 102 | required: true, 103 | message: 'Please Input Activity Password', 104 | trigger: 'blur' 105 | } 106 | ] 107 | } 108 | -------------------------------------------------------------------------------- /src/hooks/useBlogList.ts: -------------------------------------------------------------------------------- 1 | import { ref, shallowRef, watch, onMounted } from 'vue' 2 | import { useRoute } from 'vue-router' 3 | import { debounce } from 'lodash' 4 | import dayjs from 'dayjs' 5 | import { getBlogList } from '@/service/blog' 6 | import usePermission from '@/hooks/usePermission' 7 | import message from '@/utils/message' 8 | import type { GetBlogListParams, BlogItem } from '@/service/blog/type' 9 | 10 | const useBlogList = () => { 11 | const route = useRoute() 12 | 13 | const auth = usePermission('normal') 14 | const params = ref({ 15 | visibility: auth ? '' : 'public', 16 | page: 1 17 | }) 18 | const blogList = ref<(Omit & { showYear: boolean })[]>([]) 19 | const totalCount = shallowRef() 20 | const isFetching = ref(false) 21 | 22 | const load = async () => { 23 | if (isFetching.value) return 24 | if (blogList.value.length === totalCount.value) return hasLoadAll() 25 | isFetching.value = true 26 | const { data } = await getBlogList(params.value) 27 | blogList.value = [...blogList.value, ...data.blogList].map((blog, index, array) => ({ 28 | ...blog, 29 | showYear: !index || dayjs(array[index - 1]?.createdAt).year() !== dayjs(blog.createdAt).year() 30 | })) 31 | totalCount.value = data.totalCount 32 | params.value.page++ 33 | isFetching.value = false 34 | } 35 | 36 | const hasLoadAll = () => { 37 | message.success('That\'s All') 38 | window.removeEventListener('scroll', loadOnReachBottom) 39 | } 40 | 41 | const loadOnReachBottom = debounce(async () => { 42 | if (isFetching.value) return 43 | const scrollTop = document.documentElement.scrollTop || document.body.scrollTop 44 | const clientHeight = document.documentElement.clientHeight 45 | const scrollHeight = document.documentElement.scrollHeight 46 | if (scrollHeight - scrollTop - clientHeight <= 800) await load() 47 | }, 200) 48 | 49 | watch( 50 | () => route.path, 51 | path => { 52 | if (path === '/blog') 53 | window.addEventListener('scroll', loadOnReachBottom) 54 | else 55 | window.removeEventListener('scroll', loadOnReachBottom) 56 | }, 57 | { immediate: true } 58 | ) 59 | 60 | onMounted(async () => { 61 | await load() 62 | }) 63 | 64 | return { 65 | blogList, 66 | params, 67 | isFetching, 68 | totalCount 69 | } 70 | } 71 | 72 | export default useBlogList 73 | -------------------------------------------------------------------------------- /src/libComponents/JForm/index.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 98 | 99 | 110 | -------------------------------------------------------------------------------- /src/store/useProjectStore.ts: -------------------------------------------------------------------------------- 1 | import { computed, ref, watch } from 'vue' 2 | import { defineStore } from 'pinia' 3 | import { createProject, deleteProject, getProjectItem, updateProject } from '@/service/project' 4 | import type { CreateProjectParams } from '@/service/project/type' 5 | 6 | type HandleProjectType = 'create' | 'show' | 'update' | 'delete' 7 | 8 | const initFormData: CreateProjectParams = { 9 | name: '', 10 | coverIcon: '', 11 | technologyStack: [], 12 | status: 'pending' 13 | } 14 | 15 | export const useProjectStore = defineStore( 16 | 'project', 17 | () => { 18 | const curType = ref('show') 19 | const changeCurType = (newType: HandleProjectType) => { 20 | curType.value = newType 21 | } 22 | 23 | const dialogVisible = ref(false) 24 | const changeDialogVisible = (newVisible: boolean) => { 25 | dialogVisible.value = newVisible 26 | } 27 | 28 | const formData = ref(initFormData) 29 | 30 | watch(curType, (newType) => { 31 | if (newType === 'create') formData.value = initFormData 32 | }) 33 | 34 | const editable = computed(() => { 35 | return ['create', 'update'].includes(curType.value) 36 | }) 37 | 38 | const loading = ref(false) 39 | 40 | const handleCreateProject = async (formData: CreateProjectParams) => { 41 | try { 42 | loading.value = true 43 | await createProject(formData) 44 | changeDialogVisible(false) 45 | } finally { 46 | loading.value = false 47 | } 48 | } 49 | 50 | const handleShowProject = async (id: number) => { 51 | try { 52 | loading.value = true 53 | const { data } = await getProjectItem(id) 54 | changeDialogVisible(true) 55 | formData.value = data 56 | } finally { 57 | loading.value = false 58 | } 59 | } 60 | 61 | const handleEditProject = async (newFormData: CreateProjectParams, projectId: number) => { 62 | try { 63 | loading.value = true 64 | await updateProject({ ...newFormData, id: projectId }) 65 | formData.value = newFormData 66 | } finally { 67 | loading.value = false 68 | } 69 | } 70 | 71 | const handleDeleteProject = async (projectId: number) => { 72 | try { 73 | loading.value = true 74 | await deleteProject(projectId) 75 | } finally { 76 | loading.value = false 77 | } 78 | } 79 | 80 | return { 81 | curType, 82 | dialogVisible, 83 | loading, 84 | editable, 85 | formData, 86 | changeCurType, 87 | changeDialogVisible, 88 | handleCreateProject, 89 | handleShowProject, 90 | handleEditProject, 91 | handleDeleteProject 92 | } 93 | } 94 | ) 95 | -------------------------------------------------------------------------------- /src/view/Blog/Create/index.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 81 | 82 | 116 | -------------------------------------------------------------------------------- /src/view/Blog/Update/index.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 82 | 83 | 117 | -------------------------------------------------------------------------------- /src/service/axios/axios.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import message from '@/utils/message' 3 | import type { AxiosResponse, AxiosError, AxiosInstance } from 'axios' 4 | import type { Res, HttpConfig } from './type' 5 | 6 | class Http { 7 | instance: AxiosInstance 8 | showSuccessMsg: boolean // 请求成功后是否自动toast success msg 9 | showErrorMsg: boolean // 请求失败后是否自动toast error msg 10 | 11 | constructor(config: HttpConfig) { 12 | this.instance = axios.create(config) 13 | this.showSuccessMsg = config.showSuccessMsg ?? false 14 | this.showErrorMsg = config.showErrorMsg ?? false 15 | 16 | this.instance.interceptors.request.use( 17 | (config) => { 18 | return config 19 | }, 20 | (error: AxiosError) => { 21 | return error 22 | } 23 | ) 24 | 25 | this.instance.interceptors.response.use( 26 | (response: AxiosResponse) => { 27 | if (this.showSuccessMsg) message.success(response.data.msg) 28 | return Promise.resolve(response.data) 29 | }, 30 | (error: AxiosError) => { 31 | if (error!.response?.data.code) { 32 | if (this.showErrorMsg) message.error(error.response.data.msg) 33 | return Promise.reject(error.response?.data.msg) 34 | } 35 | message.error('Network Error!') 36 | return Promise.reject('Network Error!') 37 | } 38 | ) 39 | 40 | this.instance.interceptors.request.use( 41 | config.interceptors?.requestInterceptor as any, 42 | config.interceptors?.requestInterceptorCatch 43 | ) 44 | this.instance.interceptors.response.use( 45 | config.interceptors?.responseInterceptor, 46 | config.interceptors?.responseInterceptorCatch 47 | ) 48 | } 49 | 50 | request(config: HttpConfig) { 51 | return new Promise((resolve, reject) => { 52 | // 单个接口 53 | if (config.interceptors?.requestInterceptor) { 54 | config = config.interceptors.requestInterceptor(config) 55 | } 56 | if (config.showSuccessMsg === true || config.showSuccessMsg === false) { 57 | this.showSuccessMsg = config.showSuccessMsg 58 | } 59 | if (config.showErrorMsg === true || config.showErrorMsg === false) { 60 | this.showErrorMsg = config.showErrorMsg 61 | } 62 | this.instance 63 | .request(config) 64 | .then((res) => { 65 | if (config.interceptors?.responseInterceptor) { 66 | res = config.interceptors.responseInterceptor(res) 67 | } 68 | resolve(res) 69 | }) 70 | .catch((error: Error) => { 71 | reject(error) 72 | }) 73 | }) 74 | } 75 | 76 | get(config: HttpConfig) { 77 | return this.request({ ...config, method: 'GET' }) 78 | } 79 | 80 | post(config: HttpConfig) { 81 | return this.request({ ...config, method: 'POST' }) 82 | } 83 | 84 | put(config: HttpConfig) { 85 | return this.request({ ...config, method: 'PUT' }) 86 | } 87 | 88 | patch(config: HttpConfig) { 89 | return this.request({ ...config, method: 'PATCH' }) 90 | } 91 | 92 | delete(config: HttpConfig) { 93 | return this.request({ ...config, method: 'DELETE' }) 94 | } 95 | } 96 | 97 | export default Http 98 | -------------------------------------------------------------------------------- /src/view/Project/config.ts: -------------------------------------------------------------------------------- 1 | import type { FormRules } from 'element-plus' 2 | import type { Schema } from '@/libComponents/JForm/type' 3 | 4 | export const projectSchema: Schema[] = [ 5 | { 6 | component: 'input', 7 | key: 'name', 8 | itemAttrs: { 9 | label: 'name', 10 | labelWidth: 130 11 | }, 12 | attrs: { 13 | type: 'text', 14 | placeholder: 'project name' 15 | } 16 | }, 17 | { 18 | component: 'input', 19 | key: 'description', 20 | itemAttrs: { 21 | label: 'description', 22 | labelWidth: 130 23 | }, 24 | attrs: { 25 | type: 'text', 26 | placeholder: 'project description' 27 | } 28 | }, 29 | { 30 | component: 'upload', 31 | key: 'coverIcon', 32 | custom: true, 33 | itemAttrs: { 34 | label: 'cover', 35 | labelWidth: 130 36 | }, 37 | attrs: { 38 | options: [ 39 | { label: 1, value: 1 } 40 | ] 41 | } 42 | }, 43 | { 44 | component: 'select', 45 | key: 'technologyStack', 46 | itemAttrs: { 47 | label: 'technology stack', 48 | labelWidth: 130 49 | }, 50 | attrs: { 51 | options: [ 52 | 'Vue', 53 | 'React', 54 | 'Typescript', 55 | 'Webpack', 56 | 'Vite', 57 | 'Koa' 58 | ].map(i => ({ label: i, value: i })), 59 | multiple: true, 60 | filterable: true, 61 | allowCreate: true 62 | } 63 | }, 64 | { 65 | component: 'select', 66 | key: 'status', 67 | itemAttrs: { 68 | label: 'status', 69 | labelWidth: 130 70 | }, 71 | attrs: { 72 | options: [ 73 | { label: '🟡 pending', value: 'pending' }, 74 | { label: '🟢 doing', value: 'doing' }, 75 | { label: '🔵 done', value: 'done' } 76 | ], 77 | clearable: false 78 | } 79 | }, 80 | { 81 | component: 'input', 82 | key: 'codeAddress', 83 | itemAttrs: { 84 | label: 'code address', 85 | labelWidth: 130 86 | }, 87 | attrs: { 88 | type: 'text', 89 | placeholder: 'code address' 90 | } 91 | }, 92 | { 93 | component: 'input', 94 | key: 'onlineAddress', 95 | itemAttrs: { 96 | label: 'online address', 97 | labelWidth: 130 98 | }, 99 | attrs: { 100 | type: 'text', 101 | placeholder: 'online address' 102 | } 103 | }, 104 | { 105 | component: 'datePicker', 106 | key: 'startAt', 107 | itemAttrs: { 108 | label: 'start time', 109 | labelWidth: 130 110 | }, 111 | attrs: { 112 | placeholder: 'project start time', 113 | format: 'YYYY-MM-DD', 114 | valueFormat: 'YYYY-MM-DD' 115 | } 116 | }, 117 | { 118 | component: 'datePicker', 119 | key: 'doneAt', 120 | itemAttrs: { 121 | label: 'done time', 122 | labelWidth: 130 123 | }, 124 | attrs: { 125 | placeholder: 'project done time', 126 | format: 'YYYY-MM-DD', 127 | valueFormat: 'YYYY-MM-DD' 128 | } 129 | } 130 | ] 131 | 132 | export const projectRules: FormRules = { 133 | name: [ 134 | { 135 | required: true, 136 | message: 'Please Input Activity Username', 137 | trigger: 'blur' 138 | } 139 | ], 140 | description: [], 141 | coverIcon: [ 142 | { 143 | required: true, 144 | message: 'Please Upload cover icon', 145 | trigger: 'blur' 146 | } 147 | ], 148 | technologyStack: [], 149 | status: [], 150 | codeAddress: [], 151 | onlineAddress: [], 152 | startAt: [], 153 | doneAt: [] 154 | } 155 | -------------------------------------------------------------------------------- /src/components/LoginCom/index.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 114 | -------------------------------------------------------------------------------- /src/view/NotFound/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 118 | 119 | 163 | -------------------------------------------------------------------------------- /src/libComponents/JDialog/index.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 120 | 121 | 149 | -------------------------------------------------------------------------------- /src/view/Blog/Item/index.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 108 | 109 | 153 | -------------------------------------------------------------------------------- /src/hooks/useThree.ts: -------------------------------------------------------------------------------- 1 | import { ref, onMounted, onUnmounted } from 'vue' 2 | import { 3 | WebGLRenderer, 4 | Scene, 5 | PerspectiveCamera, 6 | HemisphereLight, 7 | TextureLoader, 8 | SpotLight, 9 | SpotLightHelper, 10 | PlaneGeometry, 11 | MeshLambertMaterial, 12 | Mesh, 13 | PCFSoftShadowMap, 14 | sRGBEncoding, 15 | ACESFilmicToneMapping, 16 | LinearFilter 17 | } from 'three' 18 | import { PLYLoader } from 'three/examples/jsm/loaders/PLYLoader' 19 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' 20 | 21 | const useThree = () => { 22 | const threeRef = ref() 23 | 24 | const renderer = new WebGLRenderer({ antialias: true }) 25 | const scene = new Scene() 26 | const camera = new PerspectiveCamera(35, 0, 1, 1000) 27 | const spotLight = new SpotLight(0xffffff, 5) 28 | const lightHelper = new SpotLightHelper(spotLight) 29 | 30 | const initThree = () => { 31 | renderer.setPixelRatio(window.devicePixelRatio) 32 | 33 | const width = Math.max(threeRef.value?.offsetWidth || 960, 960) 34 | const height = threeRef.value?.offsetHeight || 0 35 | 36 | renderer.setSize(width, height) 37 | 38 | threeRef.value?.appendChild(renderer.domElement) 39 | 40 | renderer.shadowMap.enabled = true 41 | renderer.shadowMap.type = PCFSoftShadowMap 42 | renderer.outputEncoding = sRGBEncoding 43 | renderer.toneMapping = ACESFilmicToneMapping 44 | renderer.toneMappingExposure = 1 45 | renderer.setAnimationLoop(render) 46 | 47 | camera.aspect = width / height 48 | camera.position.set(70, 50, 10) 49 | camera.updateProjectionMatrix() 50 | 51 | const controls = new OrbitControls(camera, renderer.domElement) 52 | controls.minDistance = 20 53 | controls.maxDistance = 100 54 | controls.maxPolarAngle = Math.PI / 2 55 | controls.target.set(0, 18, 0) 56 | controls.update() 57 | 58 | const ambient = new HemisphereLight(0xffffff, 0x444444, 0.05) 59 | scene.add(ambient) 60 | 61 | new TextureLoader() 62 | .setPath('three/') 63 | .loadAsync('disturb.jpg') 64 | .then(texture => { 65 | texture.minFilter = LinearFilter 66 | texture.magFilter = LinearFilter 67 | texture.encoding = sRGBEncoding 68 | spotLight.map = texture 69 | }) 70 | 71 | spotLight.position.set(25, 50, 25) 72 | spotLight.angle = Math.PI / 6 73 | spotLight.penumbra = 1 74 | spotLight.decay = 2 75 | spotLight.distance = 100 76 | spotLight.castShadow = true 77 | spotLight.shadow.mapSize.width = 1024 78 | spotLight.shadow.mapSize.height = 1024 79 | spotLight.shadow.camera.near = 10 80 | spotLight.shadow.camera.far = 200 81 | spotLight.shadow.focus = 1 82 | 83 | scene.add(spotLight) 84 | scene.add(lightHelper) 85 | 86 | const geometry = new PlaneGeometry(1000, 1000) 87 | const material = new MeshLambertMaterial({ color: 0x808080 }) 88 | 89 | const mesh = new Mesh(geometry, material) 90 | mesh.position.set(0, -1, 0) 91 | mesh.rotation.x = -Math.PI / 2 92 | mesh.receiveShadow = true 93 | scene.add(mesh) 94 | 95 | new PLYLoader().load('three/LeslieXin.ply', (geometry) => { 96 | geometry.scale(0.024, 0.024, 0.024) 97 | geometry.computeVertexNormals() 98 | 99 | const material = new MeshLambertMaterial() 100 | const mesh = new Mesh(geometry, material) 101 | mesh.rotation.y = -Math.PI / 2 102 | mesh.position.y = 18 103 | mesh.castShadow = true 104 | mesh.receiveShadow = true 105 | scene.add(mesh) 106 | }) 107 | 108 | window.addEventListener('resize', onWindowResize) 109 | } 110 | 111 | const onWindowResize = () => { 112 | const width = Math.max(threeRef.value?.offsetWidth || 960, 960) 113 | const height = threeRef.value?.offsetHeight || 0 114 | camera.aspect = width / height 115 | camera.updateProjectionMatrix() 116 | renderer.setSize(width, height) 117 | } 118 | 119 | const render = () => { 120 | const time = performance.now() / 3000 121 | spotLight.position.x = Math.cos(time) * 25 122 | spotLight.position.z = Math.sin(time) * 25 123 | lightHelper.update() 124 | renderer.render(scene, camera) 125 | } 126 | 127 | onMounted(() => { 128 | threeRef.value && initThree() 129 | }) 130 | 131 | onUnmounted(() => { 132 | window.removeEventListener('resize', onWindowResize) 133 | }) 134 | 135 | return { threeRef } 136 | } 137 | 138 | export default useThree 139 | -------------------------------------------------------------------------------- /src/components/PageHeader/index.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 163 | 164 | 202 | -------------------------------------------------------------------------------- /src/view/Project/index.vue: -------------------------------------------------------------------------------- 1 | 166 | 167 | 341 | 342 | 483 | -------------------------------------------------------------------------------- /src/view/NotFound/config.ts: -------------------------------------------------------------------------------- 1 | const fill = 'none' 2 | 3 | export const svgCode = ` 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 30 | 31 | 41 | 42 | 52 | 53 | 62 | 63 | 73 | 74 | 75 | 76 | 77 | 78 | 89 | 90 | 101 | 102 | 103 | 104 | 115 | 116 | 127 | 128 | 129 | 130 | 141 | 142 | 153 | 154 | 155 | 156 | 167 | 168 | 179 | 180 | 181 | 182 | 183 | 184 | 195 | 196 | 207 | 208 | 209 | 210 | 221 | 222 | 233 | 234 | 235 | 236 | 247 | 248 | 259 | 260 | 261 | 262 | 273 | 274 | 285 | 286 | 287 | 288 | 299 | 300 | 311 | 312 | 313 | 314 | 325 | 326 | 337 | 338 | 339 | 340 | 341 | 351 | 352 | 362 | 363 | 373 | 374 | 384 | 385 | 395 | 396 | 406 | 407 | 417 | 418 | 428 | 429 | 430 | 431 | 437 | 438 | 444 | 445 | 451 | 452 | 458 | 459 | 465 | 466 | 472 | 473 | 479 | 480 | 481 | 482 | 483 | 493 | 494 | 503 | 504 | 505 | 517 | 518 | 529 | 530 | 531 | 532 | 541 | 542 | 551 | 552 | 553 | 554 | 563 | 564 | 573 | 574 | 575 | 576 | 585 | 586 | 595 | 596 | 597 | 598 | 599 | 608 | 609 | 621 | 622 | 623 | 624 | 633 | 634 | 646 | 647 | 648 | 649 | 650 | 651 | 660 | 661 | 671 | 672 | 681 | 682 | 683 | 691 | 692 | 693 | 694 | 695 | 696 | ` 697 | --------------------------------------------------------------------------------