提交 a19859ad 编写于 作者: X xjh22222228

feat: Copy url

feat: SEO support
fix: bug
上级 b379bba7
......@@ -5,7 +5,7 @@
</a>
<br />
<b>发现导航</b>
<p align="center">一个纯静态、易管理的强大导航网站,希望您会喜欢</p>
<p align="center">一个纯静态、支持SEO、在线编辑的强大导航网站,希望您会喜欢</p>
<p align="center">内置收录多达 800+ 优质网站, 助您工作、学习和生活</p>
<p align="center">
<img src="https://img.shields.io/github/v/release/xjh22222228/nav" />
......@@ -50,20 +50,21 @@
## 拥有出色的特性
`发现导航` 的理念就是做一款无需依赖后端服务既简单又方便,没有繁杂的配置和数据库等配置概念, 做到开箱即用。
- [√] 内置 `800+` 实用网站。
- [√] 三叉树分类、结构清晰、分类清晰。
- [√] 颜值与简约并存,不再是杀马特时代。
- [√] 支持3种浏览模式,创新。
- [√] 支持足迹记忆。
- [√] 支持移动端浏览。
- [√] 支持搜索查询。
- [√] 支持自定义引擎搜索。
- [√] 纯静态, 提供自动化部署功能。
- [√] 完全开源,轻松定制化。
- [√] 多款主题切换。
- [√] 支持暗黑模式。
- [√] 支持快捷键操作,一步到位。
- [√] 支持在线新增数据, 没有传统的后台概念。
- 🍰 内置 `800+` 实用网站。
- 🍰 支持SEO, 没有可不行。
- 🍰 完全纯静态, 提供自动化部署功能。
- 🍰 三叉树分类、结构清晰、分类清晰。
- 🍰 颜值与简约并存,不再是杀马特时代。
- 🍰 支持多种浏览模式,创新。
- 🍰 支持足迹记忆。
- 🍰 支持移动端浏览。
- 🍰 支持搜索查询。
- 🍰 支持自定义引擎搜索。
- 🍰 完全开源,轻松定制化。
- 🍰 多款主题切换。
- 🍰 支持暗黑模式。
- 🍰 支持快捷键操作,一步到位。
- 🍰 支持在线新增数据, 没有传统的后台概念。
......
......@@ -5,6 +5,12 @@ const c: IConfig = {
// [必填], 请填写您的仓库地址
gitRepoUrl: 'https://github.com/xjh22222228/nav',
// 路由是否Hash模式, 如果是部署在github pages 务必设为 true
hashMode: false,
// 您的网站地址,这对于SEO很重要
homeUrl: 'https://nav3.cn',
// 网站标题
title: '发现导航 - 精选实用导航网站',
......
OK
\ No newline at end of file
......@@ -3094,6 +3094,16 @@
"integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==",
"dev": true
},
"clipboard": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.6.tgz",
"integrity": "sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg==",
"requires": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
"tiny-emitter": "^2.0.0"
}
},
"cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
......@@ -4118,6 +4128,11 @@
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true
},
"delegate": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
"integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw=="
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
......@@ -5611,6 +5626,14 @@
"slash": "^3.0.0"
}
},
"good-listener": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
"integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=",
"requires": {
"delegate": "^3.1.2"
}
},
"graceful-fs": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
......@@ -10524,6 +10547,11 @@
}
}
},
"select": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
"integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0="
},
"select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
......@@ -11831,6 +11859,11 @@
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
"dev": true
},
"tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
},
"tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
......
// Copyright @ 2018-2021 xiejiahe. All rights reserved. MIT license.
// See https://github.com/xjh22222228/nav
import fs from 'fs'
import config from '../nav.config.js'
import path from 'path'
const dbPath = path.join('.', 'data', 'db.json')
function addZero(num) {
return num < 10 ? '0' + num : num
}
const now = new Date()
console.log('Timezone: ', now.getTimezoneOffset())
now.setHours(now.getHours() + 8)
const date = `${now.getFullYear()}${addZero(now.getMonth() + 1)}${addZero(now.getDate())}${addZero(now.getHours())}:${addZero(now.getMinutes())}:${addZero(now.getSeconds())}`
const {
gitRepoUrl,
homeUrl,
description,
title,
keywords,
......@@ -41,10 +47,39 @@ s.parentNode.insertBefore(hm, s);
let scriptTemplate = `
${baiduScript}
${cnzzScript}
<span data-date="${date}" id="BUILD-DATE-NAV"></span>
<span style="display:none;" data-date="${date}" id="BUILD-DATE-NAV"></span>
`.trim()
try {
let seoTemplate = `
<div data-url="https://github.com/xjh22222228/nav" style="z-index:-1;position:fixed;top:-10000vh;left:-10000vh;">
`
async function buildSeo() {
const readDb = fs.readFileSync(dbPath).toString()
const parseDbJson = JSON.parse(readDb)
function r(navList) {
for (let value of navList) {
if (Array.isArray(value.nav)) {
r(value.nav)
}
seoTemplate += `<h3>${value.title || value.name || title}</h3>${value.icon ? `<img src="${value.icon}" alt="${homeUrl}" />` : ''}<p>${value.desc || description}</p><a href="${value.url || homeUrl || gitRepoUrl}"></a>`
if (value.urls && typeof value.urls === 'object') {
for (let k in value.urls) {
seoTemplate += `<a href="${value.urls[k] || homeUrl || gitRepoUrl}"></a>`
}
}
}
}
r(parseDbJson)
seoTemplate += '</div>'
}
async function build() {
fs.copyFileSync(
path.join('.', 'logo.png'),
path.join('.', 'src', 'assets', 'logo.png')
......@@ -58,9 +93,13 @@ try {
t = t.replace('<!-- nav.script -->', scriptTemplate)
}
t = t.replace('<!-- nav.seo -->', seoTemplate)
fs.writeFileSync(htmlPath, t, { encoding: 'utf-8' })
fs.unlinkSync('./nav.config.js')
console.log('Build done!')
} catch (error) {
console.log(error)
}
buildSeo()
.finally(() => build())
.catch(console.error)
......@@ -103,7 +103,7 @@ const appRoutes: Routes = [
appRoutes,
{
enableTracing: false, // <-- debugging purposes only
useHash: true,
useHash: config.hashMode,
}
),
HttpClientModule,
......
......@@ -4,11 +4,10 @@ import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'
import { setWebsiteList, getLogoUrl } from '../../utils'
import { NzMessageService } from 'ng-zorro-antd/message'
import { NzNotificationService } from 'ng-zorro-antd/notification'
import { updateFileContent } from '../../services'
import { verifyToken } from '../../services'
import { getToken, setToken } from '../../utils/user'
import { FormBuilder, FormGroup, Validators } from '@angular/forms'
import { websiteList } from '../../store'
import { VERIFY_PATH } from '../../constants'
@Component({
selector: 'app-create',
......@@ -100,18 +99,14 @@ export class CreateComponent implements OnInit {
}
this.submiting = true
updateFileContent({
message: 'verify',
content: 'OK',
path: VERIFY_PATH
}, this.token)
verifyToken(this.token)
.then(() => {
setToken(this.token);
this.message.success('登录成功, 2秒后刷新!')
this.message.success('Token验证成功, 2秒后刷新!')
setTimeout(() => window.location.reload(), 2000)
})
.catch(res => {
this.notification.error('登录失败, 请填写正确Token', res.message as string)
this.notification.error('Token 验证失败', res.message as string)
})
.finally(() => {
this.submiting = false
......
<div
class="container"
nz-dropdown
[nzDropdownMenu]="menu"
[nzDisabled]="(!isEditing.value || hasKeyword())"
[nzClickHide]="false"
>
<ng-content></ng-content>
<div *ngIf="getEditType() === EditType.isWebsite" class="icon-wrapper">
<div nz-tooltip [nzTooltipTitle]="copyPathDone ? '复制成功' : '分享网站'">
<i
class="iconfont"
[class.iconweibiaoti14]="copyPathDone"
[class.iconfenxiang]="!copyPathDone"
(click)="copyUrl($event, 1)"
(mouseout)="copyMouseout()"
>
</i>
</div>
<div nz-tooltip [nzTooltipTitle]="copyUrlDone ? '复制成功' : '复制链接'">
<i
class="iconfont copy"
[class.iconweibiaoti14]="copyUrlDone"
[class.iconcopy]="!copyUrlDone"
(click)="copyUrl($event, 2)"
(mouseout)="copyMouseout()"
>
</i>
</div>
</div>
</div>
<nz-dropdown-menu #menu="nzDropdownMenu">
<ul nz-menu nzSelectable>
......@@ -68,4 +92,3 @@
</form>
</ng-container>
</nz-modal>
.container {
position: relative;
&:hover {
.icon-wrapper {
display: block;
}
.iconfont {
transform: translateX(0) !important;
}
}
.icon-wrapper {
position: absolute;
top: 0;
right: -18px;
height: 100%;
display: none;
}
.iconfont {
display: block;
font-size: 17px;
cursor: pointer;
transform: translateX(-100px);
transition: 1s linear;
}
.copy {
font-size: 18px;
}
.iconweibiaoti14 {
color: rgb(19, 247, 19);
}
}
\ No newline at end of file
......@@ -3,11 +3,12 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'
import { NzMessageService } from 'ng-zorro-antd/message'
import { NzNotificationService } from 'ng-zorro-antd/notification'
import { getToken } from '../../utils/user'
import { setWebsiteList, queryString, getLogoUrl } from '../../utils'
import { setWebsiteList, queryString, getLogoUrl, copyText } from '../../utils'
import { websiteList, isEditing } from '../../store'
import { Router } from '@angular/router'
import { setAnnotate } from '../../utils/ripple'
import { INavProps } from '../../types'
import config from '../../../nav.config'
enum EditType {
isOne,
......@@ -35,6 +36,8 @@ export class DropdownComponent implements OnInit {
showModal = false
EditType = EditType
iconUrl = ''
copyUrlDone = false
copyPathDone = false
constructor(
private fb: FormBuilder,
......@@ -61,6 +64,29 @@ export class DropdownComponent implements OnInit {
this.validateForm.get('icon')!.setValue(res || '')
}
async copyUrl(e, type: number) {
if (this.fourIdx >= 0) {
const w = this.websiteList[this.oIdx]
.nav[this.twoIdx]
.nav[this.threeIdx]
.nav[this.fourIdx]
const { origin, hash, pathname } = window.location
const pathUrl = `${origin}${pathname}${hash}?q=${w.name}&url=${encodeURIComponent(w.url)}`
const isDone = await copyText(e, type === 1 ? pathUrl : w.url)
if (type === 1) {
this.copyPathDone = isDone
} else {
this.copyUrlDone = isDone
}
}
}
copyMouseout() {
this.copyUrlDone = false
this.copyPathDone = false
}
onIconBlur(e) {
this.iconUrl = e.target.value
}
......@@ -86,18 +112,39 @@ export class DropdownComponent implements OnInit {
title = title.trim()
if (type === EditType.isOne) {
const exists = this.websiteList.some((item, idx) => (
item.title === title && idx !== this.oIdx
))
if (exists) {
this.message.error(`已存在 ${title}, 不可重复添加`)
return
}
this.websiteList[this.oIdx].title = title
this.websiteList[this.oIdx].icon = icon
}
if (type === EditType.isTwo) {
const w = this.websiteList[this.oIdx].nav
const exists = w.some((item, idx) => (
item.title === title && idx !== this.twoIdx
))
if (exists) {
this.message.error(`已存在 ${title}, 不可重复添加`)
return
}
w[this.twoIdx].title = title
w[this.twoIdx].icon = icon
}
if (type === EditType.isThree) {
const w = this.websiteList[this.oIdx].nav[this.twoIdx].nav
const exists = w.some((item, idx) => (
item.title === title && idx !== this.threeIdx
))
if (exists) {
this.message.error(`已存在 ${title}, 不可重复添加`)
return
}
w[this.threeIdx].title = title
w[this.threeIdx].icon= icon
}
......@@ -107,6 +154,13 @@ export class DropdownComponent implements OnInit {
.nav[this.twoIdx]
.nav[this.threeIdx]
.nav
const exists = w.some((item, idx) => (
item.name === title && idx !== this.fourIdx
))
if (exists) {
this.message.error(`已存在 ${title}, 不可重复添加`)
return
}
w[this.fourIdx].name = title
w[this.fourIdx].icon = icon
w[this.fourIdx].url = url
......
// Copyright @ 2018-2021 xiejiahe. All rights reserved. MIT license.
// See https://github.com/xjh22222228/nav
import hotkeys from 'hotkeys-js'
import { Component, Output, EventEmitter, Input } from '@angular/core'
......@@ -9,7 +10,7 @@ import { NzNotificationService } from 'ng-zorro-antd/notification'
import { getToken } from '../../utils/user'
import { updateFileContent } from '../../services'
import { websiteList, isEditing } from '../../store'
import { DB_PATH, KEY_MAP } from '../../constants'
import { DB_PATH, KEY_MAP, VERSION } from '../../constants'
import { Router } from '@angular/router'
import { setAnnotate } from '../../utils/ripple'
......@@ -92,6 +93,8 @@ export class FixbarComponent {
nzContent: `
<p>Token: ${getToken()}</p>
<p>上次构建时间: ${date || '未知'}</p>
<p>当前版本: <img src="https://img.shields.io/badge/release-v${VERSION}-red.svg?longCache=true&style=flat-square"></p>
<p>最新版本: <img src="https://img.shields.io/github/v/release/xjh22222228/nav" /></p>
`,
});
}
......
......@@ -7,7 +7,7 @@
padding: 10px 0 5px 0px;
background: #fbfbfb;
cursor: auto;
display: flex;
display: none;
justify-content: center;
align-content: center;
transition: .1s linear;
......
......@@ -3,8 +3,6 @@ function isMac() {
return /mac os x/i.test(navigator.userAgent.toLowerCase());
}
export const VERIFY_PATH = 'nav.verify.txt'
export const DB_PATH = 'data/db.json'
export const VERSION = '5.0.3'
......
......@@ -18,6 +18,16 @@ try {
branchName = p[2]
} catch {}
// 验证Token
export function verifyToken(token: string) {
return http.get(`/users/${authorName}`, {
headers: {
Authorization: `token ${token}`
}
})
}
// 获取文件信息
export function getFileContent(path: string, authToken?: string) {
const _token = `${authToken ? authToken : token}`.trim()
......
......@@ -44,7 +44,9 @@ export interface ISearchEngineProps {
}
export interface IConfig {
gitRepoUrl: string,
gitRepoUrl: string
hashMode: boolean
homeUrl?: string
title: string
description: string
keywords: string
......
......@@ -2,10 +2,10 @@
import qs from 'qs'
import config from '../../nav.config'
import hotkeys from 'hotkeys-js'
import Clipboard from 'clipboard'
import { INavProps, ISearchEngineProps } from '../types'
import * as db from '../../data/db.json'
import { KEY_MAP } from '../constants'
import { Target } from '@angular/compiler'
export const websiteList = getWebsiteList()
......@@ -134,12 +134,13 @@ export function queryString() {
const parseQs = qs.parse(search)
let id = parseInt(parseQs.id) || 0
let page = parseInt(parseQs.page) || 0
let localLocation = {}
if (parseQs.id === undefined && parseQs.page === undefined) {
try {
const location = window.localStorage.getItem('location')
if (location) {
return JSON.parse(location)
localLocation = JSON.parse(location)
}
} catch {}
}
......@@ -160,7 +161,8 @@ export function queryString() {
...parseQs,
q: parseQs.q || '',
id,
page
page,
...localLocation
}
}
......@@ -311,3 +313,25 @@ export async function getLogoUrl(url: string): Promise<boolean|string> {
return null
}
}
export function copyText(el: any, text: string): Promise<boolean> {
const target = el.target
const ranId = 'copy-' + randomInt(99999999)
target.id = ranId
target.setAttribute('data-clipboard-text', text)
return new Promise(resolve => {
const clipboard = new Clipboard(`#${ranId}`)
clipboard.on('success', function() {
clipboard?.destroy?.()
target.removeAttribute('id')
resolve(true)
});
clipboard.on('error', function() {
clipboard?.destroy?.()
target.removeAttribute('id')
resolve(false)
});
})
}
......@@ -163,12 +163,12 @@
width: 200px;
margin: 12px;
border: 1px solid #eee;
overflow: hidden;
&:hover {
::ng-deep .mark {
bottom: 0 !important;
display: flex;
}
}
}
......
......@@ -123,11 +123,9 @@ $width: 1200px;
li {
position: relative;
width: 225px;
padding: 15px;
margin: 20px 20px 0 0;
transition: .1s ease-out;
border: 1px solid #eee;
overflow: hidden;
border-radius: 3px;
cursor: pointer;
......@@ -140,6 +138,7 @@ $width: 1200px;
::ng-deep .mark {
bottom: 0 !important;
display: flex;
}
}
}
......@@ -149,6 +148,7 @@ $width: 1200px;
.box-wrapper {
display: flex;
padding: 10px;
}
.right {
......@@ -164,8 +164,9 @@ $width: 1200px;
}
.desc {
margin-top: 8px;
color: #4c5d73;
padding: 10px;
font-size: 13px;
}
}
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册